Technical Documentation

Embeddex Platform - IoT Device Management & Telemetry System

Embeddex is an IoT platform for ESP32-based battery analysis systems, providing real-time battery metrics monitoring, automated analysis cycle reporting, remote device management, and Over-The-Air (OTA) firmware updates.

Step 1: Provision a Device

Before connecting your ESP32, you need to register it in the platform:

pnpm provision --device-id=esp32-001

This command will:

  1. Register the device in the database
  2. Generate MQTT authentication credentials (username/password)
  3. Create necessary MQTT topic permissions
  4. Display connection details for your ESP32 sketch

Step 2: Configure Your ESP32

Update your Arduino sketch with the credentials from the provision step:

// Device identification (from provision command)
const char* DEVICE_ID = "esp32-001";

// MQTT broker configuration
const char* MQTT_SERVER = "w9e18601.ala.eu-central-1.emqxsl.com";
const int MQTT_PORT = 8883;

// MQTT authentication (username/password - from provision output)
const char* MQTT_USERNAME = "esp32-001";  // Same as DEVICE_ID
const char* MQTT_PASSWORD = "your-password-from-provision";

// Organization ID
const char* ORG_ID = "550e8400-e29b-41d4-a716-446655440000";

Important:

  • The MQTT broker uses username/password authentication, NOT JWT tokens
  • Your MQTT_USERNAME is the same as your DEVICE_ID
  • Your MQTT_PASSWORD is displayed when you run the provision command
  • All devices use the same MQTT_SERVER, MQTT_PORT, and ORG_ID values shown above

Step 3: Upload and Monitor

Upload the sketch to your ESP32 and monitor it in the dashboard:

  1. Upload the firmware via USB or OTA
  2. Device will automatically connect to EMQX
  3. View real-time telemetry on the Devices page
  4. Check device status (online/offline) in the dashboard

For Arduino Developers

MQTT Topics Reference

Your ESP32 sketch needs to publish and subscribe to specific MQTT topics. Use these topic patterns in your code:

Topic PatternDirectionPurpose
embeddex/{org_id}/{device_id}/telemetryPublishSend battery metrics (voltage, current, temperatures, status, cycle)
embeddex/{org_id}/{device_id}/statusPublishReport device online/offline/analysing status
embeddex/{org_id}/{device_id}/otaSubscribeReceive OTA firmware update notifications
embeddex/{org_id}/{device_id}/commandSubscribeReceive remote commands from cloud

Telemetry Payload Format

When publishing to the telemetry topic, use this JSON structure for battery metrics:

{
  "device_id": "battery-device-001",
  "timestamp": 1762859230000,
  "metrics": {
    "cycle": 1,
    "voltage": 7.647,
    "current": 6013.843,
    "batteryTemperature": 25.29,
    "status": 2,
    "chargeTemperature": 27.31,
    "dischargeTemperature": 27.81,
    "negativeVoltage": 0.003
  }
}

Battery Metrics Fields:

  • cycle (number, required) - Current analysis cycle number (increments with each completed analysis)
  • voltage (number, required) - Battery voltage in Volts (typically 7.0-8.4V for 2S LiPo)
  • current (number, required) - Current in milliamps (mA), 0 when idle, 5000-6500 during discharge
  • batteryTemperature (number, required) - Battery temperature in °C
  • status (number 0-16, required) - Battery analysis status code (see below)
  • chargeTemperature (number) - Charge controller temperature in °C
  • dischargeTemperature (number) - Discharge controller temperature in °C
  • negativeVoltage (number) - Negative terminal voltage in Volts

Battery Status Codes:

  • 0 - INIT: Initialization / Idle
  • 2 - DISCHARGING: Active discharge in progress
  • 3 - DISCHARGE_COMPLETE: Discharge phase complete
  • 5 - CHARGING: Charging in progress
  • 7 - PRE_ANALYSIS: Pre-analysis checks
  • 8 - ANALYSIS_COMPLETE: ⭐ Analysis cycle complete (triggers automatic report generation)
  • 15 - WARNING: Non-critical warning
  • 16 - ERROR: Critical error

Note: The platform automatically infers device status from metrics.status:

  • When status !== 8, device status becomes "analysing"
  • When status === 8, device status becomes "online"

The platform stores all metrics as JSONB for flexibility. You can extend the metrics object with additional custom fields.

Status Payload Format

When publishing to the status topic, use this JSON structure:

{
  "device_id": "esp32-001",
  "status": "online",
  "timestamp": 1700000000000,
  "firmware_version": "1.0.0",
  "reason": "boot_complete"
}

