Refresh README and add firmware requirements for Rust port
This commit is contained in:
172
README.md
172
README.md
@@ -1,104 +1,100 @@
|
||||
# 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.
|
||||
Firmware for LilyGO T3 v1.6.1 (`ESP32 + SX1276 + SSD1306`) that runs in two roles:
|
||||
- `Sender` (`GPIO14` HIGH): reads one IEC 62056-21 meter, builds 30-slot sparse batches, sends via LoRa.
|
||||
- `Receiver` (`GPIO14` LOW): receives/ACKs batches, publishes MQTT, serves web UI, logs to SD.
|
||||
|
||||
## Current Architecture
|
||||
## Architecture Summary
|
||||
|
||||
- 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`).
|
||||
- Batch payload codec is schema v3 and uses a sparse `present_mask` over a 30-second window (no schema v2 compatibility).
|
||||
- Sender derives epoch timestamps from meter Sekundenindex (`0-0:96.8.0*255`) using an epoch anchor when time is synced.
|
||||
- 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.
|
||||
- Single codebase, role selected at boot by `detect_role()` (`src/config.cpp`).
|
||||
- LoRa transport is wrapped with firmware-level CRC16-CCITT (`src/lora_transport.cpp`).
|
||||
- Sender meter ingest is decoupled from LoRa waits via FreeRTOS meter reader task + queue on ESP32 (`src/main.cpp`).
|
||||
- Batch payload codec is schema `v3` with a 30-bit `present_mask` over `[t_last-29, t_last]` (`src/payload_codec.cpp`).
|
||||
- Sender only starts normal metering/transmit flow after valid time bootstrap from receiver ACK.
|
||||
- Receiver runs STA mode if stored config is valid and connects, otherwise AP fallback.
|
||||
|
||||
## LoRa Frame Protocol (Current)
|
||||
## LoRa Protocol
|
||||
|
||||
On-air frame format:
|
||||
On-air frame:
|
||||
|
||||
`[msg_kind:1][device_short_id:2][payload...][crc16:2]`
|
||||
|
||||
`msg_kind`:
|
||||
- `0` = `BatchUp`
|
||||
- `1` = `AckDown`
|
||||
- `0`: `BatchUp`
|
||||
- `1`: `AckDown`
|
||||
|
||||
### `BatchUp`
|
||||
### BatchUp
|
||||
|
||||
Transport is chunked (`batch_id`, `chunk_index`, `chunk_count`, `total_len`) and reassembled before payload decode.
|
||||
Transport layer chunks payload into:
|
||||
|
||||
Payload codec (`src/payload_codec.cpp`) currently uses:
|
||||
- `kMagic=0xDDB3`
|
||||
- `kSchema=3`
|
||||
- metadata: sender, batch, `t_last`, `present_mask`, battery, fault counters
|
||||
- data arrays: `energy_wh[]`, `p1_w[]`, `p2_w[]`, `p3_w[]`
|
||||
`[batch_id_le:2][chunk_index:1][chunk_count:1][total_len_le:2][chunk_payload...]`
|
||||
|
||||
`present_mask` is a 30-bit second map in the `[t_last-29, t_last]` window.
|
||||
Only set bits carry samples, so missing seconds are explicitly represented.
|
||||
Receiver reassembles all chunks before decode.
|
||||
|
||||
`n == 0` is valid for sync request packets (`present_mask == 0`).
|
||||
Payload codec (`schema=3`, magic `0xDDB3`) carries:
|
||||
- metadata: sender ID, batch ID, `t_last`, `present_mask`, battery mV, error counters
|
||||
- arrays per present sample: `energy_wh[]`, `p1_w[]`, `p2_w[]`, `p3_w[]`
|
||||
|
||||
### `AckDown` (7 bytes)
|
||||
`n == 0` with `present_mask == 0` is valid and used for sync request packets.
|
||||
|
||||
### AckDown (7 bytes payload)
|
||||
|
||||
`[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`).
|
||||
- ACK is repeated (`ACK_REPEAT_COUNT=3`, `ACK_REPEAT_DELAY_MS=200`)
|
||||
- Sender sets local time only if `time_valid=1` and `epoch >= MIN_ACCEPTED_EPOCH_UTC` (`2026-02-01 00:00:00 UTC`)
|
||||
|
||||
## Time Bootstrap Guardrail
|
||||
## Time Bootstrap and Timezone
|
||||
|
||||
On sender boot:
|
||||
Sender boot starts in sync-only mode:
|
||||
- `g_time_acquired=false`
|
||||
- no normal sampling/transmit yet
|
||||
- sync request every `SYNC_REQUEST_INTERVAL_MS` (15s)
|
||||
- sends sync requests every `SYNC_REQUEST_INTERVAL_MS` (`15s`)
|
||||
- does not run normal 1 Hz sample/batch flow yet
|
||||
|
||||
Only after valid ACK time is received:
|
||||
- system time is set
|
||||
- normal 1 Hz sampling and periodic LoRa batch transmit start
|
||||
After valid ACK time:
|
||||
- `time_set_utc()` is called
|
||||
- `g_time_acquired=true`
|
||||
- normal 1 Hz sampling + periodic batch transmission starts
|
||||
|
||||
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.
|
||||
Timezone:
|
||||
- `TIMEZONE_TZ` from `include/config.h` is applied in `time_manager`.
|
||||
- Web/OLED local-time rendering uses this timezone.
|
||||
- Default: `CET-1CEST,M3.5.0/2,M10.5.0/3`.
|
||||
|
||||
## Sender Meter Path
|
||||
|
||||
Implemented in `src/meter_driver.cpp` + sender loop in `src/main.cpp`:
|
||||
Implemented by `src/meter_driver.cpp` and sender loop in `src/main.cpp`:
|
||||
- UART: `Serial2`, `GPIO34`, `9600 7E1`
|
||||
- ESP32 RX buffer enlarged to `8192`
|
||||
- Frame detection `/ ... !`, timeout `METER_FRAME_TIMEOUT_MS=3000`
|
||||
- Parsed OBIS fields:
|
||||
- `0-0:96.8.0*255` meter Sekundenindex (hex u32)
|
||||
- `1-0:1.8.0` total energy (auto scales Wh -> kWh when unit is Wh)
|
||||
- `1-0:16.7.0` total active power
|
||||
- `1-0:36.7.0`, `56.7.0`, `76.7.0` phase powers
|
||||
|
||||
- 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=3000`).
|
||||
- Parsing runs in a dedicated sender task and is handed to the main sender loop via queue.
|
||||
- Parsed OBIS values:
|
||||
- `0-0:96.8.0*255` (meter Sekundenindex, 4-byte hex)
|
||||
- `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
|
||||
Timestamp derivation:
|
||||
- anchor offset: `epoch_offset = epoch_now - meter_seconds`
|
||||
- sample epoch: `ts_utc = meter_seconds + epoch_offset`
|
||||
- jump checks: rollback, wall-time delta mismatch, anchor drift
|
||||
|
||||
Timestamping/validation on sender:
|
||||
- Anchor when time is valid: `epoch_offset = epoch_now - meter_seconds`.
|
||||
- Derived sample time: `ts_utc = meter_seconds + epoch_offset`.
|
||||
- Meter-time checks: monotonicity and `delta_meter_seconds` vs elapsed wall time (with tolerance) plus anchor drift checks.
|
||||
- On detected jump/drift, sender records meter fault and resulting timestamp discontinuities propagate through `present_mask` to receiver.
|
||||
|
||||
Sender samples every second and transmits batches every 30 seconds.
|
||||
Sender builds sparse 30-slot windows and sends every `METER_SEND_INTERVAL_MS` (`30s`).
|
||||
|
||||
## Receiver Behavior
|
||||
|
||||
For valid `BatchUp` decode:
|
||||
1. Reassemble chunks and decode payload.
|
||||
For decoded `BatchUp`:
|
||||
1. Reassemble and decode.
|
||||
2. Send `AckDown` immediately.
|
||||
3. Drop duplicate batches per sender (`batch_id` tracking).
|
||||
4. Track duplicate stats per sender: absolute duplicates, total received, duplicate percentage, last duplicate timestamp.
|
||||
5. If `n==0`: treat as sync request only.
|
||||
6. Else reconstruct timestamps from `t_last` + `present_mask`, preserving skipped seconds, then log to SD, update web UI, publish MQTT.
|
||||
3. Track duplicates per configured sender (`EXPECTED_SENDER_IDS`).
|
||||
4. If duplicate: update duplicate counters/time, skip data write/publish.
|
||||
5. If `n==0`: sync request path only.
|
||||
6. Else reconstruct each sample timestamp from `t_last + present_mask`, then:
|
||||
- append to SD CSV
|
||||
- publish MQTT state
|
||||
- update web status and last batch table
|
||||
|
||||
## MQTT Topics and Payloads
|
||||
## MQTT
|
||||
|
||||
State topic:
|
||||
- `smartmeter/<device_id>/state`
|
||||
@@ -106,29 +102,43 @@ State topic:
|
||||
Fault topic (retained):
|
||||
- `smartmeter/<device_id>/faults`
|
||||
|
||||
State JSON fields (`src/json_codec.cpp`):
|
||||
State JSON (`src/json_codec.cpp`) includes:
|
||||
- `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)
|
||||
- optional link: `rssi`, `snr`
|
||||
- `err_last`, `rx_reject`, `rx_reject_text`
|
||||
- non-zero fault counters when available
|
||||
|
||||
Home Assistant discovery is enabled (`ENABLE_HA_DISCOVERY=true`) and publishes config topics under:
|
||||
- `homeassistant/sensor/<device_id>/<key>/config`
|
||||
Home Assistant discovery:
|
||||
- enabled by `ENABLE_HA_DISCOVERY=true`
|
||||
- publishes to `homeassistant/sensor/<device_id>/<key>/config`
|
||||
|
||||
## Web UI, Wi-Fi, Storage
|
||||
## Web UI, Wi-Fi, SD
|
||||
|
||||
- Wi-Fi/MQTT/NTP/web-auth config persists in Preferences (`wifi_manager`).
|
||||
- Wi-Fi/MQTT/NTP/web-auth config is stored in Preferences.
|
||||
- AP fallback SSID prefix: `DD3-Bridge-`.
|
||||
- Default web credentials: `admin/admin`.
|
||||
- SD logging enabled (`ENABLE_SD_LOGGING=true`).
|
||||
- Sender-specific web status includes duplicate-batch counters and the last duplicate time.
|
||||
- Sender-specific OLED page shows duplicate rate as `pct (absolute)` and last duplicate as `HH:MM`.
|
||||
- AP auth requirement is controlled by `WEB_AUTH_REQUIRE_AP` (default `false`).
|
||||
- STA auth requirement is controlled by `WEB_AUTH_REQUIRE_STA` (default `true`).
|
||||
|
||||
Web timestamp display:
|
||||
- human-facing timestamps show `epoch (HH:MM:SS TZ)` in local configured timezone.
|
||||
|
||||
SD CSV logging (`src/sd_logger.cpp`):
|
||||
- header: `ts_utc,ts_hms_utc,p_w,p1_w,p2_w,p3_w,e_kwh,bat_v,bat_pct,rssi,snr,err_m,err_d,err_tx,err_last`
|
||||
- `ts_hms_utc` is UTC `HH:MM:SS`
|
||||
|
||||
History parser (`src/web_server.cpp`):
|
||||
- expects the current CSV layout above
|
||||
- legacy CSV layouts are not parsed (no backward compatibility)
|
||||
|
||||
OLED duplicate display:
|
||||
- receiver sender-pages show duplicate rate as `pct (absolute)` and last duplicate as `HH:MM`.
|
||||
|
||||
## Build Environments
|
||||
|
||||
From `platformio.ini`:
|
||||
|
||||
- `lilygo-t3-v1-6-1`
|
||||
- `lilygo-t3-v1-6-1-test`
|
||||
- `lilygo-t3-v1-6-1-868`
|
||||
@@ -141,10 +151,12 @@ From `platformio.ini`:
|
||||
Example:
|
||||
|
||||
```bash
|
||||
~/.platformio/penv/bin/pio run -e lilygo-t3-v1-6-1
|
||||
python -m platformio 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/<device_id>/test`.
|
||||
`ENABLE_TEST_MODE` replaces normal loops with `test_sender_loop` / `test_receiver_loop` (`src/test_mode.cpp`):
|
||||
- Sender emits periodic JSON test payloads over LoRa.
|
||||
- Receiver decodes test payloads, updates display test codes, publishes MQTT to:
|
||||
- `smartmeter/<device_id>/test`
|
||||
|
||||
Reference in New Issue
Block a user