January 26, 2026 by Alessandro Colucci
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.
[env:esp32]
platform = espressif32
lib_deps = pleasedontcode/POTA@^2.0.0
#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;
}
The main class providing OTA updates and dashboard access.
POTA pota;
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");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) delay(500);
pota.begin("ESP32_DEVKIT_V1", "1.0.0", token, secret); // No WiFi params
Main processing function - call from Arduino loop().
void loop(bool enableAutoReconnect = true);
Parameters:
enableAutoReconnect: Enable automatic WebSocket reconnection (default: true)void loop() {
pota.loop(); // Always call first
// Your application code
updateSensors();
updateDashboard();
delay(100);
}
Returns the device’s unique MAC address.
String getSecureMACAddress();
String mac = pota.getSecureMACAddress();
Serial.print("Device MAC: ");
Serial.println(mac);
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-datePOTAError values - Check failed or update failedProcess:
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.
Performs OTA update from specific URL.
POTAError performOTA(const char* firmwareURL);
firmwareURL: HTTPS URL of firmware binaryReturns:
POTAError codeconst char* url = "https://example.com/firmware/v2.0.0.bin";
POTAError err = pota.performOTA(url);
⚠️ Warning: Device reboots on successful update.
Registers callback for OTA availability notifications.
void onOTAAvailable(POTACallback callback);
typedef void (*POTACallback)(const char* version);
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);
});
Safely restarts the device.
void restart();
ESP.restart()Serial.println("Rebooting in 3 seconds...");
delay(3000);
pota.restart(); // Never returns
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));
}
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 |
Access the dashboard through pota.dashboard.
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
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
);
Updates a widget’s value.
bool setValue(uint8_t widget_id, value);
intfloat bool String const 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().
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);
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);
});
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:
true on successuint8_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
Registers the function that creates dashboard widgets.
void setWidgetConfigCallback(void (*callback)());
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()).
Forces complete dashboard reinitialization.
void refreshLayout();
Use cases:
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);
}
}
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

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 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 percentage display.
uint8_t humidCard = pota.dashboard.addWidget(HUMIDITY_CARD, "Humidity");
pota.dashboard.setValue(humidCard, 55.4f); // Percentage (0-100)

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 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);

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);
});

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);
});

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);
});

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();
});


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);
});

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);
});


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

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);
});

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);
});

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);
});

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

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
);

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
);

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

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

uint8_t firmwareUpload = pota.dashboard.addWidget(
FILE_UPLOAD_CARD,
"Firmware"
);
pota.dashboard.onUpdate(firmwareUpload, [](WidgetData data) {
String fileUrl = data.getString();
downloadAndInstallFirmware(fileUrl);
});
#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);
}
// 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
// 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!
}
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
}
}
// 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);
});
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!
// ...
}
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);
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
});
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);
}
Problem: checkAndPerformOTA() returns error
Solutions:
POTAError err = pota.checkAndPerformOTA();
Serial.println(POTA::errorToString(err));
2. Verify credentials:
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi disconnected!");
}
Serial.print("Free heap: ");
Serial.println(ESP.getFreeHeap());
// ESP32 needs ~100KB free for OTA
// 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
// 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
// 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);
}
The POTA library includes several example sketches:
Dashboard Examples:
simpleDashboard.ino - Basic sensor displayinteractiveDashboard.ino - Control widgets demo chartsDashboard.ino - Data visualizationfullDashboard.ino - Complete smart greenhouseFirmware Update Examples:
firmwareUpdate.ino - Safe OTA with checksfirmwareManagerDashboard.ino - Interactive OTA managerUtility Examples:
getMAC.ino - Display device MAC address
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.
addWidget(), update with setValue()Happy building! 🚀