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>
407 lines
13 KiB
Markdown
407 lines
13 KiB
Markdown
# 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<void(TransportType)> 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)
|