# DD3-LoRa-Bridge-MultiSender Firmware for LilyGO T3 v1.6.1 (`ESP32 + SX1276 + SSD1306`) that runs as either: - `Sender` (PIN `GPIO14` HIGH): reads one IEC 62056-21 meter, batches samples, 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`). - Sender batches up to `30` samples and retries on missing ACK (`BATCH_MAX_RETRIES=2`, retry policy `Keep`). - Sender meter parsing is decoupled from LoRa ACK waits using a dedicated FreeRTOS reader task + queue (`src/main.cpp`). - Receiver uses STA mode when config is valid, otherwise AP fallback with web config. - No debug auto-reboot timer is active in normal firmware loops. ## LoRa Frame Protocol (Current) On-air frame format: `[msg_kind:1][device_short_id:2][payload...][crc16:2]` `msg_kind`: - `0` = `BatchUp` - `1` = `AckDown` ### `BatchUp` Transport is chunked (`batch_id`, `chunk_index`, `chunk_count`, `total_len`) and reassembled before payload decode. Payload codec (`src/payload_codec.cpp`) currently uses: - `kMagic=0xDDB3` - `kSchema=2` - metadata: sender, batch, timestamp, interval, battery, fault counters - data arrays: `energy_wh[]`, `p1_w[]`, `p2_w[]`, `p3_w[]` `n == 0` is valid and used for sync request packets. ### `AckDown` (7 bytes) `[flags:1][batch_id_be:2][epoch_utc_be:4]` - `flags bit0`: `time_valid` - Receiver repeats ACK (`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` - no normal sampling/transmit yet - sync request every `SYNC_REQUEST_INTERVAL_MS` (15s) Only after valid ACK time is received: - system time is set - normal 1 Hz sampling and periodic LoRa batch transmit start This blocks pre-threshold timestamps from MQTT/SD paths. Timezone handling: - Local time rendering uses `TIMEZONE_TZ` from `include/config.h`. - Default value is `CET-1CEST,M3.5.0/2,M10.5.0/3` and can be changed at compile time. ## Sender Meter Path Implemented in `src/meter_driver.cpp` + sender loop in `src/main.cpp`: - UART: `Serial2`, RX pin `GPIO34` (`PIN_METER_RX`), `9600 7E1` - ESP32 RX buffer is enlarged to `8192` bytes to survive long LoRa blocking sections. - Frame detection: starts at `'/'`, ends at `'!'`, timeout protection included (`METER_FRAME_TIMEOUT_MS=20000`). - Parsing runs in a dedicated sender task and is handed to the main sender loop via queue. - Parsed OBIS values: - `1-0:1.8.0` (total energy) - `1-0:16.7.0` (total power) - `1-0:36.7.0`, `56.7.0`, `76.7.0` (phase powers) - `1-0:1.8.0*Wh` is automatically scaled to kWh Sender samples every second and transmits batches every 30 seconds. ## 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 samples, log to SD, update web UI, publish MQTT. ## MQTT Topics and Payloads State topic: - `smartmeter//state` Fault topic (retained): - `smartmeter//faults` State JSON fields (`src/json_codec.cpp`): - `id`, `ts`, `e_kwh` - `p_w`, `p1_w`, `p2_w`, `p3_w` - `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 is enabled (`ENABLE_HA_DISCOVERY=true`) and publishes config topics under: - `homeassistant/sensor///config` ## Web UI, Wi-Fi, Storage - Wi-Fi/MQTT/NTP/web-auth config persists in Preferences (`wifi_manager`). - AP fallback SSID prefix: `DD3-Bridge-`. - Default web credentials: `admin/admin`. - SD logging 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 JSON test frames and publishes to `smartmeter//test`.