Valid status values:

  • online - Device is running normally
  • offline - Device is disconnected
  • rebooting - Device is restarting
  • updating - Device is performing OTA update
  • error - Device encountered an error

Important for Battery Analysis Devices:

You do NOT need to manually publish the "analysing" status. The platform automatically infers device status from your battery metrics:

  • When you publish telemetry with metrics.status between 0-7 (any analysis state except ANALYSIS_COMPLETE), the device status automatically becomes "analysing"
  • When you publish telemetry with metrics.status = 8 (ANALYSIS_COMPLETE), the device status automatically becomes "online"

This means your ESP32 firmware only needs to:

  1. Publish telemetry with accurate metrics.status values during battery analysis
  2. Publish explicit status updates for non-analysis states (offline, rebooting, updating, error)

Remote Commands

Your ESP32 can receive remote commands from the cloud via the command topic. Subscribe to:

embeddex/{org_id}/{device_id}/commands

Command Message Format:

{
  "command_id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "reboot",
  "timestamp": 1700000000000,
  "params": {}
}

Supported Commands:

CommandDescriptionParametersExpected Response
pingConnectivity checkNoneACK with device info
rebootRestart the deviceNoneACK, then status transitions: rebootingofflineonline
configUpdate device configuration{"key": "value"}ACK with applied config
ota_updateFirmware update{"version": "1.0.0", "url": "https://..."}ACK, download, apply, reboot
refresh_configRe-fetch battery configurationNoneACK, HTTPS fetch, state update

Command Acknowledgment:

When your device receives and processes a command, publish an acknowledgment to:

embeddex/{org_id}/{device_id}/commands/ack

ACK Message Format:

{
  "command_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "success",
  "timestamp": 1700000000000,
  "error": null
}

ACK Status Values:

  • success - Command executed successfully
  • error - Command failed (provide error message in error field)
  • invalid - Command not recognized or invalid parameters

Example Implementation:

void handleCommand(const char* payload) {
  // Parse command JSON
  JsonDocument doc;
  deserializeJson(doc, payload);

  const char* commandId = doc["command_id"];
  const char* type = doc["type"];

  // Send ACK
  sendCommandAck(commandId, "success");

  // Execute command
  if (strcmp(type, "reboot") == 0) {
    // Publish rebooting status
    publishStatus("rebooting", "remote_command");
    delay(1000);
    ESP.restart();
  } else if (strcmp(type, "ping") == 0) {
    // Already sent ACK, nothing more needed
  }
  // ... handle other commands
}

void sendCommandAck(const char* commandId, const char* status) {
  char topic[128];
  sprintf(topic, "embeddex/%s/%s/commands/ack", ORG_ID, DEVICE_ID);

  char payload[256];
  sprintf(payload, "{\"command_id\":\"%s\",\"status\":\"%s\",\"timestamp\":%lu}",
          commandId, status, millis());

  mqttClient.publish(topic, payload);
}

Battery Configuration

Before your ESP32 can perform battery analysis, it needs to fetch its assigned configuration from the platform. This is done via a direct HTTPS request (not MQTT).

How It Works

  1. Device boots and connects to WiFi
  2. Device makes HTTPS GET request to fetch configuration
  3. If config exists → Device enters STATE_READY, can analyze
  4. If no config assigned → Device enters STATE_WAITING_CONFIG

Fetching Configuration

Your ESP32 should fetch configuration on boot and when receiving the refresh_config command:

Endpoint:

GET https://oetjqvjmnlchztsxitfn.supabase.co/functions/v1/get-device-config?device_id={device_id}

Response (with configuration):

{
  "device_id": "esp32-001",
  "timestamp": 1699999999000,
  "configuration": {
    "name": "S28(B28)",
    "cell_type": 1,
    "voltage_min": 2.5,
    "voltage_max": 4.2,
    "discharge_voltage_min": 2.5,
    "charge_voltage_max": 4.2,
    "wait_time_after_discharge": 20,
    "wait_time_after_charge": 30,
    "temperature_min": 0,
    "temperature_max": 45,
    "discharge_current": 5000,
    "charge_current": 2500,
    "cutoff_current": 250,
    "charge_current_1": 0,
    "charge_current_2": 0,
    "charge_time_1": 0,
    "charge_time_2": 0
  }
}

Response (no configuration assigned):

{
  "device_id": "esp32-001",
  "timestamp": 1699999999000,
  "configuration": null
}

Configuration Parameters

