# Phase 4: GSM Integration & Transport Failover ## Objective Integrate the SIM800L (and later SIM7600) GSM module using TinyGSM, implement a transport abstraction layer that provides seamless failover between WiFi and GSM, and enable MQTT communication over cellular when WiFi is unavailable. ## Prerequisites - Phase 3 complete (WiFi, MQTT, Web UI working) - Active SIM card with data plan inserted in the TTGO T-Call nano SIM slot - APN details from the mobile carrier - MQTT broker accessible from the public internet (for GSM connectivity) ## Deliverables 1. GSM modem driver using TinyGSM (SIM800L, forward-compatible with SIM7600) 2. Transport abstraction layer (WiFi / GSM unified interface) 3. Automatic failover: WiFi -> GSM with configurable priority 4. MQTT over GSM using TinyGSM's TCP client 5. GSM signal quality monitoring 6. GSM configuration via web UI and MQTT commands --- ## 4.1 TinyGSM Integration ### Library Addition Add to `platformio.ini`: ```ini lib_deps = ...existing... vshymanskyy/TinyGSM@^0.11.7 build_flags = ...existing... -DTINY_GSM_MODEM_SIM800 ; Change to TINY_GSM_MODEM_SIM7600 for SIM7600 -DTINY_GSM_RX_BUFFER=1024 ``` ### Modem Pin Mapping (from Phase 1) ``` MODEM_RST = GPIO 5 # Reset (active LOW) MODEM_PWRKEY = GPIO 4 # Power key toggle MODEM_POWER_ON = GPIO 23 # Power enable (active HIGH) MODEM_TX = GPIO 27 # ESP32 TX -> SIM800L RX MODEM_RX = GPIO 26 # ESP32 RX <- SIM800L TX ``` ### `gsm_manager.h` / `gsm_manager.cpp` ```cpp class GsmManager { public: bool begin(); // Power on modem, initialize bool connect(const char* apn, // Connect to GPRS const char* user = "", const char* pass = ""); void disconnect(); // Disconnect GPRS bool isConnected(); // GPRS connection status void powerOff(); // Power down modem void restart(); // Reset modem int getSignalQuality(); // CSQ value (0-31, 99=unknown) int getSignalPercent(); // 0-100% mapped from CSQ String getOperator(); // Network operator name String getIMEI(); // Modem IMEI String getICCID(); // SIM card ICCID bool isSimInserted(); // SIM detection TinyGsmClient& getClient(); // TCP client for MQTT private: TinyGsm modem; TinyGsmClient gsmClient; bool modemReady; void powerOnModem(); void powerOffModem(); bool waitForNetwork(uint32_t timeoutMs = 60000); }; ``` ### Modem Power Sequence The SIM800L on the TTGO T-Call v1.4 requires a specific power-on sequence: ```cpp void GsmManager::powerOnModem() { // Enable modem power pinMode(MODEM_POWER_ON, OUTPUT); digitalWrite(MODEM_POWER_ON, HIGH); // Pull reset HIGH (not resetting) pinMode(MODEM_RST, OUTPUT); digitalWrite(MODEM_RST, HIGH); // Toggle PWRKEY to start modem pinMode(MODEM_PWRKEY, OUTPUT); digitalWrite(MODEM_PWRKEY, HIGH); delay(100); digitalWrite(MODEM_PWRKEY, LOW); delay(1000); digitalWrite(MODEM_PWRKEY, HIGH); // Wait for modem to boot (~3-5 seconds) delay(3000); } ``` ### SIM7600 Migration Notes When migrating from SIM800L to SIM7600: 1. Change build flag: `-DTINY_GSM_MODEM_SIM7600` 2. Update pin mapping (SIM7600 boards may use different GPIOs) 3. Power sequence differs for SIM7600 (check specific board schematic) 4. TinyGSM API remains identical — no application code changes needed 5. SIM7600 supports 4G/LTE vs SIM800L's 2G — faster data, better coverage 6. Consider: SIM7600 draws more peak current (~2A vs ~1A) — ensure power supply ## 4.2 Transport Abstraction Layer ### `transport_manager.h` / `transport_manager.cpp` Provides a unified interface for network connectivity, abstracting whether WiFi or GSM is the active transport. All higher-level modules (MQTT, HTTP, OTA) use this instead of directly accessing WiFi or GSM. ```cpp enum class TransportType { NONE, WIFI, GSM }; enum class TransportPriority { WIFI_PREFERRED, // Try WiFi first, fall back to GSM GSM_PREFERRED, // Try GSM first, fall back to WiFi WIFI_ONLY, // Only use WiFi GSM_ONLY // Only use GSM }; class TransportManager { public: void begin(WifiManager& wifi, GsmManager& gsm, ConfigManager& config); void loop(); // Check connections, handle failover bool isConnected(); // Any transport connected TransportType getActiveTransport(); // Which transport is in use Client& getClient(); // Returns WiFiClient or TinyGsmClient void setPriority(TransportPriority prio); TransportPriority getPriority(); int getSignalStrength(); // RSSI (WiFi) or CSQ% (GSM) String getConnectionInfo(); // Human-readable status void onTransportChange(std::function callback); private: WifiManager* wifi; GsmManager* gsm; ConfigManager* config; TransportType activeTransport; TransportPriority priority; void evaluateTransport(); // Decision logic void switchTransport(TransportType newType); unsigned long lastEvaluation; static const unsigned long EVAL_INTERVAL = 10000; // 10 seconds }; ``` ### Failover Logic ``` evaluateTransport() runs every 10 seconds: IF priority == WIFI_ONLY: Use WiFi. If WiFi down, transport = NONE. ELSE IF priority == GSM_ONLY: Use GSM. If GSM down, transport = NONE. ELSE IF priority == WIFI_PREFERRED: IF WiFi connected: Use WiFi (even if GSM also connected) ELSE IF GSM connected: Use GSM ELSE: Try WiFi first (15s timeout) If WiFi fails, try GSM If both fail, transport = NONE ELSE IF priority == GSM_PREFERRED: IF GSM connected: Use GSM ELSE IF WiFi connected: Use WiFi ELSE: Try GSM first (30s timeout) If GSM fails, try WiFi If both fail, transport = NONE ON transport change: - Fire onTransportChange callback - MQTT client reconnects using new Client - Log transport switch event ``` ### Transport Switch and MQTT Reconnect When transport changes, the MQTT client must be updated: ```cpp void TransportManager::switchTransport(TransportType newType) { if (newType == activeTransport) return; TransportType oldType = activeTransport; activeTransport = newType; // Notify listeners (MQTT client will reconnect with new Client) if (onChangeCallback) { onChangeCallback(newType); } Serial.printf("Transport: %s -> %s\n", transportName(oldType), transportName(newType)); } ``` ## 4.3 MQTT Client Update The `MqttClient` class from Phase 2 is updated to accept a `Client&` from the transport manager instead of a hardcoded `WiFiClient`: ```cpp // Phase 2 (WiFi only): WiFiClient wifiClient; mqtt.begin(wifiClient); // Phase 4 (transport abstracted): mqtt.begin(transport.getClient()); // On transport change: transport.onTransportChange([&](TransportType type) { mqtt.disconnect(); mqtt.begin(transport.getClient()); mqtt.connect(); }); ``` The MQTT client does not need to know whether it's using WiFi or GSM — `PubSubClient` works with any `Client` implementation. ## 4.4 Telemetry Update Add transport info to telemetry payload: ```json { "ts": 1716000000, "dev": "ACV-AABBCCDDEEFF", "conn": "gsm", "signal": 65, "operator": "Vodacom", ...acuvim data... } ``` - `conn`: `"wifi"` or `"gsm"` - `signal`: WiFi RSSI (dBm, negative) or GSM signal percent (0-100) - `operator`: GSM network name (only when on GSM) ## 4.5 GSM Data Efficiency GSM data costs money. Minimize data usage: - **Compact JSON keys** (already defined in Phase 2) - **Configurable poll interval for GSM**: Option to use a longer interval when on GSM (e.g., 30s vs 5s on WiFi) - **Batch telemetry**: Optionally accumulate N readings and send as array (reduces MQTT overhead) - **MQTT QoS 0 on GSM**: Reduce overhead (QoS 1 on WiFi where bandwidth is free) - **No retain on GSM**: Skip retain flag to reduce broker storage messages Add to `DeviceConfig`: ```cpp uint16_t gsm_poll_interval_sec; // Polling interval when on GSM (default: 30) bool gsm_batch_enabled; // Batch readings over GSM uint8_t gsm_batch_size; // Number of readings per batch (default: 6) ``` ## 4.6 Web UI Updates ### GSM Tab (added to captive portal) ``` ┌──────────────────────────────────────┐ │ GSM / Cellular Configuration │ │ │ │ [✓] GSM Enabled │ │ │ │ Status: ● Connected (Vodacom) │ │ Signal: ████████░░ 65% │ │ IMEI: 123456789012345 │ │ ICCID: 8927xxxxxxxxxxxxxx │ │ │ │ APN: [internet_________] │ │ Username: [________________] │ │ Password: [________________] │ │ │ │ Transport Priority: │ │ (●) WiFi preferred (GSM fallback) │ │ ( ) GSM preferred (WiFi fallback) │ │ ( ) WiFi only │ │ ( ) GSM only │ │ │ │ GSM Data Saving: │ │ Poll interval: [30] seconds │ │ [✓] Batch readings (6 per batch) │ │ │ │ [Save] │ └──────────────────────────────────────┘ ``` ### API Endpoints (additions) ``` GET /api/gsm/status Response: { "enabled": true, "connected": true, "signal_quality": 18, "signal_percent": 65, "operator": "Vodacom", "imei": "123456789012345", "iccid": "8927xxxxxxxxxxxxxx", "sim_inserted": true } POST /api/transport/priority Body: { "priority": "wifi_preferred" } ``` ## 4.7 Devices Without GSM Some devices will not have a GSM module. The firmware must handle this gracefully: - On boot, attempt to communicate with the modem via AT commands - If no response after 3 attempts: set `gsm_available = false` - Hide GSM-related UI elements when GSM is not available - Transport priority automatically becomes `WIFI_ONLY` - All GSM code paths are skipped (no error logs for expected absence) - This detection is automatic — no configuration needed ```cpp bool GsmManager::begin() { // Try AT command 3 times for (int i = 0; i < 3; i++) { if (modem.testAT(1000)) { modemReady = true; return true; } } modemReady = false; // No modem present return false; } ``` ## 4.8 Testing & Validation | Test | Method | Pass Criteria | |------|--------|---------------| | GSM connects | Insert SIM, configure APN | GPRS connected, has IP | | MQTT over GSM | Disable WiFi, monitor MQTT | Telemetry received via GSM | | WiFi->GSM failover | Disconnect WiFi router | Switches to GSM within 30s | | GSM->WiFi fallback | Re-enable WiFi | Switches back to WiFi | | No SIM card | Remove SIM | Graceful failure, WiFi-only | | No GSM module | Run on ESP32 without SIM800L | Auto-detect, WiFi-only mode | | Signal strength | Monitor GSM tab | CSQ value updates correctly | | GSM data saving | Monitor payload sizes | Compact JSON, correct batch size | | Transport priority | Change via web UI | Correct transport selection | | MQTT reconnect on switch | Force transport change | MQTT reconnects on new transport | | Continuous GSM | Run on GSM for 24h | Stable, no memory leaks | | GSM power cycle | Power cycle modem | Recovers and reconnects | ## 4.9 Phase 4 Completion Criteria - [ ] SIM800L modem initialized with correct power sequence - [ ] GPRS connection established with configurable APN - [ ] MQTT works over GSM (publish telemetry, subscribe to commands) - [ ] Transport failover: WiFi -> GSM automatic switch - [ ] Transport fallback: GSM -> WiFi when WiFi becomes available - [ ] Transport priority configurable (WiFi preferred, GSM preferred, etc.) - [ ] Signal quality reported in telemetry and web UI - [ ] Devices without GSM module detected and handled gracefully - [ ] GSM data-saving options working (longer interval, batching) - [ ] Web UI updated with GSM configuration tab --- **Previous Phase:** [Phase 3 — Captive Portal Web Interface](acuvim-spec-03.md) **Next Phase:** [Phase 5 — SD Card Offline Buffering](acuvim-spec-05.md)