Tau.Acuvim/docs/acuvim-spec-03.md
Renier Forster 84a0668c54 Initial commit: Tau Acuvim IoT monitoring system
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>
2026-05-16 19:05:32 +02:00

18 KiB

Phase 3: Captive Portal Web Interface

Objective

Build a web-based configuration interface hosted on the ESP32 that allows users to connect via phone or laptop to configure WiFi, MQTT, sleep settings, and the console application URL. The interface operates in Access Point (AP) mode as a captive portal and optionally in STA+AP concurrent mode when WiFi is already connected.

Prerequisites

  • Phase 2 complete (WiFi, MQTT, NVS working)
  • LittleFS filesystem support in PlatformIO
  • Mobile phone or laptop with a web browser

Deliverables

  1. ESP32 Access Point with captive portal redirect
  2. Responsive web UI (HTML/CSS/JS) stored in LittleFS
  3. REST API endpoints for configuration CRUD
  4. WiFi network scanner (list available SSIDs)
  5. Live status page showing connection and telemetry state
  6. Settings pages for WiFi, MQTT, sleep, console URL, Modbus

3.1 Access Point Configuration

AP Mode Behavior

  • SSID: Acuvim-{device_id_last6} (e.g., Acuvim-DDEEFF)
  • Password: acuvim123 (default, configurable via NVS)
  • IP Address: 192.168.4.1
  • DNS: Captive portal DNS server redirects all domains to 192.168.4.1
  • Channel: Auto-select or 1

Operating Modes

  1. AP-only mode: When no WiFi is configured or WiFi connection fails after 3 attempts. Device runs as AP only so the user can configure it.
  2. STA+AP mode: When WiFi is connected, AP remains active for local configuration access. This allows reconfiguration without losing the MQTT connection.
  3. AP auto-disable: Optionally disable AP after a configurable timeout (default: 5 minutes of inactivity) to save power. Re-enabled by button press or reboot.

Captive Portal

Use DNSServer to redirect all DNS queries to the ESP32 IP. Combined with the standard captive portal detection URLs, mobile devices will auto-open the configuration page:

  • Android: http://connectivitycheck.gstatic.com/generate_204
  • iOS: http://captive.apple.com/hotspot-detect.html
  • Windows: http://www.msftconnecttest.com/connecttest.txt

The web server returns a 302 redirect for any unknown path to http://192.168.4.1/.

3.2 Web Server Architecture

Technology

  • Server: ESPAsyncWebServer (async, non-blocking, handles multiple clients)
  • Filesystem: LittleFS for storing HTML/CSS/JS files
  • API: REST endpoints returning JSON
  • Auth: Basic HTTP authentication for API endpoints (optional, configurable)

Add to platformio.ini:

lib_deps =
    ...existing...
    me-no-dev/ESPAsyncWebServer@^1.2.3
    me-no-dev/AsyncTCP@^1.1.1

board_build.filesystem = littlefs

File Structure in data/ (LittleFS)

firmware/data/
├── index.html          # Main SPA shell
├── app.css             # Styles
├── app.js              # Application logic
├── favicon.ico         # Optional
└── manifest.json       # PWA manifest (optional)

Design decision: Single-page application (SPA) approach. One HTML file with tabbed navigation. All configuration pages are tabs within the same page. This minimizes file count and LittleFS usage.

3.3 REST API Endpoints

All API endpoints are prefixed with /api/.

Device Info

GET /api/status
Response:
{
  "device_id": "ACV-AABBCCDDEEFF",
  "firmware_version": "1.0.0",
  "uptime_sec": 3600,
  "free_heap": 120000,
  "wifi_connected": true,
  "wifi_ssid": "MyNetwork",
  "wifi_rssi": -45,
  "wifi_ip": "192.168.1.100",
  "mqtt_connected": true,
  "mqtt_broker": "broker.example.com",
  "gsm_connected": false,
  "gsm_signal": 0,
  "modbus_connected": true,
  "modbus_success_count": 1234,
  "modbus_error_count": 5,
  "sd_available": false,
  "sd_queued_records": 0,
  "last_telemetry_ts": 1716000000,
  "connection_type": "wifi"
}

WiFi Configuration