ParameterTypeUnitDescription
namestring-Configuration name (e.g., “S28(B28)“)
cell_typenumber-Battery cell type identifier
voltage_minnumberVMinimum safe voltage per cell
voltage_maxnumberVMaximum safe voltage per cell
discharge_voltage_minnumberVDischarge cutoff voltage
charge_voltage_maxnumberVCharge cutoff voltage
wait_time_after_dischargenumbersRest time after discharge phase
wait_time_after_chargenumbersRest time after charge phase
temperature_minnumber°CMinimum operating temperature
temperature_maxnumber°CMaximum operating temperature
discharge_currentnumbermADischarge current
charge_currentnumbermACharge current
cutoff_currentnumbermACutoff current for charge termination
charge_current_1numbermAStage 1 charge current (optional)
charge_current_2numbermAStage 2 charge current (optional)
charge_time_1numbersStage 1 charge time (optional)
charge_time_2numbersStage 2 charge time (optional)

Configuration Caching (NVS)

Configuration is cached in ESP32’s Non-Volatile Storage (NVS) for offline resilience:

  • On boot: Load cached config first, then fetch fresh from server
  • On successful fetch: Save new config to NVS
  • On network failure: Use cached config if available
  • This allows the device to operate with last-known config even if the server is unreachable

Example Implementation

#include <WiFiClientSecure.h>
#include <ArduinoJson.h>

bool fetchBatteryConfig() {
    WiFiClientSecure client;
    client.setInsecure();  // For dev; use proper certs in production

    String url = "/functions/v1/get-device-config?device_id=" + String(DEVICE_ID);

    if (!client.connect("oetjqvjmnlchztsxitfn.supabase.co", 443)) {
        Serial.println("Connection failed");
        return false;
    }

    client.print("GET " + url + " HTTP/1.1\r\n");
    client.print("Host: oetjqvjmnlchztsxitfn.supabase.co\r\n");
    client.print("Connection: close\r\n\r\n");

    // Read response and parse JSON...
    // Save to NVS if valid config received
    return true;
}

Device States

Your ESP32 maintains internal states that determine what operations it can perform.

Internal Firmware States

StateDescriptionCan Analyze?
STATE_CONNECTINGConnecting to WiFi/MQTTNo
STATE_WAITING_CONFIGNo configuration assignedNo
STATE_READYConfiguration loaded, readyYes
STATE_ANALYSINGBattery analysis in progress-
STATE_ERRORCritical error occurredNo

State Flow

stateDiagram-v2
    [*] --> STATE_CONNECTING: Boot
    STATE_CONNECTING --> STATE_READY: Config received (HTTP or NVS)
    STATE_CONNECTING --> STATE_WAITING_CONFIG: No config available
    STATE_WAITING_CONFIG --> STATE_READY: Config fetched via HTTP
    STATE_READY --> STATE_ANALYSING: Start analysis
    STATE_ANALYSING --> STATE_READY: Analysis complete
    STATE_READY --> STATE_ERROR: Critical error
    STATE_ANALYSING --> STATE_ERROR: Critical error
    STATE_ERROR --> STATE_CONNECTING: Reboot

State Transitions

FromToTrigger
STATE_CONNECTINGSTATE_READYConfig received (HTTP or NVS)
STATE_CONNECTINGSTATE_WAITING_CONFIGNo config available
STATE_WAITING_CONFIGSTATE_READYConfig fetched via HTTP
STATE_READYSTATE_ANALYSINGStart analysis command
STATE_ANALYSINGSTATE_READYAnalysis complete
Any stateSTATE_ERRORCritical error detected
STATE_ERRORSTATE_CONNECTINGReboot command

Boot Sequence

When your ESP32 boots, it follows this sequence:

  1. Initialize Serial - Start serial communication for debugging
  2. Connect to WiFi - Connect to configured wireless network
  3. Sync NTP time - Synchronize clock for accurate timestamps
  4. Load cached config from NVS - Check for previously saved configuration
  5. HTTPS GET to fetch fresh configuration - Request latest config from server
  6. Connect to MQTT broker - Establish connection to EMQX
  7. Subscribe to command topics - Listen for remote commands
  8. Enter operational state - STATE_READY (if config) or STATE_WAITING_CONFIG
void setup() {
    Serial.begin(115200);

    // 1. Connect WiFi
    WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
    while (WiFi.status() != WL_CONNECTED) delay(500);

    // 2. Sync NTP
    configTime(0, 0, "pool.ntp.org");

    // 3. Load cached config from NVS
    batteryConfig.begin();  // Loads from NVS if available

    // 4. Fetch fresh config via HTTP
    httpConfig.fetchConfig();

    // 5. Connect MQTT
    mqttClient.begin();
    mqttClient.connect();

    // 6. Subscribe to commands
    mqttClient.subscribeToCommands();

    // Device is now ready for operation
}

Resources