Building Real-Time IoT Dashboards with POTA

Your Complete Guide to Arduino, ESP32, ESP8266 and Opta Development

January 26, 2026 by Alessandro Colucci
POTA Dashboard Article

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

Arduino IDE:
    1. Open Library Manager (Sketch → Include Library → Manage Libraries)
    2. Search for “POTA”
    3. Install version 2.0.0 or higher
PlatformIO:
[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
);
 
Parameters: 
    • 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 service
    • serverSecret: Secret key for secure communication
    • ssid: WiFi SSID (optional - omit if managing WiFi yourself)
    • password: WiFi password (required if ssid provided)

Returns: POTAError - Use errorToString() for description

WiFi Management Modes:

Mode 1 - POTA manages WiFi:
pota.begin("ESP32_DEVKIT_V1", "1.0.0", token, secret, "MyWiFi", "password123");
    • Connects to WiFi automatically
    • Handles reconnection
    • Blocks until connected (30s timeout)
Mode 2 - You manage WiFi:
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);
 
What it does:
    • 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)
Usage:
void loop() {
    pota.loop();  // Always call first
    
    // Your application code
    updateSensors();
    updateDashboard();
    
    delay(100);
}

 


getSecureMACAddress()

Returns the device’s unique MAC address.

String getSecureMACAddress();
 
Returns: MAC address formatted as “XX:XX:XX:XX:XX:XX”
 
Platform-specific sources:
    • ESP32: eFuse MAC (factory-programmed, tamper-resistant)
    • ESP8266: WiFi interface MAC
    • Arduino Opta: Board information MAC
Usage:
String mac = pota.getSecureMACAddress();
Serial.print("Device MAC: ");
Serial.println(mac);

 


checkAndPerformOTA()

Checks for firmware updates and installs if available.

POTAError checkAndPerformOTA();
 
Returns:
    • POTAError::SUCCESS - Update successful (device will reboot - this never returns)
    • POTAError::NO_UPDATE_AVAILABLE - Already up-to-date
    • Other POTAError values - Check failed or update failed

Process:

    1. Queries POTA API for available updates
    2. Verifies server response authenticity
    3. Downloads firmware binary if update available
    4. Flashes new firmware 
    5. Reboots device ← Important!
Usage:
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);
 
Parameters:
    • firmwareURL: HTTPS URL of firmware binary

Returns:

    • POTAError code
Usage:
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);
 
Callback signature:
typedef void (*POTACallback)(const char* version);
 
When triggered:
    • Server pushes “new firmware available” notification via WebSocket - Real-time (no polling required)
Usage:
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();
 
Platform behavior:
    • ESP32/ESP8266: Calls ESP.restart()
    • Arduino Opta: Properly shuts down WiFi before reset
Usage:
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);
 
Usage:
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
);
 
Parameters:
    • 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

Basic usage:
uint8_t tempCard = pota.dashboard.addWidget(
    TEMPERATURE_CARD,
    "Living Room"
);
 
With tab:
uint8_t controlsTab = pota.dashboard.addWidget(TAB, "Controls");

uint8_t fanToggle = pota.dashboard.addWidget(
    TOGGLE_BUTTON_CARD,
    "Fan",
    controlsTab  // Widget appears in Controls tab
);
 
Slider with range:
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);
 
Supported value types:
    • int
    • float 
    • bool 
    • String 
    • const char*

Returns: true on success, false if widget not found

Usage examples:
// 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);
 
Parameters: 
    • force: Send all widgets regardless of changes (default: false)
Normal usage:
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 update:
// Force send everything (useful after reconnection)
pota.dashboard.sendUpdates(true);

 


onUpdate()

Registers callback for widget interactions.

void onUpdate(uint8_t widget_id, WidgetCallback callback);
 
Callback signature:
void callback(WidgetData data);
 
Usage - Toggle Button:
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");
});
 
Usage - Slider:
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));
});
 
Usage - Joystick:
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);
 
Supported array types: 
    • int* - Integer values
    • float* - Decimal values
    • const char* const* - String labels (X-axis only)

Returns:

    • true on success
Usage - Bar Chart:
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();
 
Usage - Line Chart:
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)());
 
Called automatically:
    • When first dashboard connects
    • After calling refreshLayout()