GET /api/wifi/scan
Response:
{
  "networks": [
    {"ssid": "Network1", "rssi": -40, "encryption": "WPA2", "channel": 6},
    {"ssid": "Network2", "rssi": -65, "encryption": "WPA2", "channel": 11},
    {"ssid": "OpenNetwork", "rssi": -70, "encryption": "OPEN", "channel": 1}
  ]
}

GET /api/wifi/config
Response:
{
  "ssid": "MyNetwork",
  "password": "********",
  "enabled": true,
  "connected": true,
  "ip": "192.168.1.100"
}

POST /api/wifi/config
Body:
{
  "ssid": "MyNetwork",
  "password": "mypassword",
  "enabled": true
}
Response:
{
  "success": true,
  "message": "WiFi configuration saved. Connecting..."
}

POST /api/wifi/disconnect
Response:
{
  "success": true,
  "message": "WiFi disconnected"
}

MQTT Configuration

GET /api/mqtt/config
Response:
{
  "broker": "broker.example.com",
  "port": 1883,
  "username": "device1",
  "password": "********",
  "topic_prefix": "acuvim",
  "tls": false,
  "connected": true
}

POST /api/mqtt/config
Body:
{
  "broker": "broker.example.com",
  "port": 1883,
  "username": "device1",
  "password": "secretpass",
  "topic_prefix": "acuvim",
  "tls": false
}
Response:
{
  "success": true,
  "message": "MQTT configuration saved. Reconnecting..."
}

POST /api/mqtt/test
Body:
{
  "broker": "broker.example.com",
  "port": 1883,
  "username": "device1",
  "password": "secretpass"
}
Response:
{
  "success": true,
  "message": "MQTT connection test successful"
}

Sleep Configuration

GET /api/sleep/config
Response:
{
  "sleep_enabled": false,
  "sleep_duration_min": 0,
  "wake_duration_sec": 300
}

POST /api/sleep/config
Body:
{
  "sleep_enabled": true,
  "sleep_duration_min": 15,
  "wake_duration_sec": 300
}
Response:
{
  "success": true,
  "message": "Sleep configuration saved. Will take effect after current wake cycle."
}

Console Application

GET /api/console/config
Response:
{
  "url": "https://console.example.com"
}

POST /api/console/config
Body:
{
  "url": "https://console.example.com"
}
Response:
{
  "success": true,
  "message": "Console URL saved"
}

Modbus / Acuvim II

GET /api/modbus/config
Response:
{
  "slave_address": 1,
  "baud_rate": 9600,
  "poll_interval_sec": 5
}

POST /api/modbus/config
Body:
{
  "slave_address": 1,
  "baud_rate": 9600,
  "poll_interval_sec": 5
}
Response:
{
  "success": true,
  "message": "Modbus configuration saved. Reinitializing..."
}

GSM Configuration (Prepared for Phase 4)

GET /api/gsm/config
Response:
{
  "apn": "internet",
  "username": "",
  "password": "",
  "enabled": false,
  "connected": false,
  "signal_strength": 0
}

POST /api/gsm/config
Body:
{
  "apn": "internet",
  "username": "",
  "password": "",
  "enabled": true
}

Device Management

POST /api/device/restart
Response:
{
  "success": true,
  "message": "Device restarting in 2 seconds..."
}

POST /api/device/factory-reset
Response:
{
  "success": true,
  "message": "Factory reset complete. Device restarting..."
}

GET /api/device/info
Response:
{
  "device_id": "ACV-AABBCCDDEEFF",
  "firmware_version": "1.0.0",
  "hardware": "TTGO T-Call v1.4",
  "mac_address": "AA:BB:CC:DD:EE:FF",
  "chip_model": "ESP32-WROVER-B",
  "flash_size": 8388608,
  "psram_size": 4194304,
  "sdk_version": "v4.4.x"
}

Live Telemetry (for status page)

GET /api/telemetry/latest
Response:
{
  ... same as MQTT telemetry JSON from Phase 2 ...
}

3.4 Web UI Design

Layout

Single-page application with a top navigation bar and tabbed content area. Designed to be mobile-first (phone-friendly).

Pages / Tabs

