# 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`: ```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` ```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](acuvim-spec-02.md) **Next Phase:** [Phase 4 — GSM Integration & Transport Failover](acuvim-spec-04.md)