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:
- Register the device in the database
- Generate MQTT authentication credentials (username/password)
- Create necessary MQTT topic permissions
- 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_USERNAMEis the same as yourDEVICE_ID - Your
MQTT_PASSWORDis displayed when you run the provision command - All devices use the same
MQTT_SERVER,MQTT_PORT, andORG_IDvalues shown above
Step 3: Upload and Monitor
Upload the sketch to your ESP32 and monitor it in the dashboard:
- Upload the firmware via USB or OTA
- Device will automatically connect to EMQX
- View real-time telemetry on the Devices page
- 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 Pattern | Direction | Purpose |
|---|---|---|
embeddex/{org_id}/{device_id}/telemetry | Publish | Send battery metrics (voltage, current, temperatures, status, cycle) |
embeddex/{org_id}/{device_id}/status | Publish | Report device online/offline/analysing status |
embeddex/{org_id}/{device_id}/ota | Subscribe | Receive OTA firmware update notifications |
embeddex/{org_id}/{device_id}/command | Subscribe | Receive 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 dischargebatteryTemperature(number, required) - Battery temperature in °Cstatus(number 0-16, required) - Battery analysis status code (see below)chargeTemperature(number) - Charge controller temperature in °CdischargeTemperature(number) - Discharge controller temperature in °CnegativeVoltage(number) - Negative terminal voltage in Volts
Battery Status Codes:
0- INIT: Initialization / Idle2- DISCHARGING: Active discharge in progress3- DISCHARGE_COMPLETE: Discharge phase complete5- CHARGING: Charging in progress7- PRE_ANALYSIS: Pre-analysis checks8- ANALYSIS_COMPLETE: ⭐ Analysis cycle complete (triggers automatic report generation)15- WARNING: Non-critical warning16- 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 normallyoffline- Device is disconnectedrebooting- Device is restartingupdating- Device is performing OTA updateerror- 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.statusbetween 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:
- Publish telemetry with accurate
metrics.statusvalues during battery analysis - 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:
| Command | Description | Parameters | Expected Response |
|---|---|---|---|
ping | Connectivity check | None | ACK with device info |
reboot | Restart the device | None | ACK, then status transitions: rebooting → offline → online |
config | Update device configuration | {"key": "value"} | ACK with applied config |
ota_update | Firmware update | {"version": "1.0.0", "url": "https://..."} | ACK, download, apply, reboot |
refresh_config | Re-fetch battery configuration | None | ACK, 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 successfullyerror- Command failed (provide error message inerrorfield)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
- Device boots and connects to WiFi
- Device makes HTTPS GET request to fetch configuration
- If config exists → Device enters
STATE_READY, can analyze - 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
| Parameter | Type | Unit | Description |
|---|---|---|---|
name | string | - | Configuration name (e.g., “S28(B28)“) |
cell_type | number | - | Battery cell type identifier |
voltage_min | number | V | Minimum safe voltage per cell |
voltage_max | number | V | Maximum safe voltage per cell |
discharge_voltage_min | number | V | Discharge cutoff voltage |
charge_voltage_max | number | V | Charge cutoff voltage |
wait_time_after_discharge | number | s | Rest time after discharge phase |
wait_time_after_charge | number | s | Rest time after charge phase |
temperature_min | number | °C | Minimum operating temperature |
temperature_max | number | °C | Maximum operating temperature |
discharge_current | number | mA | Discharge current |
charge_current | number | mA | Charge current |
cutoff_current | number | mA | Cutoff current for charge termination |
charge_current_1 | number | mA | Stage 1 charge current (optional) |
charge_current_2 | number | mA | Stage 2 charge current (optional) |
charge_time_1 | number | s | Stage 1 charge time (optional) |
charge_time_2 | number | s | Stage 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
| State | Description | Can Analyze? |
|---|---|---|
STATE_CONNECTING | Connecting to WiFi/MQTT | No |
STATE_WAITING_CONFIG | No configuration assigned | No |
STATE_READY | Configuration loaded, ready | Yes |
STATE_ANALYSING | Battery analysis in progress | - |
STATE_ERROR | Critical error occurred | No |
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
| From | To | Trigger |
|---|---|---|
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 command |
STATE_ANALYSING | STATE_READY | Analysis complete |
| Any state | STATE_ERROR | Critical error detected |
STATE_ERROR | STATE_CONNECTING | Reboot command |
Boot Sequence
When your ESP32 boots, it follows this sequence:
- Initialize Serial - Start serial communication for debugging
- Connect to WiFi - Connect to configured wireless network
- Sync NTP time - Synchronize clock for accurate timestamps
- Load cached config from NVS - Check for previously saved configuration
- HTTPS GET to fetch fresh configuration - Request latest config from server
- Connect to MQTT broker - Establish connection to EMQX
- Subscribe to command topics - Listen for remote commands
- Enter operational state -
STATE_READY(if config) orSTATE_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
- ESP32 Arduino Core - ESP32 firmware framework
- PlatformIO Documentation - Build system and IDE
- MQTT Protocol Specification - MQTT 3.1.1 specification
- ArduinoJson Library - JSON serialization for Arduino