┌──────────────────────────────────────┐
│  Acuvim Monitor    [Status dot]      │
├──────────────────────────────────────┤
│ Status │ WiFi │ MQTT │ Sleep │ Device│
├──────────────────────────────────────┤
│                                      │
│     (Active tab content here)        │
│                                      │
│                                      │
│                                      │
│                                      │
└──────────────────────────────────────┘

Tab: Status

Displays real-time device status. Auto-refreshes every 5 seconds.

┌──────────────────────────────────────┐
│ Device: ACV-AABBCCDDEEFF             │
│ Firmware: v1.0.0                     │
│ Uptime: 2h 15m 30s                   │
├──────────────────────────────────────┤
│ Connections                          │
│ ● WiFi: Connected (-45 dBm)         │
│ ● MQTT: Connected                   │
│ ○ GSM: Disabled                     │
│ ● Modbus: OK (1234 reads, 5 errors) │
├──────────────────────────────────────┤
│ Latest Readings                      │
│ Va: 230.1V  Vb: 231.4V  Vc: 229.8V │
│ Ia: 15.2A   Ib: 14.8A   Ic: 15.5A  │
│ P: 10.5kW   Q: 2.1kVAR  S: 10.7kVA │
│ PF: 0.98    F: 50.01Hz              │
│ Import: 12345.6 kWh                  │
└──────────────────────────────────────┘

Tab: WiFi

┌──────────────────────────────────────┐
│ WiFi Configuration                   │
│                                      │
│ [✓] WiFi Enabled                     │
│                                      │
│ Current: MyNetwork (-45 dBm)         │
│                                      │
│ Available Networks:  [🔄 Scan]       │
│ ┌──────────────────────────────────┐ │
│ │ ● Network1     -40 dBm  WPA2    │ │
│ │ ● Network2     -65 dBm  WPA2    │ │
│ │ ● OpenNet      -70 dBm  OPEN    │ │
│ └──────────────────────────────────┘ │
│                                      │
│ SSID: [________________]            │
│ Password: [________________]        │
│                                      │
│         [Save & Connect]             │
└──────────────────────────────────────┘

Tab: MQTT

