Introduction
POTA transforms IoT development by providing a complete dashboard system directly in your Arduino code. Create sophisticated web interfaces with real-time updates, interactive controls, and data visualization—all without writing HTML, CSS, or JavaScript.
This guide covers the complete public API of the POTA library, showing you how to build production-ready IoT dashboards using only C++ on your microcontroller.
If this is your first time using POTA and you want to see what a dashboard can look like and explore its full potential, you can check out a live demo by clicking the following link: DEMO DASHBOARD
Want to integrate the POTA library into your project quickly?
Simply upload your code to PCGen, specify that you want to use the POTA library, and describe how your custom dashboard should be built in the requirements. PCGen will generate it for you in no time.
Quick Start
Installation
-
- Open Library Manager (Sketch → Include Library → Manage Libraries)
- Search for “POTA”
- Install version 2.0.0 or higher
[env:esp32]
platform = espressif32
lib_deps = pleasedontcode/POTA@^2.0.0
Minimal Example
#include <POTA.h>
POTA pota;
uint8_t tempWidget;
void setupDashboard() {
tempWidget = pota.dashboard.addWidget(
TEMPERATURE_CARD,
"Room Temperature"
);
}
void setup() {
Serial.begin(115200);
// Initialize POTA with WiFi management
POTAError err = pota.begin(
"XIAO_ESP32S3", // Device type
"1.0.0", // Firmware version
"your_auth_token", // From POTA service
"your_secret_key", // From POTA service
"WiFi_SSID", // WiFi network
"WiFi_Password" // WiFi password
);
if (err != POTAError::SUCCESS) {
Serial.println(POTA::errorToString(err));
return;
}
// Configure dashboard widgets
pota.dashboard.setWidgetConfigCallback(setupDashboard);
}
void loop() {
pota.loop(); // Handle WebSocket and dashboard updates
// Update temperature value
float temp = readTemperatureSensor(); // simulated value
pota.dashboard.setValue(tempWidget, temp);
pota.dashboard.sendUpdates();
delay(500);
}
float readTemperatureSensor() {
static float t = 20.0;
t += random(-5, 6) * 0.1;
return t;
}
Core API Reference
POTA Class
The main class providing OTA updates and dashboard access.
POTA pota;
begin()
Initializes the POTA library with optional WiFi management.
POTAError begin(
const char* deviceType,
const char* firmwareVersion,
const char* authToken,
const char* serverSecret,
const char* ssid = nullptr,
const char* password = nullptr
);
-
deviceType: Device identifier (e.g., “ESP32_DEVKIT_V1”, “ESP8266_NODEMCU_V1_0”, “ARDUINO_OPTA_WIFI”)firmwareVersion: Current firmware version (e.g., “1.0.0”, “2.3.1”)authToken: Authentication token from POTA serviceserverSecret: Secret key for secure communicationssid: WiFi SSID (optional - omit if managing WiFi yourself)password: WiFi password (required if ssid provided)
Returns: POTAError - Use errorToString() for description
WiFi Management Modes:
pota.begin("ESP32_DEVKIT_V1", "1.0.0", token, secret, "MyWiFi", "password123");
-
- Connects to WiFi automatically
- Handles reconnection
- Blocks until connected (30s timeout)
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) delay(500);
pota.begin("ESP32_DEVKIT_V1", "1.0.0", token, secret); // No WiFi params
-
- More control over WiFi behavior
- Can implement custom reconnection logic
- Useful for complex network scenarios
loop()
Main processing function - call from Arduino loop().
void loop(bool enableAutoReconnect = true);
-
- Maintains WebSocket connection
- Processes incoming messages
- Sends pending dashboard updates
- Handles OTA notifications
- Auto-reconnects if connection drops (with backoff)
Parameters:
-
enableAutoReconnect: Enable automatic WebSocket reconnection (default: true)
void loop() {
pota.loop(); // Always call first
// Your application code
updateSensors();
updateDashboard();
delay(100);
}
getSecureMACAddress()
Returns the device’s unique MAC address.
String getSecureMACAddress();
-
- ESP32: eFuse MAC (factory-programmed, tamper-resistant)
- ESP8266: WiFi interface MAC
- Arduino Opta: Board information MAC
String mac = pota.getSecureMACAddress();
Serial.print("Device MAC: ");
Serial.println(mac);
checkAndPerformOTA()
Checks for firmware updates and installs if available.
POTAError checkAndPerformOTA();
-
POTAError::SUCCESS- Update successful (device will reboot - this never returns)POTAError::NO_UPDATE_AVAILABLE- Already up-to-date- Other
POTAErrorvalues - Check failed or update failed
Process:
-
- Queries POTA API for available updates
- Verifies server response authenticity
- Downloads firmware binary if update available
- Flashes new firmware
- Reboots device ← Important!
POTAError err = pota.checkAndPerformOTA();
if (err == POTAError::SUCCESS) {
// Never reached - device reboots on success
} else if (err == POTAError::NO_UPDATE_AVAILABLE) {
Serial.println("Firmware up-to-date");
} else {
Serial.print("OTA failed: ");
Serial.println(POTA::errorToString(err));
}
⚠️ Warning: This function reboots your device on successful update! Save any critical data before calling.
performOTA()
Performs OTA update from specific URL.
POTAError performOTA(const char* firmwareURL);
-
firmwareURL: HTTPS URL of firmware binary
Returns:
-
POTAErrorcode
const char* url = "https://example.com/firmware/v2.0.0.bin";
POTAError err = pota.performOTA(url);
⚠️ Warning: Device reboots on successful update.
onOTAAvailable()
Registers callback for OTA availability notifications.
void onOTAAvailable(POTACallback callback);
typedef void (*POTACallback)(const char* version);
-
- Server pushes “new firmware available” notification via WebSocket - Real-time (no polling required)
pota.onOTAAvailable([](const char* version) {
Serial.print("New firmware available: ");
Serial.println(version);
// Option 1: Auto-update immediately
pota.checkAndPerformOTA();
// Option 2: User confirmation
if (userConfirmsUpdate()) {
pota.checkAndPerformOTA();
}
// Option 3: Schedule for later
scheduleOTAUpdate(version);
});
restart()
Safely restarts the device.
void restart();
-
- ESP32/ESP8266: Calls
ESP.restart() - Arduino Opta: Properly shuts down WiFi before reset
- ESP32/ESP8266: Calls
Serial.println("Rebooting in 3 seconds...");
delay(3000);
pota.restart(); // Never returns
errorToString()
Converts error code to human-readable string.
static const char* errorToString(POTAError err);
POTAError err = pota.begin(...);
if (err != POTAError::SUCCESS) {
Serial.print("POTA Error: ");
Serial.println(POTA::errorToString(err));
}
POTAError Enum
Error codes returned by POTA functions:
| Error Code | Description | |
|---|---|---|
SUCCESS |
Operation completed successfully | |
PARAMETER_INVALID_SSID |
SSID is null or empty | |
PARAMETER_INVALID_PASSWORD |
Password is null or empty | |
PARAMETER_INVALID_DEVICETYPE |
Device type is invalid | |
PARAMETER_INVALID_FWVERSION |
Firmware version is invalid | |
PARAMETER_INVALID_AUTHTOKEN |
Auth token is invalid | |
PARAMETER_INVALID_SECRET |
Secret key is invalid | |
PARAMETER_INVALID_OUTPUT |
Output buffer is null or too small | |
PARAMETER_INVALID_OTA_URL |
OTA URL parameter is invalid | |
WIFI_CONNECT_FAILED |
WiFi connection timeout | |
CLIENT_NOT_INITIALIZED |
Wi-Fi client was not initialized (call begin first) | |
CONNECTION_FAILED |
Cannot connect to POTA server | |
JSON_PARSE_FAILED |
Invalid server response | |
TOKEN_GENERATION_FAILED |
Failed to generate HMAC-SHA256 token | |
TOKEN_MISMATCH |
Security verification failed | |
NO_UPDATE_AVAILABLE |
Firmware is up-to-date | |
OTA_FAILED |
OTA process failed (generic) | |
OTA_DOWNLOAD_FAILED |
Firmware download failed | |
OTA_DECOMPRESSION_FAILED |
OTA firmware decompression failed (Portenta) | |
OTA_APPLY_FAILED |
OTA firmware application failed | |
OTA_NOT_CAPABLE |
Portenta bootloader too old or not OTA-capable | |
OTA_BEGIN_FAILED |
OTA initialization failed | |
OTA_WIFI_FW_MISSING |
Arduino OPTA Wi-Fi firmware not installed | |
PLATFORM_NOT_SUPPORTED |
Board not supported | |
BUFFER_OVERFLOW_REQUEST |
Buffer overflow while building JSON request | |
BUFFER_OVERFLOW_RESPONSE |
Buffer overflow while reading server response | |
CERTIFICATE_MISSING |
Root CA certificate not found | |
SERVER_ERROR_4XX |
Server returned HTTP 4xx error |
Dashboard API Reference
Access the dashboard through pota.dashboard.
Widget Types
All available widget types (use in addWidget()):
Display Widgets:
GENERIC_CARD // Generic text/value display
TEMPERATURE_CARD // Temperature with icon
HUMIDITY_CARD // Humidity percentage
AIR_CARD // Air quality (CO2, VOC, PM)
ENERGY_CARD // Power/energy metrics
FEEDBACK_CARD // Status indicator (5 states)
PROGRESS_CARD // Progress bar (0-100%)
INDICATOR_BUTTON_CARD // Button with status indicator
SEPARATOR_CARD // Visual section divider
Control Widgets:
TOGGLE_BUTTON_CARD // On/Off switch
SLIDER_CARD // Numeric slider
RANGE_SLIDER_CARD // Min/max range selector
DROPDOWN_CARD // Selection menu
TEXT_INPUT_CARD // Text input field
PASSWORD_CARD // Masked password input
COLOR_PICKER_CARD // RGB color selector
PUSH_BUTTON_CARD // Momentary button
ACTION_BUTTON_CARD // Critical action button
Joystick Widgets:
JOYSTICK_2D_CARD // 2D joystick (X+Y)
JOYSTICK_X_CARD // Horizontal only
JOYSTICK_Y_CARD // Vertical only
Time Widgets:
TIME_SYNC_CARD // Time synchronization
WEEK_SELECTION_CARD // Day-of-week selector
TIME_DIFFERENCE_CARD // Elapsed time display
Chart Widgets:
BAR_CHART // Vertical/horizontal bars
LINE_CHART // Line graph
AREA_CHART // Filled area chart
PIE_CHART // Pie chart
DOUGHNUT_CHART // Doughnut chart
Media Widgets:
LINK_CARD // Clickable hyperlink
IMAGE_CARD // Image display
FILE_UPLOAD_CARD // File upload interface
Layout Widgets:
TAB // Tab container
addWidget()
Creates a new dashboard widget.
uint8_t addWidget(
WidgetType type,
const char* name,
uint8_t tab_id = 0,
const char* extra = "",
float step = 1.0,
float min = 0.0,
float max = 100.0
);
-
type: Widget type (see Widget Types above)name: Display label (max 64 characters)tab_id: Parent tab ID (0 = main tab, optional)extra: Extra configuration (widget-specific, optional)step: Value increment for sliders (default: 1.0)min: Minimum value for sliders/progress (default: 0.0)max: Maximum value for sliders/progress (default: 100.0)
Returns: Widget ID (0-255) - store this to update the widget later
uint8_t tempCard = pota.dashboard.addWidget(
TEMPERATURE_CARD,
"Living Room"
);
uint8_t controlsTab = pota.dashboard.addWidget(TAB, "Controls");
uint8_t fanToggle = pota.dashboard.addWidget(
TOGGLE_BUTTON_CARD,
"Fan",
controlsTab // Widget appears in Controls tab
);
uint8_t brightness = pota.dashboard.addWidget(
SLIDER_CARD,
"LED Brightness",
0, // Main tab
"%", // Unit suffix
5.0, // Step by 5
0.0, // Min 0
100.0 // Max 100
);
setValue()
Updates a widget’s value.
bool setValue(uint8_t widget_id, value);
-
intfloatboolStringconst char*
Returns: true on success, false if widget not found
// Display widgets
pota.dashboard.setValue(tempCard, 23.5f); // float
pota.dashboard.setValue(humidityCard, 65); // int
pota.dashboard.setValue(statusCard, STATUS_SUCCESS); // int constant
// Control widgets
pota.dashboard.setValue(toggle, true); // bool
pota.dashboard.setValue(slider, 75.0f); // float
pota.dashboard.setValue(dropdown, "Auto"); // String
// Text widgets
pota.dashboard.setValue(textInput, "Device_01");
pota.dashboard.setValue(infoCard, "System OK");
Important: Changes are queued until you call sendUpdates().
sendUpdates()
Transmits all pending widget updates to connected dashboards.
void sendUpdates(bool force = false);
-
force: Send all widgets regardless of changes (default: false)
void loop() {
pota.loop();
// Update multiple widgets
pota.dashboard.setValue(tempCard, readTemp());
pota.dashboard.setValue(humidityCard, readHumidity());
pota.dashboard.setValue(pressureCard, readPressure());
// Send all changes in one transmission
pota.dashboard.sendUpdates();
delay(2000);
}
// Force send everything (useful after reconnection)
pota.dashboard.sendUpdates(true);
onUpdate()
Registers callback for widget interactions.
void onUpdate(uint8_t widget_id, WidgetCallback callback);
void callback(WidgetData data);
uint8_t pumpToggle = pota.dashboard.addWidget(
TOGGLE_BUTTON_CARD,
"Water Pump"
);
pota.dashboard.onUpdate(pumpToggle, [](WidgetData data) {
bool state = data.getBool();
digitalWrite(PUMP_PIN, state ? HIGH : LOW);
Serial.print("Pump: ");
Serial.println(state ? "ON" : "OFF");
});
uint8_t brightness = pota.dashboard.addWidget(
SLIDER_CARD,
"Brightness",
0, "%", 1.0, 0.0, 100.0
);
pota.dashboard.onUpdate(brightness, [](WidgetData data) {
int value = data.getInt();
analogWrite(LED_PIN, map(value, 0, 100, 0, 255));
});
uint8_t joystick = pota.dashboard.addWidget(
JOYSTICK_2D_CARD,
"Robot Control"
);
pota.dashboard.onUpdate(joystick, [](WidgetData data) {
float x = data.x; // -1.0 to 1.0
float y = data.y; // -1.0 to 1.0
const char* dir = data.direction; // "up","down","left","right","idle"
controlRobot(x, y);
});
setX() / setY()
Sets chart axis data.
bool setX(uint8_t widget_id, array_pointer, size_t size);
bool setY(uint8_t widget_id, array_pointer, size_t size);
-
int*- Integer valuesfloat*- Decimal valuesconst char* const*- String labels (X-axis only)
Returns:
-
trueon success
uint8_t waterChart = pota.dashboard.addWidget(
BAR_CHART,
"Weekly Water Usage"
);
const char* days[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
float liters[] = {45.2, 52.1, 38.7, 61.3, 49.8, 55.2, 42.6};
pota.dashboard.setX(waterChart, days, 7);
pota.dashboard.setY(waterChart, liters, 7);
pota.dashboard.sendUpdates();
uint8_t tempChart = pota.dashboard.addWidget(
LINE_CHART,
"24h Temperature"
);
int hours[24];
float temps[24];
// Populate arrays...
for (int i = 0; i < 24; i++) {
hours[i] = i;
temps[i] = temperatureHistory[i];
}
pota.dashboard.setX(tempChart, hours, 24);
pota.dashboard.setY(tempChart, temps, 24);
pota.dashboard.sendUpdates();
Important: - X and Y arrays must have the same length - Arrays must remain valid until sendUpdates() is called - Use static or global arrays, not local stack arrays
setWidgetConfigCallback()
Registers the function that creates dashboard widgets.
void setWidgetConfigCallback(void (*callback)());
-
- When first dashboard connects
- After calling
refreshLayout()
void setupDashboard() {
// Create all your widgets here
tempWidget = pota.dashboard.addWidget(TEMPERATURE_CARD, "Temperature");
humidityWidget = pota.dashboard.addWidget(HUMIDITY_CARD, "Humidity");
fanToggle = pota.dashboard.addWidget(TOGGLE_BUTTON_CARD, "Fan");
// Register callbacks
pota.dashboard.onUpdate(fanToggle, handleFanToggle);
}
void setup() {
pota.begin(...);
pota.dashboard.setWidgetConfigCallback(setupDashboard);
}
⚠️ Important: Call this before any dashboard connects (typically in setup()).
refreshLayout()
Forces complete dashboard reinitialization.
void refreshLayout();
-
- Clears all widgets
- Re-executes widget configuration callback
- Sends new layout to all connected dashboards
Use cases:
-
- Dynamic UI changes based on mode/state
- Conditional widget visibility
- Runtime layout reconfiguration
void switchToAdvancedMode() {
advancedMode = true;
pota.dashboard.refreshLayout(); // Recreate entire dashboard
}
void setupDashboard() {
// Basic widgets always present
pota.dashboard.addWidget(TEMPERATURE_CARD, "Temperature");
// Advanced widgets only in advanced mode
if (advancedMode) {
pota.dashboard.addWidget(SLIDER_CARD, "PID Kp", 0, "", 0.01, 0, 10);
pota.dashboard.addWidget(SLIDER_CARD, "PID Ki", 0, "", 0.01, 0, 10);
}
}
WidgetData Structure
Data structure passed to widget callbacks.
Members:
struct WidgetData {
variant value; // Primary value (int/float/bool/String)
float x; // Joystick X coordinate
float y; // Joystick Y coordinate
float min; // Range slider minimum
float max; // Range slider maximum
const char* direction; // Joystick direction
};
Getter methods:
int getInt() const;
float getFloat() const;
bool getBool() const;
String getString() const;
String getTypeName() const; // For debugging
String toString() const; // Convert to string
pota.dashboard.onUpdate(widgetID, [](WidgetData data) {
// Type-safe getters with automatic conversion
int intValue = data.getInt(); // Works for int, float, bool, String
float floatValue = data.getFloat(); // Works for float, int, bool, String
bool boolValue = data.getBool(); // Works for bool, int, float, String
String strValue = data.getString(); // Works for String, int, float, bool
// Direct member access
if (widgetType == JOYSTICK_2D_CARD) {
float x = data.x;
float y = data.y;
const char* dir = data.direction;
}
if (widgetType == RANGE_SLIDER_CARD) {
float minVal = data.min;
float maxVal = data.max;
}
});
Type conversion warnings: If you use the wrong getter, a warning is printed to Serial: ⚠️ WARNING: Expected float, got int
Widget-Specific Guides
Display Widgets
GENERIC_CARD

Generic display for any text or numeric value.
uint8_t statusCard = pota.dashboard.addWidget(GENERIC_CARD, "System Status");
pota.dashboard.setValue(statusCard, "All systems operational");
pota.dashboard.setValue(statusCard, 12345); // Numbers work too
TEMPERATURE_CARD

Temperature display with icon.
uint8_t tempCard = pota.dashboard.addWidget(
TEMPERATURE_CARD,
"Core Temp"
);
float celsius = 25.2;
pota.dashboard.setValue(tempCard, celsius);
Default unit: °C (displayed automatically)
HUMIDITY_CARD

Humidity percentage display.
uint8_t humidCard = pota.dashboard.addWidget(HUMIDITY_CARD, "Humidity");
pota.dashboard.setValue(humidCard, 55.4f); // Percentage (0-100)
FEEDBACK_CARD

Status indicator with 5 color-coded states.
uint8_t statusCard = pota.dashboard.addWidget(FEEDBACK_CARD, "Safety Interlock");
// Status constants
pota.dashboard.setValue(statusCard, STATUS_NONE); // Gray (neutral)
pota.dashboard.setValue(statusCard, STATUS_INFO); // Blue (info)
pota.dashboard.setValue(statusCard, STATUS_SUCCESS); // Green (OK)
pota.dashboard.setValue(statusCard, STATUS_WARNING); // Yellow (caution)
pota.dashboard.setValue(statusCard, STATUS_DANGER); // Red (critical)
void updateSystemStatus() {
if (temperature > 85) {
pota.dashboard.setValue(statusCard, STATUS_DANGER);
} else if (temperature > 75) {
pota.dashboard.setValue(statusCard, STATUS_WARNING);
} else {
pota.dashboard.setValue(statusCard, STATUS_SUCCESS);
}
}
PROGRESS_CARD

Progress bar (0-100%).
uint8_t progressCard = pota.dashboard.addWidget(
PROGRESS_CARD,
"Batch Progress",
0, "", // tab_id, extra
1, // step
0, 100 // min, max
);
int percentage = 75;
pota.dashboard.setValue(progressCard, percentage);
Control Widgets
TOGGLE_BUTTON_CARD

On/Off switch.
uint8_t pumpToggle = pota.dashboard.addWidget(
TOGGLE_BUTTON_CARD,
"Main Power"
);
// Set initial state
pota.dashboard.setValue(pumpToggle, false);
// Handle user toggles
pota.dashboard.onUpdate(pumpToggle, [](WidgetData data) {
bool on = data.getBool();
digitalWrite(PUMP_PIN, on ? HIGH : LOW);
});
SLIDER_CARD

Numeric slider with automatic integer/float mode.
uint8_t brightness = pota.dashboard.addWidget(
SLIDER_CARD,
"Brightness %",
0, "",
5.0, // step >= 1.0 → Integer mode
0.0,
100.0
);
pota.dashboard.onUpdate(brightness, [](WidgetData data) {
int value = data.getInt(); // Returns 0, 5, 10, 15, ...
analogWrite(LED_PIN, map(value, 0, 100, 0, 255));
});
uint8_t tempSetpoint = pota.dashboard.addWidget(
SLIDER_CARD,
"Target Temp °C",
0, "",
0.1, // step < 1.0 → Float mode
15.0,
30.0
);
pota.dashboard.onUpdate(tempSetpoint, [](WidgetData data) {
float value = data.getFloat(); // Returns 15.0, 15.1, 15.2, ...
setTargetTemperature(value);
});
RANGE_SLIDER_CARD

Dual-handle slider for min/max selection.
uint8_t tempRange = pota.dashboard.addWidget(
RANGE_SLIDER_CARD,
"Alarm Range",
0, "",
0.5, // step
10.0, // absolute min
35.0 // absolute max
);
pota.dashboard.onUpdate(tempRange, [](WidgetData data) {
float min = data.min;
float max = data.max;
setHeatingThreshold(min);
setCoolingThreshold(max);
});
DROPDOWN_CARD

Selection menu with comma-separated options.
uint8_t modeDropdown = pota.dashboard.addWidget(
DROPDOWN_CARD,
"Power Mode",
0,
"Standard,Manual,Eco,Boost" // Options in extra parameter
);
// Set initial selection
pota.dashboard.setValue(modeDropdown, "Standard");
// Handle selection
pota.dashboard.onUpdate(modeDropdown, [](WidgetData data) {
String mode = data.getString();
if (mode == "Standard") enableStandardMode();
else if (mode == "Manual") enableManualMode();
else if (mode == "Eco") enableEcoMode();
else if (mode == "Boost") enableBoostMode();
});
TEXT_INPUT_CARD / PASSWORD_CARD


uint8_t deviceName = pota.dashboard.addWidget(
TEXT_INPUT_CARD,
"Node Name"
);
pota.dashboard.setValue(deviceName, "NODE-01");
pota.dashboard.onUpdate(deviceName, [](WidgetData data) {
String name = data.getString();
saveDeviceName(name);
});
COLOR_PICKER_CARD

uint8_t colorPicker = pota.dashboard.addWidget(
COLOR_PICKER_CARD,
"UI Accent"
);
pota.dashboard.setValue(colorPicker, "#FF5733");
pota.dashboard.onUpdate(colorPicker, [](WidgetData data) {
String hex = data.getString(); // "#RRGGBB"
// Parse hex to RGB
long rgb = strtol(&hex[1], NULL, 16);
uint8_t r = (rgb >> 16) & 0xFF;
uint8_t g = (rgb >> 8) & 0xFF;
uint8_t b = rgb & 0xFF;
setLEDColor(r, g, b);
});
PUSH_BUTTON_CARD / ACTION_BUTTON_CARD


uint8_t refreshBtn = pota.dashboard.addWidget(
PUSH_BUTTON_CARD,
"Manual Override"
);
pota.dashboard.onUpdate(refreshBtn, [](WidgetData data) {
Serial.println("Manual refresh triggered");
readAllSensors();
updateDashboard();
});
Joystick Widgets
JOYSTICK_2D_CARD

uint8_t joystick = pota.dashboard.addWidget(
JOYSTICK_2D_CARD,
"Camera PTZ"
);
pota.dashboard.onUpdate(joystick, [](WidgetData data) {
float x = data.x; // -1.0 to +1.0 (left to right)
float y = data.y; // -1.0 to +1.0 (down to up)
const char* direction = data.direction; // "up","down","left","right","idle"
// Motor control
int leftSpeed = (y + x) * 127;
int rightSpeed = (y - x) * 127;
setMotorSpeeds(leftSpeed, rightSpeed);
});
Time Widgets
TIME_SYNC_CARD

uint8_t timeSync = pota.dashboard.addWidget(
TIME_SYNC_CARD,
"NTP Sync"
);
pota.dashboard.onUpdate(timeSync, [](WidgetData data) {
String timestamp = data.getString(); // Milliseconds
unsigned long timeSeconds = timestamp.toInt() / 1000;
setSystemTime(timeSeconds);
});
WEEK_SELECTION_CARD

uint8_t scheduleDays = pota.dashboard.addWidget(
WEEK_SELECTION_CARD,
"Operating Days"
);
pota.dashboard.onUpdate(scheduleDays, [](WidgetData data) {
String selected = data.getString(); // "mon,wed,fri"
// Parse selected days
bool activeDays[7] = {false};
// ... parsing logic ...
updateSchedule(activeDays);
});
Chart Widgets

All charts use the same API: setX() and setY().
uint8_t chart = pota.dashboard.addWidget(LINE_CHART, "Temp History");
// String labels
const char* hours[] = {"00:00", "06:00", "12:00", "18:00", "24:00"};
float temps[] = {18.5, 19.2, 23.4, 22.1, 19.8};
pota.dashboard.setX(chart, hours, 5);
pota.dashboard.setY(chart, temps, 5);
pota.dashboard.sendUpdates();
Chart types:
-
BAR_CHART- Vertical/horizontal barsLINE_CHART- Line graphAREA_CHART- Filled areaPIE_CHART- Pie chart (Y values should sum to 100)DOUGHNUT_CHART- Doughnut chart
Layout Widgets
TAB

uint8_t overviewTab = pota.dashboard.addWidget(TAB, "🌡️ Telemetry");
uint8_t controlTab = pota.dashboard.addWidget(TAB, "🎛️ Controls");
// Add widgets to tabs
uint8_t tempCard = pota.dashboard.addWidget(
TEMPERATURE_CARD,
"Temperature",
overviewTab // Parent tab ID
);
uint8_t fanToggle = pota.dashboard.addWidget(
TOGGLE_BUTTON_CARD,
"Fan",
controlTab
);
SEPARATOR_CARD

uint8_t separator = pota.dashboard.addWidget(
SEPARATOR_CARD,
"Environmental Telemetry (DEMO)",
0,
"Real-time monitoring of core sensors, ambient conditions, and active sessions." // Optional description
);
Media Widgets
LINK_CARD

uint8_t manualLink = pota.dashboard.addWidget(
LINK_CARD,
"Manual",
0,
"https://docs.example.com/manual" // URL
);
IMAGE_CARD

uint8_t cameraFeed = pota.dashboard.addWidget(
IMAGE_CARD,
"Security Camera",
0,
"https://camera.local/snapshot.jpg" // Image URL
);
FILE_UPLOAD_CARD

uint8_t firmwareUpload = pota.dashboard.addWidget(
FILE_UPLOAD_CARD,
"Firmware"
);
pota.dashboard.onUpdate(firmwareUpload, [](WidgetData data) {
String fileUrl = data.getString();
downloadAndInstallFirmware(fileUrl);
});
Complete Examples
#include <POTA.h>
#include "secrets.h"
POTA pota;
uint8_t tempCard, humidCard, statusCard;
void setupDashboard() {
tempCard = pota.dashboard.addWidget(TEMPERATURE_CARD, "Temperature");
humidCard = pota.dashboard.addWidget(HUMIDITY_CARD, "Humidity");
statusCard = pota.dashboard.addWidget(FEEDBACK_CARD, "Status");
}
void setup() {
Serial.begin(115200);
pota.begin(DEVICE_TYPE, FIRMWARE_VERSION,
AUTH_TOKEN, SERVER_SECRET,
WIFI_SSID, WIFI_PASSWORD);
pota.dashboard.setWidgetConfigCallback(setupDashboard);
}
void loop() {
pota.loop();
float temp = readTemperature();
float humid = readHumidity();
pota.dashboard.setValue(tempCard, temp);
pota.dashboard.setValue(humidCard, humid);
// Update status based on conditions
if (temp > 30 || humid > 80) {
pota.dashboard.setValue(statusCard, STATUS_WARNING);
} else {
pota.dashboard.setValue(statusCard, STATUS_SUCCESS);
}
pota.dashboard.sendUpdates();
delay(2000);
}
#include <POTA.h>
#include "secrets.h"
POTA pota;
struct {
uint8_t controlTab;
uint8_t lightToggle, fanToggle;
uint8_t brightness, tempSetpoint;
} widgets;
void setupDashboard() {
widgets.controlTab = pota.dashboard.addWidget(TAB, "Controls");
widgets.lightToggle = pota.dashboard.addWidget(
TOGGLE_BUTTON_CARD, "Lights", widgets.controlTab);
widgets.fanToggle = pota.dashboard.addWidget(
TOGGLE_BUTTON_CARD, "Fan", widgets.controlTab);
widgets.brightness = pota.dashboard.addWidget(
SLIDER_CARD, "Brightness", widgets.controlTab, "%", 5.0, 0, 100);
widgets.tempSetpoint = pota.dashboard.addWidget(
SLIDER_CARD, "Target Temp", widgets.controlTab, "°C", 0.5, 18, 30);
// Register callbacks
pota.dashboard.onUpdate(widgets.lightToggle, [](WidgetData data) {
digitalWrite(LIGHT_PIN, data.getBool() ? HIGH : LOW);
});
pota.dashboard.onUpdate(widgets.fanToggle, [](WidgetData data) {
digitalWrite(FAN_PIN, data.getBool() ? HIGH : LOW);
});
pota.dashboard.onUpdate(widgets.brightness, [](WidgetData data) {
int value = data.getInt();
analogWrite(LED_PIN, map(value, 0, 100, 0, 255));
});
pota.dashboard.onUpdate(widgets.tempSetpoint, [](WidgetData data) {
float temp = data.getFloat();
setTargetTemperature(temp);
});
}
void setup() {
Serial.begin(115200);
pinMode(LIGHT_PIN, OUTPUT);
pinMode(FAN_PIN, OUTPUT);
pinMode(LED_PIN, OUTPUT);
pota.begin(DEVICE_TYPE, FIRMWARE_VERSION,
AUTH_TOKEN, SERVER_SECRET,
WIFI_SSID, WIFI_PASSWORD);
pota.dashboard.setWidgetConfigCallback(setupDashboard);
}
void loop() {
pota.loop();
pota.dashboard.sendUpdates();
}
#include <POTA.h>
#include "secrets.h"
POTA pota;
uint8_t tempChart;
const int HISTORY_SIZE = 24;
float tempHistory[HISTORY_SIZE];
int currentIndex = 0;
void setupDashboard() {
tempChart = pota.dashboard.addWidget(LINE_CHART, "24h Temperature");
}
void setup() {
Serial.begin(115200);
pota.begin(DEVICE_TYPE, FIRMWARE_VERSION,
AUTH_TOKEN, SERVER_SECRET,
WIFI_SSID, WIFI_PASSWORD);
pota.dashboard.setWidgetConfigCallback(setupDashboard);
}
void loop() {
pota.loop();
static unsigned long lastReading = 0;
if (millis() - lastReading >= 3600000) { // Every hour
// Add new reading
tempHistory[currentIndex] = readTemperature();
currentIndex = (currentIndex + 1) % HISTORY_SIZE;
// Update chart
int hours[HISTORY_SIZE];
for (int i = 0; i < HISTORY_SIZE; i++) {
hours[i] = i;
}
pota.dashboard.setX(tempChart, hours, HISTORY_SIZE);
pota.dashboard.setY(tempChart, tempHistory, HISTORY_SIZE);
lastReading = millis();
}
pota.dashboard.sendUpdates();
delay(1000);
}
Best Practices
Performance
// Good: Reasonable update intervals
void loop() {
pota.loop();
static unsigned long lastUpdate = 0;
if (millis() - lastUpdate >= 2000) { // 2 seconds
updateSensors();
lastUpdate = millis();
}
}
// Bad: Too frequent updates waste bandwidth
void loop() {
pota.loop();
updateSensors(); // Every loop iteration!
delay(10);
}
// Good: Group multiple updates
pota.dashboard.setValue(tempCard, temp);
pota.dashboard.setValue(humidCard, humid);
pota.dashboard.setValue(pressCard, press);
pota.dashboard.sendUpdates(); // One transmission
// Bad: Multiple transmissions
pota.dashboard.setValue(tempCard, temp);
pota.dashboard.sendUpdates();
pota.dashboard.setValue(humidCard, humid);
pota.dashboard.sendUpdates();
// Good: Reasonable data points
const int POINTS = 24; // 24 hours
float data[POINTS];
// Bad: Too many points
const int POINTS = 1000; // Overkill for display
Memory Management
// Good: Global storage
uint8_t tempWidget, humidWidget;
void setupDashboard() {
tempWidget = pota.dashboard.addWidget(...);
}
// Bad: Lost after function returns
void setupDashboard() {
uint8_t tempWidget = pota.dashboard.addWidget(...);
// tempWidget is lost!
}
// Good: Static or global arrays
float chartData[24];
void updateChart() {
pota.dashboard.setX(chart, labels, 24);
pota.dashboard.setY(chart, chartData, 24);
pota.dashboard.sendUpdates();
}
// Bad: Local array destroyed
void updateChart() {
float chartData[24]; // Stack allocated
pota.dashboard.setY(chart, chartData, 24);
pota.dashboard.sendUpdates(); // ⚠️ Array already destroyed!
}
Error Handling
void setup() {
Serial.begin(115200);
POTAError err = pota.begin(...);
if (err != POTAError::SUCCESS) {
Serial.print("POTA init failed: ");
Serial.println(POTA::errorToString(err));
// Decide what to do
while (1) delay(1000); // Halt
// OR: Continue without POTA
}
}
Security
// Bad: Credentials in code
pota.begin("ESP32", "1.0.0", "abc123", "secret", "MyWiFi", "password");
// Good: Use secrets.h (not in version control)
#include "secrets.h"
pota.begin(DEVICE_TYPE, FIRMWARE_VERSION,
AUTH_TOKEN, SERVER_SECRET,
WIFI_SSID, WIFI_PASSWORD);
#define DEVICE_TYPE "XIAO_ESP32S3"
#define FIRMWARE_VERSION "1.0.0"
#define AUTH_TOKEN "your_token_here"
#define SERVER_SECRET "your_secret_here"
#define WIFI_SSID "your_wifi"
#define WIFI_PASSWORD "your_password"
pota.dashboard.onUpdate(tempSetpoint, [](WidgetData data) {
float temp = data.getFloat();
// Validate range
if (temp < 10.0 || temp > 40.0) {
Serial.println("Invalid temperature setpoint");
return;
}
setTargetTemperature(temp);
});
Troubleshooting
Widgets Not Appearing
Problem: Dashboard is empty
Solutions:
// Must be called in setup()
pota.dashboard.setWidgetConfigCallback(setupDashboard);
void setupDashboard() {
uint8_t id = pota.dashboard.addWidget(...);
Serial.print("Widget ID: ");
Serial.println(id); // Should print 1, 2, 3...
}
void loop() {
pota.loop(); // Must be called!
// ...
}
Values Not Updating
Problem: Widgets show old values
Solutions:
pota.dashboard.setValue(widget, value);
pota.dashboard.sendUpdates(); // Required!
void loop() {
pota.loop(); // Handles WebSocket
// ...
}
// Store ID from addWidget()
uint8_t myWidget = pota.dashboard.addWidget(...);
// Use same ID in setValue()
pota.dashboard.setValue(myWidget, value);
Type Mismatch Warnings
Problem: ⚠️ WARNING: Expected float, got int
Solution: Use correct getter for widget type
// Slider (numeric) → use getInt() or getFloat()
pota.dashboard.onUpdate(slider, [](WidgetData data) {
float value = data.getFloat(); // Correct
});
// Toggle → use getBool()
pota.dashboard.onUpdate(toggle, [](WidgetData data) {
bool state = data.getBool(); // Correct
});
// Dropdown → use getString()
pota.dashboard.onUpdate(dropdown, [](WidgetData data) {
String selected = data.getString(); // Correct
});
Chart Not Displaying
Problem: Chart is empty or shows old data
Solutions:
const int SIZE = 10;
float x[SIZE], y[SIZE];
pota.dashboard.setX(chart, x, SIZE); // Same SIZE
pota.dashboard.setY(chart, y, SIZE); // Same SIZE
pota.dashboard.setX(chart, x, size);
pota.dashboard.setY(chart, y, size);
pota.dashboard.sendUpdates(); // Required!
// Good: Static/global
float chartData[24];
// Bad: Local (destroyed after function returns)
void updateChart() {
float data[24]; // ⚠️ Don't do this
pota.dashboard.setY(chart, data, 24);
}
OTA Update Fails
Problem: checkAndPerformOTA() returns error
Solutions:
POTAError err = pota.checkAndPerformOTA();
Serial.println(POTA::errorToString(err));
2. Verify credentials:
-
- Check AUTH_TOKEN is correct
- Check SERVER_SECRET matches
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi disconnected!");
}
Serial.print("Free heap: ");
Serial.println(ESP.getFreeHeap());
// ESP32 needs ~100KB free for OTA
Platform-Specific Notes
ESP32
// Option 1: POTA manages WiFi
pota.begin(type, version, token, secret, ssid, password);
// Option 2: Manual management
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) delay(500);
pota.begin(type, version, token, secret);
OTA: - Uses esp_https_ota library - Requires ~100KB free heap - Supports both .bin files
ESP8266
// Same as ESP32
pota.begin(type, version, token, secret, ssid, password);
OTA: - Uses ESP8266httpUpdate library - More memory constrained than ESP32 - Requires careful memory management
Special Note: - Uses NuSock library for stable SSL WebSocket connections - Automatically included by POTA
Arduino Opta
// Must use POTA WiFi management
pota.begin(type, version, token, secret, ssid, password);
OTA: - Uses Arduino_Portenta_OTA library - Process: download → decompress → apply - Requires compatible bootloader
Special Notes: - Uses NuSock library for WebSocket stability - restart() properly shuts down WiFi before reset - WiFi firmware must be installed
#include <WiFi.h>
void setup() {
Serial.begin(115200);
String fwVersion = WiFi.firmwareVersion();
Serial.print("WiFi FW: ");
Serial.println(fwVersion);
}
Additional Resources
Example Sketches
The POTA library includes several example sketches:
Dashboard Examples:
-
simpleDashboard.ino- Basic sensor displayinteractiveDashboard.ino- Control widgets demochartsDashboard.ino- Data visualizationfullDashboard.ino- Complete smart greenhouse
Firmware Update Examples:
-
firmwareUpdate.ino- Safe OTA with checksfirmwareManagerDashboard.ino- Interactive OTA manager
Utility Examples:
-
getMAC.ino- Display device MAC address
Links
-
- GitHub Repository: https://github.com/pleasedontcode/POTA
- POTA Service: https://www.pleasedontcode.com/please-over-the-air/
- Demo Dashboard: https://www.pleasedontcode.com/demo-dashboard
- Fast POTA Integration with PCGen: https://www.pleasedontcode.com/please-code-generator/
Conclusion
POTA 2.0.0 provides a complete, production-ready solution for building IoT dashboards entirely in Arduino C++. With 33 widget types, real-time WebSocket communication, and secure OTA updates, you can create professional monitoring and control interfaces without web development expertise.
Key Takeaways
-
- Simple API: Create widgets with
addWidget(), update withsetValue() - Real-Time: WebSocket ensures instant updates in both directions
- Type-Safe: Automatic type conversion with warnings
- Cross-Platform: ESP32, ESP8266, Arduino Opta support
- Secure: HTTPS + JWT authentication + HMAC verification
- Simple API: Create widgets with
Next Steps
-
- Install POTA from Arduino Library Manager
- Try the example sketches
- Build your first dashboard
- Join the community on GitHub
Happy building! 🚀
January 26, 2026 by Alessandro Colucci