Complete IoT monitoring platform for Acuvim II power meters via ESP32. Firmware (Phases 1-7): - ESP32-WROVER-B (TTGO T-Call v1.4) with RS485 Modbus RTU - WiFi STA+AP concurrent mode with GSM/GPRS failover - Transport abstraction layer with 4 priority modes - MQTT protocol with 20 commands, LWT, QoS, exponential backoff - SD card offline buffering with JSONL rotation and non-blocking drain - OTA firmware updates with dual partition rollback protection - Watchdog timer, crash loop detection, Acuvim health monitoring - Captive portal provisioning with AP mode Console backend (Phase 8): - .NET 10 minimal API with PostgreSQL + EF Core - JWT authentication, SignalR real-time updates - MQTTnet 5.x bridge service with health monitoring - Device, telemetry, firmware, alert, group management - Rate limiting, security headers, Swagger/OpenAPI Frontend (Phase 9): - React 18 + TypeScript + Vite with Ant Design 5 - ECharts telemetry visualization, TanStack Query - SignalR live updates, device management UI - Dashboard, fleet management, firmware deployment Testing & Production (Phase 10): - 28 firmware unit tests (Modbus, JSON, config, version) - 23 xUnit backend tests (device, telemetry, command, alert) - Docker Compose with nginx, TLS MQTT, PostgreSQL - Production deployment, commissioning, and troubleshooting docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1162 lines
26 KiB
Markdown
1162 lines
26 KiB
Markdown
# MQTT Protocol Reference
|
|
|
|
Complete reference for the MQTT topic structure, payload formats, command/response protocol, and quality-of-service settings used between ESP32 devices and the console application.
|
|
|
|
---
|
|
|
|
## 1. Topic Structure
|
|
|
|
All topics use the following prefix convention:
|
|
|
|
```
|
|
{prefix}/{device_id}/{message_type}
|
|
```
|
|
|
|
- **prefix**: Configurable, default `acuvim`
|
|
- **device_id**: Unique device identifier, format `ACV-AABBCCDDEEFF` (derived from MAC address)
|
|
- **message_type**: One of the types listed below
|
|
|
|
### Topic Map
|
|
|
|
| Topic | Direction | QoS | Retain | Description |
|
|
|-------|-----------|-----|--------|-------------|
|
|
| `acuvim/{device_id}/telemetry` | Device -> Broker | 0 | No | Acuvim II power meter readings |
|
|
| `acuvim/{device_id}/heartbeat` | Device -> Broker | 1 | No | Device health and status |
|
|
| `acuvim/{device_id}/cmd` | Console -> Device | 1 | No | Commands to the device |
|
|
| `acuvim/{device_id}/resp` | Device -> Console | 1 | No | Command responses |
|
|
| `acuvim/{device_id}/status` | Device / Broker (LWT) | 1 | Yes | Online/offline status |
|
|
| `acuvim/{device_id}/alerts` | Device -> Broker | 1 | No | Threshold violation alerts |
|
|
| `devices/register` | Device -> Broker | 1 | No | Device self-registration |
|
|
|
|
### Wildcard Subscriptions (Console)
|
|
|
|
The console application subscribes to the following wildcard topics to receive messages from all devices:
|
|
|
|
```
|
|
acuvim/+/telemetry
|
|
acuvim/+/heartbeat
|
|
acuvim/+/resp
|
|
acuvim/+/status
|
|
acuvim/+/alerts
|
|
devices/register
|
|
```
|
|
|
|
---
|
|
|
|
## 2. Telemetry
|
|
|
|
### Topic
|
|
|
|
```
|
|
acuvim/{device_id}/telemetry
|
|
```
|
|
|
|
### QoS and Delivery
|
|
|
|
- **QoS 0** (at most once): Telemetry is high-frequency data. Occasional message loss is acceptable. Using QoS 0 reduces overhead, especially over GSM.
|
|
- **Retain:** No
|
|
- **Publish interval:** Configurable via `poll_interval_sec`, default 5 seconds on WiFi, 30 seconds on GSM.
|
|
|
|
### Payload Format
|
|
|
|
```json
|
|
{
|
|
"ts": 1716000000,
|
|
"dev": "ACV-AABBCCDDEEFF",
|
|
"v": {
|
|
"a": 230.1,
|
|
"b": 231.4,
|
|
"c": 229.8,
|
|
"ab": 399.2,
|
|
"bc": 400.1,
|
|
"ca": 398.7
|
|
},
|
|
"i": {
|
|
"a": 15.2,
|
|
"b": 14.8,
|
|
"c": 15.5
|
|
},
|
|
"p": {
|
|
"total": 10.5,
|
|
"a": 3.5,
|
|
"b": 3.4,
|
|
"c": 3.6,
|
|
"reactive": 2.1,
|
|
"apparent": 10.7,
|
|
"pf": 0.98
|
|
},
|
|
"f": 50.01,
|
|
"e": {
|
|
"imp_act": 12345.6,
|
|
"exp_act": 0.0,
|
|
"imp_react": 1234.5,
|
|
"exp_react": 0.0
|
|
},
|
|
"d": {
|
|
"act": 10.2,
|
|
"max_act": 15.0,
|
|
"react": 2.0
|
|
},
|
|
"thd": {
|
|
"va": 2.1,
|
|
"vb": 2.3,
|
|
"vc": 2.0,
|
|
"ia": 5.4,
|
|
"ib": 5.1,
|
|
"ic": 5.6
|
|
},
|
|
"conn": "wifi",
|
|
"rssi": -45,
|
|
"src": "live"
|
|
}
|
|
```
|
|
|
|
### Field Reference
|
|
|
|
| Field Path | Type | Unit | Description |
|
|
|------------|------|------|-------------|
|
|
| `ts` | integer | epoch seconds (UTC) | Timestamp of the reading |
|
|
| `dev` | string | - | Device ID |
|
|
| `v.a` | float | V | Phase A voltage (L-N) |
|
|
| `v.b` | float | V | Phase B voltage (L-N) |
|
|
| `v.c` | float | V | Phase C voltage (L-N) |
|
|
| `v.ab` | float | V | Line voltage A-B |
|
|
| `v.bc` | float | V | Line voltage B-C |
|
|
| `v.ca` | float | V | Line voltage C-A |
|
|
| `i.a` | float | A | Phase A current |
|
|
| `i.b` | float | A | Phase B current |
|
|
| `i.c` | float | A | Phase C current |
|
|
| `p.total` | float | kW | Total active power |
|
|
| `p.a` | float | kW | Phase A active power |
|
|
| `p.b` | float | kW | Phase B active power |
|
|
| `p.c` | float | kW | Phase C active power |
|
|
| `p.reactive` | float | kVAR | Total reactive power |
|
|
| `p.apparent` | float | kVA | Total apparent power |
|
|
| `p.pf` | float | - | Total power factor (-1.0 to 1.0) |
|
|
| `f` | float | Hz | Line frequency |
|
|
| `e.imp_act` | float | kWh | Import active energy (accumulated) |
|
|
| `e.exp_act` | float | kWh | Export active energy (accumulated) |
|
|
| `e.imp_react` | float | kVARh | Import reactive energy (accumulated) |
|
|
| `e.exp_react` | float | kVARh | Export reactive energy (accumulated) |
|
|
| `d.act` | float | kW | Active power demand |
|
|
| `d.max_act` | float | kW | Maximum active power demand |
|
|
| `d.react` | float | kVAR | Reactive power demand |
|
|
| `thd.va` | float | % | Voltage THD Phase A |
|
|
| `thd.vb` | float | % | Voltage THD Phase B |
|
|
| `thd.vc` | float | % | Voltage THD Phase C |
|
|
| `thd.ia` | float | % | Current THD Phase A |
|
|
| `thd.ib` | float | % | Current THD Phase B |
|
|
| `thd.ic` | float | % | Current THD Phase C |
|
|
| `conn` | string | - | Active transport: `"wifi"` or `"gsm"` |
|
|
| `rssi` | integer | dBm | Signal strength (WiFi RSSI or GSM equivalent) |
|
|
| `src` | string | - | Data source: `"live"` (real-time) or `"sd"` (buffered from SD card) |
|
|
|
|
### Notes
|
|
|
|
- All floating-point values are IEEE 754 single-precision, read from Modbus as big-endian 32-bit floats across two consecutive registers.
|
|
- Short JSON keys are used to minimize payload size, which is important when transmitting over GSM.
|
|
- The `thd` group may be omitted when transmitting over GSM to reduce payload size.
|
|
- When replaying buffered data from the SD card, `src` is set to `"sd"` and the `ts` reflects the original measurement time.
|
|
|
|
---
|
|
|
|
## 3. Heartbeat
|
|
|
|
### Topic
|
|
|
|
```
|
|
acuvim/{device_id}/heartbeat
|
|
```
|
|
|
|
### QoS and Delivery
|
|
|
|
- **QoS 1** (at least once): Heartbeats are critical for device health monitoring. Guaranteed delivery ensures the console can accurately track device status.
|
|
- **Retain:** No
|
|
- **Publish interval:** Configurable via `heartbeat_interval_sec`, default 60 seconds.
|
|
- Sent immediately on boot, on transport switch, and on demand via the `set_heartbeat` command.
|
|
|
|
### Payload Format
|
|
|
|
```json
|
|
{
|
|
"ts": 1716000000,
|
|
"dev": "ACV-AABBCCDDEEFF",
|
|
"fw": "1.2.0",
|
|
"up": 86400,
|
|
"boot": 3,
|
|
"hb": 1440,
|
|
"conn": {
|
|
"type": "wifi",
|
|
"wifi": {
|
|
"ssid": "SiteNetwork",
|
|
"rssi": -45,
|
|
"ip": "192.168.1.100"
|
|
},
|
|
"gsm": {
|
|
"enabled": true,
|
|
"connected": false,
|
|
"signal": 0,
|
|
"operator": ""
|
|
},
|
|
"mqtt": true
|
|
},
|
|
"health": {
|
|
"heap_free": 120000,
|
|
"heap_min": 95000,
|
|
"psram_free": 3800000,
|
|
"cpu_temp": 52.3,
|
|
"reset_reason": "POWER_ON",
|
|
"uptime_sec": 86400
|
|
},
|
|
"modbus": {
|
|
"connected": true,
|
|
"success": 17280,
|
|
"errors": 12,
|
|
"error_rate": 0.07,
|
|
"last_error": 0,
|
|
"last_read_ms": 45
|
|
},
|
|
"sd": {
|
|
"available": true,
|
|
"queued": 0,
|
|
"free_mb": 3800
|
|
},
|
|
"ota": {
|
|
"version": "1.2.0",
|
|
"partition": "app0",
|
|
"update_available": false
|
|
}
|
|
}
|
|
```
|
|
|
|
### Field Reference
|
|
|
|
| Field Path | Type | Description |
|
|
|------------|------|-------------|
|
|
| `ts` | integer | UTC epoch timestamp |
|
|
| `dev` | string | Device ID |
|
|
| `fw` | string | Current firmware version (semver) |
|
|
| `up` | integer | Uptime in seconds since last boot |
|
|
| `boot` | integer | Total boot count (persisted in NVS, increments each restart) |
|
|
| `hb` | integer | Heartbeat sequence number since last boot |
|
|
| `conn.type` | string | Active transport: `"wifi"` or `"gsm"` |
|
|
| `conn.wifi.ssid` | string | Connected WiFi SSID |
|
|
| `conn.wifi.rssi` | integer | WiFi signal strength in dBm |
|
|
| `conn.wifi.ip` | string | Assigned IP address |
|
|
| `conn.gsm.enabled` | boolean | Whether GSM is enabled in config |
|
|
| `conn.gsm.connected` | boolean | Whether GSM is currently connected |
|
|
| `conn.gsm.signal` | integer | GSM signal quality (0-31 CSQ scale) |
|
|
| `conn.gsm.operator` | string | Mobile network operator name |
|
|
| `conn.mqtt` | boolean | Whether MQTT is currently connected |
|
|
| `health.heap_free` | integer | Current free heap memory in bytes |
|
|
| `health.heap_min` | integer | Minimum free heap since boot (leak detection) |
|
|
| `health.psram_free` | integer | Free PSRAM in bytes |
|
|
| `health.cpu_temp` | float | ESP32 internal temperature in Celsius |
|
|
| `health.reset_reason` | string | Last reset reason (see table below) |
|
|
| `health.uptime_sec` | integer | Uptime in seconds |
|
|
| `modbus.connected` | boolean | Whether last Modbus read succeeded |
|
|
| `modbus.success` | integer | Total successful Modbus reads since boot |
|
|
| `modbus.errors` | integer | Total failed Modbus reads since boot |
|
|
| `modbus.error_rate` | float | Error percentage (errors / total * 100) |
|
|
| `modbus.last_error` | integer | Last Modbus error code (0 = none) |
|
|
| `modbus.last_read_ms` | integer | Duration of last complete read cycle in ms |
|
|
| `sd.available` | boolean | Whether an SD card is detected |
|
|
| `sd.queued` | integer | Number of buffered telemetry records awaiting drain |
|
|
| `sd.free_mb` | integer | Free space on SD card in megabytes |
|
|
| `ota.version` | string | Currently running firmware version |
|
|
| `ota.partition` | string | Active OTA partition (`"app0"` or `"app1"`) |
|
|
| `ota.update_available` | boolean | Whether a newer firmware version is available |
|
|
|
|
### Reset Reason Values
|
|
|
|
| Value | Meaning |
|
|
|-------|---------|
|
|
| `POWER_ON` | Normal power-on reset |
|
|
| `EXTERNAL` | External reset pin |
|
|
| `SW_RESET` | Software restart (e.g., `ESP.restart()`) |
|
|
| `PANIC` | Guru Meditation / exception handler |
|
|
| `INT_WDT` | Interrupt watchdog timeout |
|
|
| `TASK_WDT` | Task watchdog timeout |
|
|
| `WDT` | Other watchdog timeout |
|
|
| `DEEP_SLEEP` | Wake from deep sleep |
|
|
| `BROWNOUT` | Brownout reset (low voltage) |
|
|
| `UNKNOWN` | Unrecognized reset reason |
|
|
|
|
### Console Health Detection
|
|
|
|
The console uses heartbeat timing to determine device status:
|
|
|
|
| Condition | Status |
|
|
|-----------|--------|
|
|
| Heartbeat received within 1x interval | Online |
|
|
| No heartbeat for 3x interval | Degraded |
|
|
| No heartbeat for 5x interval | Offline |
|
|
|
|
---
|
|
|
|
## 4. Command / Response Protocol
|
|
|
|
### Command (Console to Device)
|
|
|
|
**Topic:** `acuvim/{device_id}/cmd`
|
|
**QoS:** 1
|
|
**Retain:** No
|
|
|
|
```json
|
|
{
|
|
"cmd": "<command_name>",
|
|
"request_id": "<unique_identifier>",
|
|
"params": { }
|
|
}
|
|
```
|
|
|
|
- `cmd`: The command name (see complete list below).
|
|
- `request_id`: A unique string generated by the console to correlate the response. Format is typically a UUID or short identifier like `"cmd-a1b2c3"`.
|
|
- `params`: Command-specific parameters. Omit or pass `{}` for commands with no parameters.
|
|
|
|
### Response (Device to Console)
|
|
|
|
**Topic:** `acuvim/{device_id}/resp`
|
|
**QoS:** 1
|
|
**Retain:** No
|
|
|
|
```json
|
|
{
|
|
"request_id": "<matching_identifier>",
|
|
"status": "success",
|
|
"data": { },
|
|
"message": "Human-readable description"
|
|
}
|
|
```
|
|
|
|
- `request_id`: Must match the `request_id` from the originating command.
|
|
- `status`: Either `"success"` or `"error"`.
|
|
- `data`: Command-specific response data. May be omitted for simple acknowledgements.
|
|
- `message`: Human-readable status message.
|
|
|
|
### Command Timeout
|
|
|
|
If the console does not receive a response within 60 seconds, the command is marked as `"timeout"`. The device should respond as quickly as possible; most commands respond in under 2 seconds.
|
|
|
|
---
|
|
|
|
## 5. Complete Command Reference
|
|
|
|
### 5.1 ping
|
|
|
|
Connectivity test. The device responds with a pong.
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"cmd": "ping",
|
|
"request_id": "ping-001"
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"request_id": "ping-001",
|
|
"status": "success",
|
|
"data": { "pong": true },
|
|
"message": "pong"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 5.2 get_config
|
|
|
|
Returns the full device configuration (passwords are masked).
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"cmd": "get_config",
|
|
"request_id": "cfg-001"
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"request_id": "cfg-001",
|
|
"status": "success",
|
|
"data": {
|
|
"device_id": "ACV-AABBCCDDEEFF",
|
|
"wifi": {
|
|
"ssid": "SiteNetwork",
|
|
"enabled": true
|
|
},
|
|
"mqtt": {
|
|
"broker": "console.example.com",
|
|
"port": 8883,
|
|
"tls": true,
|
|
"topic_prefix": "acuvim"
|
|
},
|
|
"gsm": {
|
|
"apn": "internet",
|
|
"enabled": true
|
|
},
|
|
"modbus": {
|
|
"slave_addr": 1,
|
|
"baud_rate": 9600,
|
|
"poll_interval": 5
|
|
},
|
|
"heartbeat_interval": 60,
|
|
"console_url": "https://console.example.com",
|
|
"firmware_version": "1.2.0"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 5.3 get_status
|
|
|
|
Returns the current device status including health metrics.
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"cmd": "get_status",
|
|
"request_id": "stat-001"
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"request_id": "stat-001",
|
|
"status": "success",
|
|
"data": {
|
|
"uptime": 86400,
|
|
"heap_free": 120000,
|
|
"heap_min": 95000,
|
|
"wifi_connected": true,
|
|
"wifi_rssi": -45,
|
|
"gsm_connected": false,
|
|
"mqtt_connected": true,
|
|
"modbus_connected": true,
|
|
"modbus_errors": 12,
|
|
"sd_available": true,
|
|
"sd_queued": 0,
|
|
"boot_count": 3,
|
|
"reset_reason": "POWER_ON"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 5.4 get_telemetry
|
|
|
|
Returns the most recent Acuvim II reading.
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"cmd": "get_telemetry",
|
|
"request_id": "tel-001"
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"request_id": "tel-001",
|
|
"status": "success",
|
|
"data": {
|
|
"ts": 1716000000,
|
|
"v": { "a": 230.1, "b": 231.4, "c": 229.8 },
|
|
"i": { "a": 15.2, "b": 14.8, "c": 15.5 },
|
|
"p": { "total": 10.5, "pf": 0.98 },
|
|
"f": 50.01
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 5.5 restart
|
|
|
|
Restarts the ESP32. The device publishes the response before restarting.
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"cmd": "restart",
|
|
"request_id": "rst-001"
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"request_id": "rst-001",
|
|
"status": "success",
|
|
"message": "Restarting in 2 seconds"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 5.6 factory_reset
|
|
|
|
Clears all NVS configuration and restarts the device into AP mode. Requires explicit confirmation.
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"cmd": "factory_reset",
|
|
"request_id": "fr-001",
|
|
"params": {
|
|
"confirm": true
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"request_id": "fr-001",
|
|
"status": "success",
|
|
"message": "Factory reset complete, restarting into AP mode"
|
|
}
|
|
```
|
|
|
|
If `confirm` is missing or `false`:
|
|
```json
|
|
{
|
|
"request_id": "fr-001",
|
|
"status": "error",
|
|
"message": "Factory reset requires params.confirm = true"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 5.7 wifi_scan
|
|
|
|
Scans for available WiFi networks and returns the results.
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"cmd": "wifi_scan",
|
|
"request_id": "scan-001"
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"request_id": "scan-001",
|
|
"status": "success",
|
|
"data": {
|
|
"networks": [
|
|
{ "ssid": "SiteNetwork", "rssi": -40, "enc": "WPA2", "ch": 6 },
|
|
{ "ssid": "GuestWifi", "rssi": -65, "enc": "WPA2", "ch": 11 },
|
|
{ "ssid": "OpenNet", "rssi": -78, "enc": "OPEN", "ch": 1 }
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 5.8 wifi_set
|
|
|
|
Updates WiFi credentials. The device will disconnect from the current network and connect to the new one.
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"cmd": "wifi_set",
|
|
"request_id": "wset-001",
|
|
"params": {
|
|
"ssid": "NewNetwork",
|
|
"password": "newpassword123"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"request_id": "wset-001",
|
|
"status": "success",
|
|
"message": "WiFi credentials updated, reconnecting"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 5.9 wifi_disconnect
|
|
|
|
Disconnects from the current WiFi network without clearing credentials.
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"cmd": "wifi_disconnect",
|
|
"request_id": "wdisc-001"
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"request_id": "wdisc-001",
|
|
"status": "success",
|
|
"message": "WiFi disconnected"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 5.10 mqtt_set
|
|
|
|
Updates the MQTT broker configuration. The device will disconnect and reconnect with the new settings.
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"cmd": "mqtt_set",
|
|
"request_id": "mset-001",
|
|
"params": {
|
|
"broker": "mqtt.newserver.com",
|
|
"port": 8883,
|
|
"username": "ACV-AABBCCDDEEFF",
|
|
"password": "newpassword",
|
|
"topic_prefix": "acuvim",
|
|
"tls": true
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"request_id": "mset-001",
|
|
"status": "success",
|
|
"message": "MQTT configuration updated, reconnecting"
|
|
}
|
|
```
|
|
|
|
**Warning:** This command can render the device unreachable via MQTT if incorrect values are provided. Ensure fallback access via the captive portal.
|
|
|
|
---
|
|
|
|
### 5.11 gsm_set
|
|
|
|
Updates GSM/cellular configuration.
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"cmd": "gsm_set",
|
|
"request_id": "gset-001",
|
|
"params": {
|
|
"apn": "internet",
|
|
"username": "",
|
|
"password": "",
|
|
"enabled": true
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"request_id": "gset-001",
|
|
"status": "success",
|
|
"message": "GSM configuration updated"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 5.12 transport_priority
|
|
|
|
Sets the transport priority order for network failover.
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"cmd": "transport_priority",
|
|
"request_id": "tp-001",
|
|
"params": {
|
|
"priority": "wifi_first"
|
|
}
|
|
}
|
|
```
|
|
|
|
Valid values for `priority`:
|
|
|
|
| Value | Behaviour |
|
|
|-------|-----------|
|
|
| `wifi_first` | Prefer WiFi, fall back to GSM (default) |
|
|
| `gsm_first` | Prefer GSM, fall back to WiFi |
|
|
| `wifi_only` | WiFi only, no GSM fallback |
|
|
| `gsm_only` | GSM only, no WiFi |
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"request_id": "tp-001",
|
|
"status": "success",
|
|
"message": "Transport priority set to wifi_first"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 5.13 sleep_set
|
|
|
|
Configures deep sleep mode.
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"cmd": "sleep_set",
|
|
"request_id": "slp-001",
|
|
"params": {
|
|
"enabled": true,
|
|
"sleep_min": 15,
|
|
"wake_sec": 60
|
|
}
|
|
}
|
|
```
|
|
|
|
- `sleep_min`: Duration of deep sleep in minutes.
|
|
- `wake_sec`: Duration the device stays awake to transmit data.
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"request_id": "slp-001",
|
|
"status": "success",
|
|
"message": "Sleep mode configured: 15min sleep, 60s wake"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 5.14 modbus_set
|
|
|
|
Updates Modbus RS485 communication parameters.
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"cmd": "modbus_set",
|
|
"request_id": "mb-001",
|
|
"params": {
|
|
"slave_addr": 1,
|
|
"baud_rate": 9600,
|
|
"poll_interval": 5
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"request_id": "mb-001",
|
|
"status": "success",
|
|
"message": "Modbus configuration updated"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 5.15 console_set
|
|
|
|
Sets the console application URL used for OTA firmware downloads.
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"cmd": "console_set",
|
|
"request_id": "curl-001",
|
|
"params": {
|
|
"url": "https://console.example.com"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"request_id": "curl-001",
|
|
"status": "success",
|
|
"message": "Console URL updated"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 5.16 ota_update
|
|
|
|
Triggers an OTA firmware update. The device will download the binary from the specified URL, verify the checksum, and install it.
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"cmd": "ota_update",
|
|
"request_id": "ota-001",
|
|
"params": {
|
|
"url": "https://console.example.com/api/firmware/download/1.3.0",
|
|
"version": "1.3.0",
|
|
"checksum": "a1b2c3d4e5f6..."
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response (immediate acknowledgement):**
|
|
```json
|
|
{
|
|
"request_id": "ota-001",
|
|
"status": "success",
|
|
"message": "OTA update started, downloading 1.3.0"
|
|
}
|
|
```
|
|
|
|
The device will reboot after successful installation. The console should monitor for the device to come back online with the new firmware version in its next heartbeat.
|
|
|
|
---
|
|
|
|
### 5.17 ota_check
|
|
|
|
Checks with the console whether a firmware update is available.
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"cmd": "ota_check",
|
|
"request_id": "ochk-001"
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"request_id": "ochk-001",
|
|
"status": "success",
|
|
"data": {
|
|
"update_available": true,
|
|
"current_version": "1.2.0",
|
|
"available_version": "1.3.0",
|
|
"url": "https://console.example.com/api/firmware/download/1.3.0",
|
|
"size": 1048576,
|
|
"checksum": "a1b2c3d4e5f6...",
|
|
"mandatory": false
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 5.18 set_heartbeat
|
|
|
|
Changes the heartbeat reporting interval.
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"cmd": "set_heartbeat",
|
|
"request_id": "hb-001",
|
|
"params": {
|
|
"interval_sec": 30
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"request_id": "hb-001",
|
|
"status": "success",
|
|
"message": "Heartbeat interval set to 30 seconds"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 5.19 set_alerts
|
|
|
|
Configures alert thresholds for the Acuvim II health monitoring.
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"cmd": "set_alerts",
|
|
"request_id": "alt-001",
|
|
"params": {
|
|
"thresholds": {
|
|
"overvoltage": 260.0,
|
|
"undervoltage": 200.0,
|
|
"overcurrent_factor": 1.2,
|
|
"voltage_imbalance_pct": 10.0,
|
|
"current_imbalance_pct": 20.0,
|
|
"frequency_low": 49.5,
|
|
"frequency_high": 50.5,
|
|
"low_power_factor": 0.85,
|
|
"high_thd": 8.0
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```json
|
|
{
|
|
"request_id": "alt-001",
|
|
"status": "success",
|
|
"message": "Alert thresholds updated"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 5.20 register_ack
|
|
|
|
Sent by the console to acknowledge a device registration. This is the only command initiated by the console without a prior user action.
|
|
|
|
**Request (Console to Device):**
|
|
```json
|
|
{
|
|
"cmd": "register_ack",
|
|
"request_id": "reg-001",
|
|
"params": {
|
|
"status": "accepted",
|
|
"device_name": "Building A - Main Incomer"
|
|
}
|
|
}
|
|
```
|
|
|
|
The device saves the acknowledgement to NVS and uses `device_name` for display in the captive portal.
|
|
|
|
---
|
|
|
|
## 6. Alert Payload
|
|
|
|
### Topic
|
|
|
|
```
|
|
acuvim/{device_id}/alerts
|
|
```
|
|
|
|
### QoS and Delivery
|
|
|
|
- **QoS 1**: Alerts represent important events that should not be lost.
|
|
- **Retain:** No
|
|
|
|
### Payload Format
|
|
|
|
```json
|
|
{
|
|
"ts": 1716000000,
|
|
"dev": "ACV-AABBCCDDEEFF",
|
|
"alert": "overvoltage",
|
|
"severity": "warning",
|
|
"message": "Phase A voltage 265.3V exceeds 260V threshold",
|
|
"phase": "A",
|
|
"value": 265.3,
|
|
"threshold": 260.0
|
|
}
|
|
```
|
|
|
|
### Field Reference
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `ts` | integer | UTC epoch timestamp |
|
|
| `dev` | string | Device ID |
|
|
| `alert` | string | Alert type identifier (see table below) |
|
|
| `severity` | string | `"info"`, `"warning"`, or `"critical"` |
|
|
| `message` | string | Human-readable description |
|
|
| `phase` | string | Affected phase (`"A"`, `"B"`, `"C"`, or `null` for non-phase-specific alerts) |
|
|
| `value` | float | Measured value that triggered the alert |
|
|
| `threshold` | float | Configured threshold that was exceeded |
|
|
|
|
### Alert Types
|
|
|
|
| Alert Type | Default Threshold | Severity | Description |
|
|
|------------|-------------------|----------|-------------|
|
|
| `overvoltage` | > 260 V | warning | Phase voltage exceeds upper limit |
|
|
| `undervoltage` | < 200 V | warning | Phase voltage below lower limit |
|
|
| `overcurrent` | > CT rated x 1.2 | warning | Phase current exceeds rated capacity |
|
|
| `voltage_imbalance` | > 10% difference | info | Excessive voltage imbalance between phases |
|
|
| `current_imbalance` | > 20% difference | info | Excessive current imbalance between phases |
|
|
| `frequency_deviation` | < 49.5 or > 50.5 Hz | warning | Frequency outside normal range |
|
|
| `low_power_factor` | < 0.85 | info | Poor power factor |
|
|
| `high_thd` | > 8% | info | High total harmonic distortion |
|
|
| `modbus_loss` | 5 consecutive errors | critical | Lost communication with Acuvim II |
|
|
| `modbus_high_error_rate` | > 5% over 1 hour | warning | Elevated Modbus error rate |
|
|
|
|
### Alert Deduplication
|
|
|
|
The device does not re-publish the same alert while the condition persists. An alert is considered cleared when the value returns within the threshold minus a hysteresis margin (e.g., overvoltage triggers at 260V, clears at 255V). After clearing, the alert can trigger again if the condition reoccurs.
|
|
|
|
---
|
|
|
|
## 7. Device Registration
|
|
|
|
### Topic
|
|
|
|
```
|
|
devices/register
|
|
```
|
|
|
|
### QoS and Delivery
|
|
|
|
- **QoS 1**: Registration must be reliably delivered.
|
|
- **Retain:** No
|
|
|
|
### Payload Format
|
|
|
|
```json
|
|
{
|
|
"device_id": "ACV-AABBCCDDEEFF",
|
|
"mac": "AA:BB:CC:DD:EE:FF",
|
|
"firmware": "1.2.0",
|
|
"hardware": "TTGO T-Call v1.4",
|
|
"chip": "ESP32-WROVER-B",
|
|
"imei": "123456789012345",
|
|
"capabilities": ["wifi", "gsm", "sd", "modbus", "ota"],
|
|
"ts": 1716000000
|
|
}
|
|
```
|
|
|
|
### Field Reference
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `device_id` | string | Unique device ID derived from MAC address |
|
|
| `mac` | string | WiFi MAC address |
|
|
| `firmware` | string | Current firmware version |
|
|
| `hardware` | string | Board/hardware identification |
|
|
| `chip` | string | ESP32 chip model |
|
|
| `imei` | string | GSM modem IMEI (empty string if no GSM module) |
|
|
| `capabilities` | array | List of available features, dynamically detected at boot |
|
|
| `ts` | integer | UTC epoch timestamp |
|
|
|
|
### Capabilities
|
|
|
|
| Capability | Meaning |
|
|
|------------|---------|
|
|
| `wifi` | WiFi radio available (always present) |
|
|
| `gsm` | GSM modem detected and operational |
|
|
| `sd` | SD card detected and mounted |
|
|
| `modbus` | Modbus RS485 interface available (always present) |
|
|
| `ota` | OTA update support (always present) |
|
|
|
|
### Registration Triggers
|
|
|
|
The device publishes a registration message:
|
|
|
|
1. On first boot (no registration acknowledgement stored in NVS).
|
|
2. After a factory reset (NVS cleared).
|
|
3. After a firmware update (re-register with new version).
|
|
4. Every 24 hours (periodic re-registration to confirm presence).
|
|
|
|
---
|
|
|
|
## 8. Last Will and Testament (LWT)
|
|
|
|
### Topic
|
|
|
|
```
|
|
acuvim/{device_id}/status
|
|
```
|
|
|
|
### QoS, Retain, and Behaviour
|
|
|
|
- **QoS 1**
|
|
- **Retain: Yes** (so new subscribers immediately know the device state)
|
|
|
|
The LWT is configured at MQTT connect time. If the device disconnects unexpectedly (network loss, crash, power failure), the broker publishes the LWT message on the device's behalf.
|
|
|
|
### LWT Payload (Published by Broker on Unexpected Disconnect)
|
|
|
|
```json
|
|
{
|
|
"status": "offline",
|
|
"timestamp": 0
|
|
}
|
|
```
|
|
|
|
The `timestamp` in the LWT is set to `0` because the broker publishes it and does not know the actual time. The console uses the message receipt time instead.
|
|
|
|
### Online Payload (Published by Device on Connect)
|
|
|
|
Immediately after connecting, the device publishes to the same topic:
|
|
|
|
```json
|
|
{
|
|
"status": "online",
|
|
"timestamp": 1716000000,
|
|
"ip": "192.168.1.100",
|
|
"fw": "1.2.0"
|
|
}
|
|
```
|
|
|
|
This overwrites the retained LWT message, so any new subscriber will see the device as online.
|
|
|
|
---
|
|
|
|
## 9. QoS Summary
|
|
|
|
| Message Type | QoS | Rationale |
|
|
|--------------|-----|-----------|
|
|
| Telemetry | 0 | High-frequency, loss tolerable, reduces broker and network overhead |
|
|
| Heartbeat | 1 | Critical for status tracking, must be reliably delivered |
|
|
| Command (cmd) | 1 | Must reach the device to trigger actions |
|
|
| Response (resp) | 1 | Console needs to confirm command execution |
|
|
| Status (LWT) | 1 | Online/offline state must be reliably propagated |
|
|
| Alerts | 1 | Important events that should not be missed |
|
|
| Registration | 1 | Device must be registered in the console |
|
|
|
|
---
|
|
|
|
## 10. MQTT Client Configuration
|
|
|
|
### Device (ESP32)
|
|
|
|
| Parameter | Value |
|
|
|-----------|-------|
|
|
| Client ID | Device ID (e.g., `ACV-AABBCCDDEEFF`) |
|
|
| Keep Alive | 60 seconds |
|
|
| Clean Session | `false` (broker retains subscriptions across reconnects) |
|
|
| Max Payload Size | 1024 bytes (PubSubClient default, increase if needed) |
|
|
| Reconnect Backoff | 10s, 20s, 40s, 60s (max) |
|
|
|
|
### Console (.NET)
|
|
|
|
| Parameter | Value |
|
|
|-----------|-------|
|
|
| Client ID | `acuvim-console` |
|
|
| Keep Alive | 60 seconds |
|
|
| Clean Session | `false` |
|
|
| Protocol | MQTT v5 (via MQTTnet) |
|
|
| Auto Reconnect | Yes |
|
|
|
|
---
|
|
|
|
## 11. Topic Examples
|
|
|
|
For a device with ID `ACV-A1B2C3D4E5F6`:
|
|
|
|
```
|
|
acuvim/ACV-A1B2C3D4E5F6/telemetry <- Device publishes meter readings
|
|
acuvim/ACV-A1B2C3D4E5F6/heartbeat <- Device publishes health data
|
|
acuvim/ACV-A1B2C3D4E5F6/cmd <- Console publishes commands
|
|
acuvim/ACV-A1B2C3D4E5F6/resp <- Device publishes command responses
|
|
acuvim/ACV-A1B2C3D4E5F6/status <- Online/offline (LWT)
|
|
acuvim/ACV-A1B2C3D4E5F6/alerts <- Device publishes threshold alerts
|
|
devices/register <- Device publishes registration
|
|
```
|