# DD3-LoRa-Bridge-MultiSender Firmware for LilyGO T3 v1.6.1 (`ESP32 + SX1276 + SSD1306`) that runs as either: - `Sender` (PIN `GPIO14` HIGH): reads multiple IEC 62056-21 meters, batches data, sends over LoRa. - `Receiver` (PIN `GPIO14` LOW): receives/ACKs batches, publishes MQTT, serves web UI, logs to SD. ## Current Architecture - Single codebase, role selected at boot via `detect_role()` (`include/config.h`, `src/config.cpp`). - LoRa link uses explicit CRC16 frame protection in firmware (`src/lora_transport.cpp`), in addition to LoRa PHY CRC. - Sender batches up to `30` samples and retransmits on missing ACK (`BATCH_MAX_RETRIES=2`, policy `Keep`). - Receiver handles AP fallback when STA config is missing/invalid and exposes a config/status web UI. ## LoRa Frame Protocol (Current) Frame format on-air: `[msg_kind:1][device_short_id:2][payload...][crc16:2]` `msg_kind`: - `0` = `BatchUp` - `1` = `AckDown` ### `BatchUp` `BatchUp` is chunked in transport (`batch_id`, `chunk_index`, `chunk_count`, `total_len`) and then decoded via `payload_codec`. Payload header contains: - fixed magic/schema fields (`kMagic=0xDDB3`, `kSchema=2`) - `schema_id` - sender/batch/time/error metadata Supported payload schemas in this branch: - `schema_id=1` (`EnergyMulti`): integer kWh for up to 3 meters (`energy1_kwh`, `energy2_kwh`, `energy3_kwh`) - `schema_id=0` (legacy): older energy/power delta encoding path remains decode-compatible `n == 0` is used as sync request (no meter samples). ### `AckDown` (7 bytes) `[flags:1][batch_id_be:2][epoch_utc_be:4]` - `flags bit0`: `time_valid` - Receiver sends ACK repeatedly (`ACK_REPEAT_COUNT=3`, `ACK_REPEAT_DELAY_MS=200`). - Sender accepts time only if `time_valid=1` and `epoch >= MIN_ACCEPTED_EPOCH_UTC` (`2026-02-01 00:00:00 UTC`). ## Time Bootstrap Guardrail On sender boot: - `g_time_acquired=false` - only sync requests every `SYNC_REQUEST_INTERVAL_MS` (15s) - no normal sampling/transmit until valid ACK time received This prevents publishing/storing pre-threshold timestamps. ## Multi-Meter Sender Behavior Implemented in `src/meter_driver.cpp` + sender path in `src/main.cpp`: - Meter protocol: IEC 62056-21 ASCII, Mode D style framing (`/ ... !`) - UART settings: `9600 7E1` - Parsed OBIS: `1-0:1.8.0` - Conversion: floor to integer kWh (`floorf`) Meter count is build-dependent (`include/config.h`): - Debug builds (`SERIAL_DEBUG_MODE=1`): `METER_COUNT=2` - Prod builds (`SERIAL_DEBUG_MODE=0`): `METER_COUNT=3` Default RX pins: - Meter1: `GPIO34` (`Serial2`) - Meter2: `GPIO25` (`Serial1`) - Meter3: `GPIO3` (`Serial`, prod only because debug serial is disabled) ## Receiver Behavior For valid `BatchUp` decode: 1. Reassemble chunks and decode payload. 2. Send `AckDown` immediately. 3. Drop duplicate batches per sender (`batch_id` tracking). 4. If `n==0`: treat as sync request only. 5. Else convert to `MeterData`, log to SD, update web UI, publish MQTT. ## MQTT Topics and Payloads State topic: - `smartmeter//state` Fault topic (retained): - `smartmeter//faults` For `EnergyMulti` samples, state JSON includes: - `id`, `ts` - `energy1_kwh`, `energy2_kwh`, optional `energy3_kwh` - `bat_v`, `bat_pct` - optional link fields: `rssi`, `snr` - fault/reject fields: `err_last`, `rx_reject`, `rx_reject_text` (+ non-zero counters) Home Assistant discovery publishing is enabled (`ENABLE_HA_DISCOVERY=true`) but still advertises legacy keys (`e_kwh`, `p_w`, `p1_w`, `p2_w`, `p3_w`) in `src/mqtt_client.cpp`. ## Web UI, Wi-Fi, Storage - STA config is stored in Preferences (`wifi_manager`). - If STA/MQTT config is unavailable, receiver starts AP mode with SSID prefix `DD3-Bridge-`. - Web auth defaults are `admin/admin` (`WEB_AUTH_DEFAULT_USER/PASS`). - SD logging is enabled (`ENABLE_SD_LOGGING=true`). ## Build Environments From `platformio.ini`: - `lilygo-t3-v1-6-1` - `lilygo-t3-v1-6-1-test` - `lilygo-t3-v1-6-1-868` - `lilygo-t3-v1-6-1-868-test` - `lilygo-t3-v1-6-1-payload-test` - `lilygo-t3-v1-6-1-868-payload-test` - `lilygo-t3-v1-6-1-prod` - `lilygo-t3-v1-6-1-868-prod` Example: ```bash ~/.platformio/penv/bin/pio run -e lilygo-t3-v1-6-1 ``` ## Test Mode `ENABLE_TEST_MODE` replaces normal sender/receiver loops with dedicated test loops (`src/test_mode.cpp`). It sends/receives plain JSON test frames and publishes to `smartmeter//test`.