┌──────────────────────────────────────┐
│ MQTT Configuration                   │
│                                      │
│ Status: ● Connected                  │
│                                      │
│ Broker: [broker.example.com____]     │
│ Port:   [1883___]                    │
│ Username: [device1__________]        │
│ Password: [****************]         │
│ Topic Prefix: [acuvim_______]        │
│ [ ] Use TLS                          │
│                                      │
│   [Test Connection]  [Save]          │
│                                      │
│ Console URL:                         │
│ [https://console.example.com__]      │
│                [Save]                │
└──────────────────────────────────────┘

Tab: Sleep

┌──────────────────────────────────────┐
│ Sleep Configuration                  │
│                                      │
│ [  ] Enable Deep Sleep              │
│                                      │
│ Sleep Duration: [15] minutes         │
│ Wake Duration:  [300] seconds        │
│                                      │
│ When enabled, the device will:       │
│ - Wake up for [300]s                 │
│ - Read & transmit Acuvim data        │
│ - Sleep for [15] minutes             │
│ - Repeat                             │
│                                      │
│              [Save]                  │
├──────────────────────────────────────┤
│ Polling                              │
│ Poll Interval: [5] seconds           │
│ Heartbeat Interval: [60] seconds     │
│              [Save]                  │
└──────────────────────────────────────┘

Tab: Device

┌──────────────────────────────────────┐
│ Device Management                    │
│                                      │
│ Device ID:  ACV-AABBCCDDEEFF         │
│ Firmware:   v1.0.0                   │
│ Hardware:   TTGO T-Call v1.4         │
│ MAC:        AA:BB:CC:DD:EE:FF        │
│ Free Heap:  120,000 bytes            │
│ Flash:      8 MB                     │
│ PSRAM:      4 MB                     │
│                                      │
│ Modbus Configuration                 │
│ Slave Address: [1__]                 │
│ Baud Rate:     [9600 ▼]             │
│ (1200/2400/4800/9600/19200)          │
│              [Save]                  │
│                                      │
│ ┌────────────────────────────────┐   │
│ │      [Restart Device]         │   │
│ └────────────────────────────────┘   │
│ ┌────────────────────────────────┐   │
│ │    [Factory Reset] ⚠          │   │
│ └────────────────────────────────┘   │
│ (Factory reset clears all settings)  │
└──────────────────────────────────────┘

3.5 Web UI Technical Implementation

Size Budget

Total LittleFS budget for web files: ~200KB (leaving room for other data)

  • index.html: ~8KB (gzipped inline CSS/JS for single-file delivery)
  • Or split: HTML ~3KB, CSS ~3KB, JS ~15KB
  • Use gzip compression in ESPAsyncWebServer for efficient delivery

Technology Choices

  • No framework — vanilla HTML/CSS/JS to minimize size
  • CSS: Minimal responsive grid, CSS custom properties for theming
  • JS: Fetch API for REST calls, DOM manipulation, no build step
  • Icons: Unicode symbols or minimal SVG (no icon font libraries)

Security

  • API endpoints check Origin header to prevent CSRF from external sites
  • Factory reset and restart require confirmation (JS confirm() dialog)
  • Passwords are never returned in plaintext from GET endpoints (masked with ********)
  • Optional: basic auth on the AP web interface (disabled by default for initial setup ease)

CORS

  • Allow requests from 192.168.4.1 and the STA IP address
  • No external origins allowed

3.6 Web Server Implementation

web_server.h / web_server.cpp

class WebServer {
public:
    void begin(ConfigManager& config, WifiManager& wifi,
               MqttClient& mqtt, AcuvimReader& acuvim);
    void stop();

private:
    AsyncWebServer server;
    DNSServer dnsServer;

    void setupRoutes();
    void setupCaptivePortal();

    // Route handlers
    void handleStatus(AsyncWebServerRequest* request);
    void handleWifiScan(AsyncWebServerRequest* request);
    void handleWifiConfig(AsyncWebServerRequest* request);
    void handleMqttConfig(AsyncWebServerRequest* request);
    void handleSleepConfig(AsyncWebServerRequest* request);
    void handleConsoleConfig(AsyncWebServerRequest* request);
    void handleModbusConfig(AsyncWebServerRequest* request);
    void handleGsmConfig(AsyncWebServerRequest* request);
    void handleDeviceRestart(AsyncWebServerRequest* request);
    void handleFactoryReset(AsyncWebServerRequest* request);
    void handleDeviceInfo(AsyncWebServerRequest* request);
    void handleLatestTelemetry(AsyncWebServerRequest* request);

    // References to other modules
    ConfigManager* configMgr;
    WifiManager* wifiMgr;
    MqttClient* mqttClient;
    AcuvimReader* acuvimReader;
};

3.7 Testing & Validation

Test Method Pass Criteria
AP mode starts Boot without WiFi config See Acuvim-XXXXXX network
Captive portal Connect phone to AP Auto-opens config page
WiFi scan Click Scan on WiFi tab Lists available networks
WiFi connect Enter SSID/password, save Connects to WiFi, shows IP
STA+AP mode Connect WiFi via portal Both AP and STA active
MQTT config Enter broker details, save MQTT connects, persists on reboot
MQTT test Click Test Connection Reports success or failure with reason
Sleep config Set sleep/wake times Settings saved, displayed on reload
Console URL Enter URL, save Persists across reboot
Modbus config Change slave addr/baud Modbus reinitializes with new settings
Restart Click Restart Device reboots, reconnects
Factory reset Click Factory Reset All settings cleared, AP mode
Mobile responsive Open on phone All tabs usable, no horizontal scroll
Multi-client Open on 2 devices Both can access simultaneously
Status refresh Open status tab Auto-updates every 5 seconds

3.8 Phase 3 Completion Criteria

  • AP mode creates captive portal with auto-redirect
  • Web UI loads on mobile and desktop browsers
  • WiFi scan returns available networks
  • WiFi credentials can be configured and saved
  • MQTT settings configurable with test connection feature
  • Sleep mode settings configurable
  • Console URL configurable
  • Modbus settings configurable
  • Device restart works from web UI
  • Factory reset clears all settings
  • Status page shows real-time device and telemetry data
  • All settings persist across reboot
  • STA+AP concurrent mode works (config access while WiFi connected)

Previous Phase: Phase 2 — WiFi, MQTT & NVS Next Phase: Phase 4 — GSM Integration & Transport Failover