Usage:
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();
 
What it does: 
    • 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
Usage:
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
Usage:
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_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_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_CARD

Humidity percentage display.

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

 


FEEDBACK_CARD

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)
 
Dynamic status example:
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_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

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

SLIDER_CARD

Numeric slider with automatic integer/float mode.

Integer Mode (step >= 1.0):
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));
});
 
Float Mode (step < 1.0):
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

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

TEXT_INPUT_CARDPASSWORD_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

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

PUSH_BUTTON_CARDACTION_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

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

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

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

LINE_CHART_CARD

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 bars
    • LINE_CHART - Line graph
    • AREA_CHART - Filled area
    • PIE_CHART - Pie chart (Y values should sum to 100)
    • DOUGHNUT_CHART - Doughnut chart

 


Layout Widgets

TAB

TAB_WIDGET

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

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

IMAGE_CARD

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

 


FILE_UPLOAD_CARD

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


 
Example 1: Temperature Monitor
#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);
}

 


 
Example 2: Smart Home Controller
#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();
}

 


 
Example 3: Data Logger with Charts
#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

 
1. Update Frequency
// 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);
}
 
2. Batch Updates
// 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();
 
3. Chart Data
// 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

 
1. Use Global Widget IDs
// 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!
}
 
2. Chart Arrays Must Persist
// 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

 
Always check begin() result:
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

 
1. Never Hardcode Credentials
// 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);
 
secrets.h example:
#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"
 
2. Validate User Input
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:

1. Check callback registration:
// Must be called in setup()
pota.dashboard.setWidgetConfigCallback(setupDashboard);
 
2. Check widget creation:
void setupDashboard() {
    uint8_t id = pota.dashboard.addWidget(...);
    Serial.print("Widget ID: ");
    Serial.println(id);  // Should print 1, 2, 3...
}
 
3. Verify WiFi/WebSocket connection:
void loop() {
    pota.loop();  // Must be called!
    // ...
}

 


Values Not Updating

Problem: Widgets show old values

Solutions:

1. Call sendUpdates():
pota.dashboard.setValue(widget, value);
pota.dashboard.sendUpdates();  // Required!
 
2. Check loop() is called:
void loop() {
    pota.loop();  // Handles WebSocket
    // ...
}
 
3. Verify widget ID:
// 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:

1. Ensure arrays have same length:
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
 
2. Call sendUpdates() after setting data:
pota.dashboard.setX(chart, x, size);
pota.dashboard.setY(chart, y, size);
pota.dashboard.sendUpdates();  // Required!
 
3. Use persistent arrays (not local):
// 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:

1. Check return value:
POTAError err = pota.checkAndPerformOTA();
Serial.println(POTA::errorToString(err));

 

2. Verify credentials:

    • Check AUTH_TOKEN is correct
    • Check SERVER_SECRET matches
 
3. Ensure stable WiFi:
if (WiFi.status() != WL_CONNECTED) {
    Serial.println("WiFi disconnected!");
}
 
4. Check available memory:
Serial.print("Free heap: ");
Serial.println(ESP.getFreeHeap());
// ESP32 needs ~100KB free for OTA

 


Platform-Specific Notes


ESP32

WiFi Management:
// 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

WiFi Management:
// 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

WiFi Management:
// 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

Check WiFi firmware:
#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 display
    • interactiveDashboard.ino - Control widgets demo 
    • chartsDashboard.ino - Data visualization
    • fullDashboard.ino - Complete smart greenhouse

Firmware Update Examples:

    • firmwareUpdate.ino - Safe OTA with checks
    • firmwareManagerDashboard.ino - Interactive OTA manager

Utility Examples:

    • getMAC.ino - Display device MAC address

Links

 


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

    1. Simple API: Create widgets with addWidget(), update with setValue()
    2. Real-Time: WebSocket ensures instant updates in both directions
    3. Type-Safe: Automatic type conversion with warnings
    4. Cross-Platform: ESP32, ESP8266, Arduino Opta support
    5. Secure: HTTPS + JWT authentication + HMAC verification

Next Steps

    1. Install POTA from Arduino Library Manager
    2. Try the example sketches
    3. Build your first dashboard
    4. Join the community on GitHub

Happy building! 🚀

Chat with us on WhatsApp