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>
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
- ESP32 Access Point with captive portal redirect
- Responsive web UI (HTML/CSS/JS) stored in LittleFS
- REST API endpoints for configuration CRUD
- WiFi network scanner (list available SSIDs)
- Live status page showing connection and telemetry state
- 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
- 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.
- STA+AP mode: When WiFi is connected, AP remains active for local configuration access. This allows reconfiguration without losing the MQTT connection.
- 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
Originheader 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.1and 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