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
|
# DD3-LoRa-Bridge-MultiSender
|
||||||
|
|
||||||
Firmware for LilyGO T3 v1.6.1 (`ESP32 + SX1276 + SSD1306`) that runs as either:
|
Firmware for LilyGO T3 v1.6.1 (`ESP32 + SX1276 + SSD1306`) that runs in two roles:
|
||||||
- `Sender` (PIN `GPIO14` HIGH): reads one IEC 62056-21 meter, batches samples, sends over LoRa.
|
- `Sender` (`GPIO14` HIGH): reads one IEC 62056-21 meter, builds 30-slot sparse batches, sends via LoRa.
|
||||||
- `Receiver` (PIN `GPIO14` LOW): receives/ACKs batches, publishes MQTT, serves web UI, logs to SD.
|
- `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`).
|
- Single codebase, role selected at boot by `detect_role()` (`src/config.cpp`).
|
||||||
- LoRa link uses explicit CRC16 frame protection in firmware (`src/lora_transport.cpp`).
|
- LoRa transport is wrapped with firmware-level CRC16-CCITT (`src/lora_transport.cpp`).
|
||||||
- Sender batches up to `30` samples and retries on missing ACK (`BATCH_MAX_RETRIES=2`, retry policy `Keep`).
|
- Sender meter ingest is decoupled from LoRa waits via FreeRTOS meter reader task + queue on ESP32 (`src/main.cpp`).
|
||||||
- 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` with a 30-bit `present_mask` over `[t_last-29, t_last]` (`src/payload_codec.cpp`).
|
||||||
- Batch payload codec is schema v3 and uses a sparse `present_mask` over a 30-second window (no schema v2 compatibility).
|
- Sender only starts normal metering/transmit flow after valid time bootstrap from receiver ACK.
|
||||||
- Sender derives epoch timestamps from meter Sekundenindex (`0-0:96.8.0*255`) using an epoch anchor when time is synced.
|
- Receiver runs STA mode if stored config is valid and connects, otherwise AP fallback.
|
||||||
- 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)
|
## LoRa Protocol
|
||||||
|
|
||||||
On-air frame format:
|
On-air frame:
|
||||||
|
|
||||||
`[msg_kind:1][device_short_id:2][payload...][crc16:2]`
|
`[msg_kind:1][device_short_id:2][payload...][crc16:2]`
|
||||||
|
|
||||||
`msg_kind`:
|
`msg_kind`:
|
||||||
- `0` = `BatchUp`
|
- `0`: `BatchUp`
|
||||||
- `1` = `AckDown`
|
- `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:
|
`[batch_id_le:2][chunk_index:1][chunk_count:1][total_len_le:2][chunk_payload...]`
|
||||||
- `kMagic=0xDDB3`
|
|
||||||
- `kSchema=3`
|
|
||||||
- metadata: sender, batch, `t_last`, `present_mask`, battery, fault counters
|
|
||||||
- data arrays: `energy_wh[]`, `p1_w[]`, `p2_w[]`, `p3_w[]`
|
|
||||||
|
|
||||||
`present_mask` is a 30-bit second map in the `[t_last-29, t_last]` window.
|
Receiver reassembles all chunks before decode.
|
||||||
Only set bits carry samples, so missing seconds are explicitly represented.
|
|
||||||
|
|
||||||
`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:1][batch_id_be:2][epoch_utc_be:4]`
|
||||||
|
|
||||||
- `flags bit0`: `time_valid`
|
- `flags bit0`: `time_valid`
|
||||||
- Receiver repeats ACK (`ACK_REPEAT_COUNT=3`, `ACK_REPEAT_DELAY_MS=200`).
|
- ACK is repeated (`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`).
|
- 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`
|
- `g_time_acquired=false`
|
||||||
- no normal sampling/transmit yet
|
- sends sync requests every `SYNC_REQUEST_INTERVAL_MS` (`15s`)
|
||||||
- sync request every `SYNC_REQUEST_INTERVAL_MS` (15s)
|
- does not run normal 1 Hz sample/batch flow yet
|
||||||
|
|
||||||
Only after valid ACK time is received:
|
After valid ACK time:
|
||||||
- system time is set
|
- `time_set_utc()` is called
|
||||||
- normal 1 Hz sampling and periodic LoRa batch transmit start
|
- `g_time_acquired=true`
|
||||||
|
- normal 1 Hz sampling + periodic batch transmission starts
|
||||||
|
|
||||||
This blocks pre-threshold timestamps from MQTT/SD paths.
|
Timezone:
|
||||||
|
- `TIMEZONE_TZ` from `include/config.h` is applied in `time_manager`.
|
||||||
Timezone handling:
|
- Web/OLED local-time rendering uses this timezone.
|
||||||
- Local time rendering uses `TIMEZONE_TZ` from `include/config.h`.
|
- Default: `CET-1CEST,M3.5.0/2,M10.5.0/3`.
|
||||||
- Default value is `CET-1CEST,M3.5.0/2,M10.5.0/3` and can be changed at compile time.
|
|
||||||
|
|
||||||
## Sender Meter Path
|
## 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`
|
Timestamp derivation:
|
||||||
- ESP32 RX buffer is enlarged to `8192` bytes to survive long LoRa blocking sections.
|
- anchor offset: `epoch_offset = epoch_now - meter_seconds`
|
||||||
- Frame detection: starts at `'/'`, ends at `'!'`, timeout protection included (`METER_FRAME_TIMEOUT_MS=3000`).
|
- sample epoch: `ts_utc = meter_seconds + epoch_offset`
|
||||||
- Parsing runs in a dedicated sender task and is handed to the main sender loop via queue.
|
- jump checks: rollback, wall-time delta mismatch, anchor drift
|
||||||
- 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
|
|
||||||
|
|
||||||
Timestamping/validation on sender:
|
Sender builds sparse 30-slot windows and sends every `METER_SEND_INTERVAL_MS` (`30s`).
|
||||||
- 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.
|
|
||||||
|
|
||||||
## Receiver Behavior
|
## Receiver Behavior
|
||||||
|
|
||||||
For valid `BatchUp` decode:
|
For decoded `BatchUp`:
|
||||||
1. Reassemble chunks and decode payload.
|
1. Reassemble and decode.
|
||||||
2. Send `AckDown` immediately.
|
2. Send `AckDown` immediately.
|
||||||
3. Drop duplicate batches per sender (`batch_id` tracking).
|
3. Track duplicates per configured sender (`EXPECTED_SENDER_IDS`).
|
||||||
4. Track duplicate stats per sender: absolute duplicates, total received, duplicate percentage, last duplicate timestamp.
|
4. If duplicate: update duplicate counters/time, skip data write/publish.
|
||||||
5. If `n==0`: treat as sync request only.
|
5. If `n==0`: sync request path only.
|
||||||
6. Else reconstruct timestamps from `t_last` + `present_mask`, preserving skipped seconds, then log to SD, update web UI, publish MQTT.
|
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:
|
State topic:
|
||||||
- `smartmeter/<device_id>/state`
|
- `smartmeter/<device_id>/state`
|
||||||
@@ -106,29 +102,43 @@ State topic:
|
|||||||
Fault topic (retained):
|
Fault topic (retained):
|
||||||
- `smartmeter/<device_id>/faults`
|
- `smartmeter/<device_id>/faults`
|
||||||
|
|
||||||
State JSON fields (`src/json_codec.cpp`):
|
State JSON (`src/json_codec.cpp`) includes:
|
||||||
- `id`, `ts`, `e_kwh`
|
- `id`, `ts`, `e_kwh`
|
||||||
- `p_w`, `p1_w`, `p2_w`, `p3_w`
|
- `p_w`, `p1_w`, `p2_w`, `p3_w`
|
||||||
- `bat_v`, `bat_pct`
|
- `bat_v`, `bat_pct`
|
||||||
- optional link fields: `rssi`, `snr`
|
- optional link: `rssi`, `snr`
|
||||||
- fault/reject fields: `err_last`, `rx_reject`, `rx_reject_text` (+ non-zero counters)
|
- `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:
|
Home Assistant discovery:
|
||||||
- `homeassistant/sensor/<device_id>/<key>/config`
|
- 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-`.
|
- AP fallback SSID prefix: `DD3-Bridge-`.
|
||||||
- Default web credentials: `admin/admin`.
|
- Default web credentials: `admin/admin`.
|
||||||
- SD logging enabled (`ENABLE_SD_LOGGING=true`).
|
- AP auth requirement is controlled by `WEB_AUTH_REQUIRE_AP` (default `false`).
|
||||||
- Sender-specific web status includes duplicate-batch counters and the last duplicate time.
|
- STA auth requirement is controlled by `WEB_AUTH_REQUIRE_STA` (default `true`).
|
||||||
- Sender-specific OLED page shows duplicate rate as `pct (absolute)` and last duplicate as `HH:MM`.
|
|
||||||
|
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
|
## Build Environments
|
||||||
|
|
||||||
From `platformio.ini`:
|
From `platformio.ini`:
|
||||||
|
|
||||||
- `lilygo-t3-v1-6-1`
|
- `lilygo-t3-v1-6-1`
|
||||||
- `lilygo-t3-v1-6-1-test`
|
- `lilygo-t3-v1-6-1-test`
|
||||||
- `lilygo-t3-v1-6-1-868`
|
- `lilygo-t3-v1-6-1-868`
|
||||||
@@ -141,10 +151,12 @@ From `platformio.ini`:
|
|||||||
Example:
|
Example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
~/.platformio/penv/bin/pio run -e lilygo-t3-v1-6-1
|
python -m platformio run -e lilygo-t3-v1-6-1
|
||||||
```
|
```
|
||||||
|
|
||||||
## Test Mode
|
## Test Mode
|
||||||
|
|
||||||
`ENABLE_TEST_MODE` replaces normal sender/receiver loops with dedicated test loops (`src/test_mode.cpp`).
|
`ENABLE_TEST_MODE` replaces normal loops with `test_sender_loop` / `test_receiver_loop` (`src/test_mode.cpp`):
|
||||||
It sends/receives JSON test frames and publishes to `smartmeter/<device_id>/test`.
|
- Sender emits periodic JSON test payloads over LoRa.
|
||||||
|
- Receiver decodes test payloads, updates display test codes, publishes MQTT to:
|
||||||
|
- `smartmeter/<device_id>/test`
|
||||||
|
|||||||
419
Requirements.md
Normal file
419
Requirements.md
Normal file
@@ -0,0 +1,419 @@
|
|||||||
|
# Firmware Requirements (Rust Port Preparation)
|
||||||
|
|
||||||
|
## 1. Scope
|
||||||
|
|
||||||
|
This document defines the behavior that must be preserved when recreating this firmware in another language (target: Rust).
|
||||||
|
It is based on the current `lora-refactor` code state and captures:
|
||||||
|
- functional behavior
|
||||||
|
- protocol/data contracts
|
||||||
|
- module and function responsibilities
|
||||||
|
- runtime state-machine requirements
|
||||||
|
|
||||||
|
Function names below are C++ references. Rust naming/layout may differ, but the behavior must remain equivalent.
|
||||||
|
|
||||||
|
## 2. System-Level Requirements
|
||||||
|
|
||||||
|
- Role selection:
|
||||||
|
- `Sender` when `GPIO14` reads HIGH.
|
||||||
|
- `Receiver` when `GPIO14` reads LOW.
|
||||||
|
- Device identity:
|
||||||
|
- derive `short_id` from MAC bytes 4/5.
|
||||||
|
- canonical `device_id` format: `dd3-XXXX` uppercase hex.
|
||||||
|
- LoRa transport:
|
||||||
|
- frame format: `[msg_kind][short_id_be][payload][crc16_ccitt]`.
|
||||||
|
- reject invalid CRC/msg-kind/length.
|
||||||
|
- Payload codec:
|
||||||
|
- schema `3` with `present_mask` (30-bit sparse second map).
|
||||||
|
- support `n==0` sync-request packets.
|
||||||
|
- Time bootstrap guardrail:
|
||||||
|
- sender must not run normal sampling/transmit until valid ACK time received.
|
||||||
|
- accept ACK time only if `time_valid=1` and `epoch >= MIN_ACCEPTED_EPOCH_UTC`.
|
||||||
|
- Sampling/transmit cadence:
|
||||||
|
- sender sample cadence 1 Hz.
|
||||||
|
- sender batch cadence 30 s.
|
||||||
|
- sync-request cadence 15 s while unsynced.
|
||||||
|
- Receiver behavior:
|
||||||
|
- decode/reconstruct sparse timestamps.
|
||||||
|
- ACK each decoded batch promptly.
|
||||||
|
- update MQTT, web status, SD logging.
|
||||||
|
- Persistence:
|
||||||
|
- Wi-Fi/MQTT/NTP/web credentials in Preferences namespace `dd3cfg`.
|
||||||
|
- Web and display time rendering:
|
||||||
|
- local timezone from `TIMEZONE_TZ`.
|
||||||
|
- SD logging:
|
||||||
|
- CSV columns include both `ts_utc` and `ts_hms_utc`.
|
||||||
|
- history parser expects this current layout.
|
||||||
|
|
||||||
|
## 3. Protocol and Data Contracts
|
||||||
|
|
||||||
|
- `LoraMsgKind`:
|
||||||
|
- `BatchUp=0`
|
||||||
|
- `AckDown=1`
|
||||||
|
- `AckDown` payload fixed length `7` bytes:
|
||||||
|
- `[flags:1][batch_id_be:2][epoch_utc_be:4]`
|
||||||
|
- `flags bit0 = time_valid`
|
||||||
|
- `BatchInput`:
|
||||||
|
- fixed arrays length `30` (`energy_wh`, `p1_w`, `p2_w`, `p3_w`)
|
||||||
|
- `present_mask` must satisfy: only low 30 bits used and `bit_count == n`
|
||||||
|
- Timestamp constraints:
|
||||||
|
- receiver rejects decoded data whose timestamps are below `MIN_ACCEPTED_EPOCH_UTC`
|
||||||
|
- CSV header (current required layout):
|
||||||
|
- `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`
|
||||||
|
|
||||||
|
## 4. Module and Function Requirements
|
||||||
|
|
||||||
|
## `src/config.cpp`
|
||||||
|
|
||||||
|
- `DeviceRole detect_role()`
|
||||||
|
- configure role pin input pulldown and map to sender/receiver role.
|
||||||
|
|
||||||
|
## `src/data_model.cpp`
|
||||||
|
|
||||||
|
- `void init_device_ids(uint16_t&, char*, size_t)`
|
||||||
|
- read MAC, derive short ID, format canonical device ID.
|
||||||
|
- `const char *rx_reject_reason_text(RxRejectReason)`
|
||||||
|
- stable mapping for diagnostics and payloads.
|
||||||
|
|
||||||
|
## `src/html_util.cpp`
|
||||||
|
|
||||||
|
- `String html_escape(const String&)`
|
||||||
|
- escape `& < > " '`.
|
||||||
|
- `String url_encode_component(const String&)`
|
||||||
|
- percent-encode non-safe characters.
|
||||||
|
- `bool sanitize_device_id(const String&, String&)`
|
||||||
|
- accept `XXXX` or `dd3-XXXX`; reject path traversal, `%`, invalid hex.
|
||||||
|
- Internal helpers to preserve behavior:
|
||||||
|
- `is_hex_char`
|
||||||
|
- `to_upper_hex4`
|
||||||
|
|
||||||
|
## `src/meter_driver.cpp`
|
||||||
|
|
||||||
|
- `void meter_init()`
|
||||||
|
- configure `Serial2` at `9600 7E1`, RX pin `PIN_METER_RX`, RX buffer size `8192` on ESP32.
|
||||||
|
- `bool meter_poll_frame(const char *&, size_t&)`
|
||||||
|
- incremental frame collector with start `/`, end `!`, timeout, overflow handling.
|
||||||
|
- `bool meter_parse_frame(const char*, size_t, MeterData&)`
|
||||||
|
- parse OBIS values and set meter data fields.
|
||||||
|
- `bool meter_read(MeterData&)`
|
||||||
|
- compatibility wrapper around poll+parse.
|
||||||
|
- Internal parse helpers to preserve numeric behavior:
|
||||||
|
- `parse_obis_ascii_value`
|
||||||
|
- `parse_obis_ascii_unit_scale`
|
||||||
|
- `hex_nibble`
|
||||||
|
- `parse_obis_hex_u32`
|
||||||
|
- `meter_debug_log`
|
||||||
|
|
||||||
|
## `src/power_manager.cpp`
|
||||||
|
|
||||||
|
- `void power_sender_init()`
|
||||||
|
- sender low-power setup (CPU freq, Wi-Fi/BT off, ADC setup).
|
||||||
|
- `void power_receiver_init()`
|
||||||
|
- receiver power setup.
|
||||||
|
- `void power_configure_unused_pins_sender()`
|
||||||
|
- configure known-unused pins with pulldown.
|
||||||
|
- `void read_battery(MeterData&)`
|
||||||
|
- averaged ADC conversion and voltage calibration.
|
||||||
|
- `uint8_t battery_percent_from_voltage(float)`
|
||||||
|
- LUT + interpolation.
|
||||||
|
- `void light_sleep_ms(uint32_t)`
|
||||||
|
- timer-based light sleep.
|
||||||
|
- `void go_to_deep_sleep(uint32_t)`
|
||||||
|
- timer-based deep sleep.
|
||||||
|
|
||||||
|
## `src/time_manager.cpp`
|
||||||
|
|
||||||
|
- `void time_receiver_init(const char*, const char*)`
|
||||||
|
- configure NTP servers and timezone env.
|
||||||
|
- `uint32_t time_get_utc()`
|
||||||
|
- return epoch or `0` when not plausible.
|
||||||
|
- `bool time_is_synced()`
|
||||||
|
- sync status helper.
|
||||||
|
- `void time_set_utc(uint32_t)`
|
||||||
|
- set system time and sync flags.
|
||||||
|
- `void time_get_local_hhmm(char*, size_t)`
|
||||||
|
- timezone-based local `HH:MM` output.
|
||||||
|
- `uint32_t time_get_last_sync_utc()`
|
||||||
|
- `uint32_t time_get_last_sync_age_sec()`
|
||||||
|
- Internal behavior-critical helpers:
|
||||||
|
- `note_last_sync`
|
||||||
|
- `ensure_timezone_set`
|
||||||
|
|
||||||
|
## `src/lora_transport.cpp`
|
||||||
|
|
||||||
|
- `void lora_init()`
|
||||||
|
- initialize SX1276 with configured LoRa params.
|
||||||
|
- `bool lora_send(const LoraPacket&)`
|
||||||
|
- frame pack + CRC append + transmit.
|
||||||
|
- `bool lora_receive(LoraPacket&, uint32_t timeout_ms)`
|
||||||
|
- parse frame, validate, return metadata including RSSI/SNR.
|
||||||
|
- `RxRejectReason lora_get_last_rx_reject_reason()`
|
||||||
|
- consume-and-clear reject reason.
|
||||||
|
- `bool lora_get_last_rx_signal(int16_t&, float&)`
|
||||||
|
- access last RX signal snapshot.
|
||||||
|
- `void lora_idle()`
|
||||||
|
- `void lora_sleep()`
|
||||||
|
- `void lora_receive_continuous()`
|
||||||
|
- `bool lora_receive_window(LoraPacket&, uint32_t)`
|
||||||
|
- `uint32_t lora_airtime_ms(size_t)`
|
||||||
|
- compute packet airtime from SF/BW/CR/preamble.
|
||||||
|
- Internal behavior-critical helpers:
|
||||||
|
- `note_reject`
|
||||||
|
- `crc16_ccitt`
|
||||||
|
|
||||||
|
## `src/payload_codec.cpp`
|
||||||
|
|
||||||
|
- `bool encode_batch(const BatchInput&, uint8_t*, size_t, size_t*)`
|
||||||
|
- schema v3 encoder with metadata, sparse present mask, delta coding.
|
||||||
|
- `bool decode_batch(const uint8_t*, size_t, BatchInput*)`
|
||||||
|
- strict schema/magic/flags decode + bounds checks.
|
||||||
|
- Varint primitives:
|
||||||
|
- `uleb128_encode`, `uleb128_decode`
|
||||||
|
- `zigzag32`, `unzigzag32`
|
||||||
|
- `svarint_encode`, `svarint_decode`
|
||||||
|
- Internal helpers:
|
||||||
|
- `write_u16_le`, `write_u32_le`
|
||||||
|
- `read_u16_le`, `read_u32_le`
|
||||||
|
- `ensure_capacity`
|
||||||
|
- `bit_count32`
|
||||||
|
- Optional self-test:
|
||||||
|
- `payload_codec_self_test` (when `PAYLOAD_CODEC_TEST`).
|
||||||
|
|
||||||
|
## `src/json_codec.cpp`
|
||||||
|
|
||||||
|
- `bool meterDataToJson(const MeterData&, String&)`
|
||||||
|
- create MQTT state JSON with stable field semantics.
|
||||||
|
- Internal numeric formatting helpers:
|
||||||
|
- `round2`
|
||||||
|
- `round_to_i32`
|
||||||
|
- `short_id_from_device_id`
|
||||||
|
- `format_float_2`
|
||||||
|
- `set_int_or_null`
|
||||||
|
|
||||||
|
## `src/mqtt_client.cpp`
|
||||||
|
|
||||||
|
- `void mqtt_init(const WifiMqttConfig&, const char*)`
|
||||||
|
- `void mqtt_loop()`
|
||||||
|
- `bool mqtt_is_connected()`
|
||||||
|
- `bool mqtt_publish_state(const MeterData&)`
|
||||||
|
- `bool mqtt_publish_faults(const char*, const FaultCounters&, FaultType, uint32_t)`
|
||||||
|
- `bool mqtt_publish_discovery(const char*)`
|
||||||
|
- `bool mqtt_publish_test(const char*, const String&)` (test mode only)
|
||||||
|
- Internal behavior-critical helpers:
|
||||||
|
- `fault_text`
|
||||||
|
- `mqtt_connect`
|
||||||
|
- `publish_discovery_sensor`
|
||||||
|
|
||||||
|
## `src/wifi_manager.cpp`
|
||||||
|
|
||||||
|
- `void wifi_manager_init()`
|
||||||
|
- `bool wifi_load_config(WifiMqttConfig&)`
|
||||||
|
- `bool wifi_save_config(const WifiMqttConfig&)`
|
||||||
|
- `bool wifi_connect_sta(const WifiMqttConfig&, uint32_t timeout_ms)`
|
||||||
|
- `void wifi_start_ap(const char*, const char*)`
|
||||||
|
- `bool wifi_is_connected()`
|
||||||
|
- `String wifi_get_ssid()`
|
||||||
|
|
||||||
|
## `src/sd_logger.cpp`
|
||||||
|
|
||||||
|
- `void sd_logger_init()`
|
||||||
|
- `bool sd_logger_is_ready()`
|
||||||
|
- `void sd_logger_log_sample(const MeterData&, bool include_error_text)`
|
||||||
|
- append/create per-day CSV under `/dd3/<device_id>/YYYY-MM-DD.csv`.
|
||||||
|
- Internal behavior-critical helpers:
|
||||||
|
- `fault_text`
|
||||||
|
- `ensure_dir`
|
||||||
|
- `format_date_utc`
|
||||||
|
- `format_hms_utc`
|
||||||
|
|
||||||
|
## `src/display_ui.cpp`
|
||||||
|
|
||||||
|
Public display API that must remain behavior-equivalent:
|
||||||
|
- `display_power_down`
|
||||||
|
- `display_init`
|
||||||
|
- `display_set_role`
|
||||||
|
- `display_set_self_ids`
|
||||||
|
- `display_set_sender_statuses`
|
||||||
|
- `display_set_last_meter`
|
||||||
|
- `display_set_last_read`
|
||||||
|
- `display_set_last_tx`
|
||||||
|
- `display_set_sender_queue`
|
||||||
|
- `display_set_sender_batches`
|
||||||
|
- `display_set_last_error`
|
||||||
|
- `display_set_receiver_status`
|
||||||
|
- `display_set_test_code` (test mode)
|
||||||
|
- `display_set_test_code_for_sender` (test mode)
|
||||||
|
- `display_tick`
|
||||||
|
|
||||||
|
Internal rendering helpers to preserve behavior:
|
||||||
|
- `oled_set_power`
|
||||||
|
- `age_seconds`
|
||||||
|
- `round_power_w`
|
||||||
|
- `render_last_error_line`
|
||||||
|
- `render_last_sync_line`
|
||||||
|
- `render_sender_status`
|
||||||
|
- `render_sender_measurement`
|
||||||
|
- `render_receiver_status`
|
||||||
|
- `render_receiver_sender`
|
||||||
|
|
||||||
|
## `src/web_server.cpp`
|
||||||
|
|
||||||
|
Public web API:
|
||||||
|
- `web_server_set_config`
|
||||||
|
- `web_server_set_sender_faults`
|
||||||
|
- `web_server_set_last_batch`
|
||||||
|
- `web_server_begin_ap`
|
||||||
|
- `web_server_begin_sta`
|
||||||
|
- `web_server_loop`
|
||||||
|
|
||||||
|
Internal route/state functions to preserve behavior:
|
||||||
|
- `format_local_hms`
|
||||||
|
- `format_epoch_local_hms`
|
||||||
|
- `timestamp_age_seconds`
|
||||||
|
- `round_power_w`
|
||||||
|
- `auth_required`
|
||||||
|
- `fault_text`
|
||||||
|
- `ensure_auth`
|
||||||
|
- `html_header`
|
||||||
|
- `html_footer`
|
||||||
|
- `format_faults`
|
||||||
|
- `sanitize_sd_download_path`
|
||||||
|
- `checkbox_checked`
|
||||||
|
- `sanitize_history_device_id`
|
||||||
|
- `sanitize_download_filename`
|
||||||
|
- `history_reset`
|
||||||
|
- `history_date_from_epoch`
|
||||||
|
- `history_open_next_file`
|
||||||
|
- `history_parse_line`
|
||||||
|
- `history_tick`
|
||||||
|
- `render_sender_block`
|
||||||
|
- `append_sd_listing`
|
||||||
|
- `handle_root`
|
||||||
|
- `handle_wifi_get`
|
||||||
|
- `handle_wifi_post`
|
||||||
|
- `handle_sender`
|
||||||
|
- `handle_manual`
|
||||||
|
- `handle_history_start`
|
||||||
|
- `handle_history_data`
|
||||||
|
- `handle_sd_download`
|
||||||
|
|
||||||
|
## `src/test_mode.cpp` (`ENABLE_TEST_MODE`)
|
||||||
|
|
||||||
|
- `test_sender_loop`
|
||||||
|
- periodic JSON test frame transmit.
|
||||||
|
- `test_receiver_loop`
|
||||||
|
- decode test JSON, update display test markers, publish MQTT test topic.
|
||||||
|
|
||||||
|
## `src/main.cpp` (Core Orchestration)
|
||||||
|
|
||||||
|
These functions define end-to-end firmware behavior and must have equivalents:
|
||||||
|
|
||||||
|
- Logging/utilities:
|
||||||
|
- `serial_debug_printf`
|
||||||
|
- `bit_count32`
|
||||||
|
- `abs_diff_u32`
|
||||||
|
|
||||||
|
- Meter-time anchoring and ingest:
|
||||||
|
- `meter_time_update_snapshot`
|
||||||
|
- `set_last_meter_sample`
|
||||||
|
- `parse_meter_frame_sample`
|
||||||
|
- `meter_queue_push_latest`
|
||||||
|
- `meter_reader_task_entry`
|
||||||
|
- `meter_reader_start`
|
||||||
|
- `meter_reader_pump`
|
||||||
|
|
||||||
|
- Sender/receiver state setup and shared state:
|
||||||
|
- `init_sender_statuses`
|
||||||
|
- `update_battery_cache`
|
||||||
|
- `battery_sample_due`
|
||||||
|
|
||||||
|
- Queue and sample batching:
|
||||||
|
- `batch_queue_drop_oldest`
|
||||||
|
- `sender_note_rx_reject`
|
||||||
|
- `batch_queue_peek`
|
||||||
|
- `batch_queue_enqueue`
|
||||||
|
- `reset_build_counters`
|
||||||
|
- `append_meter_sample`
|
||||||
|
- `last_sample_ts`
|
||||||
|
|
||||||
|
- Fault tracking/publish:
|
||||||
|
- `note_fault`
|
||||||
|
- `age_seconds`
|
||||||
|
- `counters_changed`
|
||||||
|
- `publish_faults_if_needed`
|
||||||
|
|
||||||
|
- Watchdog:
|
||||||
|
- `watchdog_init`
|
||||||
|
- `watchdog_kick`
|
||||||
|
|
||||||
|
- Binary helpers and ID conversion:
|
||||||
|
- `write_u16_le`
|
||||||
|
- `read_u16_le`
|
||||||
|
- `write_u16_be`
|
||||||
|
- `read_u16_be`
|
||||||
|
- `write_u32_be`
|
||||||
|
- `read_u32_be`
|
||||||
|
- `sender_id_from_short_id`
|
||||||
|
- `short_id_from_sender_id`
|
||||||
|
|
||||||
|
- Numeric normalization/sanitization:
|
||||||
|
- `kwh_to_wh_from_float`
|
||||||
|
- `float_to_i16_w`
|
||||||
|
- `float_to_i16_w_clamped`
|
||||||
|
- `battery_mv_from_voltage`
|
||||||
|
|
||||||
|
- Timeout and airtime-driven scheduling:
|
||||||
|
- `compute_batch_rx_timeout_ms`
|
||||||
|
- `compute_batch_ack_timeout_ms`
|
||||||
|
|
||||||
|
- LoRa TX pipeline:
|
||||||
|
- `send_batch_payload`
|
||||||
|
- `send_batch_ack`
|
||||||
|
- `prepare_inflight_from_queue`
|
||||||
|
- `send_inflight_batch`
|
||||||
|
- `send_meter_batch`
|
||||||
|
- `send_sync_request`
|
||||||
|
- `resend_inflight_batch`
|
||||||
|
- `finish_inflight_batch`
|
||||||
|
|
||||||
|
- LoRa RX reassembly/decode:
|
||||||
|
- `reset_batch_rx`
|
||||||
|
- `process_batch_packet`
|
||||||
|
|
||||||
|
- Role loop orchestration:
|
||||||
|
- `setup`
|
||||||
|
- `sender_loop`
|
||||||
|
- `receiver_loop`
|
||||||
|
- `loop`
|
||||||
|
|
||||||
|
## 5. Rust Porting Constraints and Recommendations
|
||||||
|
|
||||||
|
- Preserve wire compatibility first:
|
||||||
|
- LoRa frame byte layout, CRC16, ACK format, payload schema v3.
|
||||||
|
- Preserve persistent storage keys:
|
||||||
|
- Preferences keys (`ssid`, `pass`, `mqhost`, `mqport`, `mquser`, `mqpass`, `ntp1`, `ntp2`, `webuser`, `webpass`, `valid`).
|
||||||
|
- Preserve timing constants and acceptance thresholds:
|
||||||
|
- bootstrap guardrail, retry counts, schedule intervals, min accepted epoch.
|
||||||
|
- Preserve CSV output layout exactly:
|
||||||
|
- consumers (history parser and external tooling) depend on it.
|
||||||
|
- Preserve enum meanings:
|
||||||
|
- `FaultType`, `RxRejectReason`, `LoraMsgKind`.
|
||||||
|
|
||||||
|
Suggested Rust module split:
|
||||||
|
- `config`, `ids`, `meter`, `power`, `time`, `lora_transport`, `payload_codec`, `batch`, `mqtt`, `wifi_cfg`, `sd_log`, `web`, `display`, `runtime`.
|
||||||
|
|
||||||
|
Suggested Rust primitives:
|
||||||
|
- async task for meter reader + bounded channel (drop-oldest behavior).
|
||||||
|
- explicit state structs for sender/receiver loops.
|
||||||
|
- serde-free/manual codec for wire compatibility where needed.
|
||||||
|
|
||||||
|
## 6. Port Validation Checklist
|
||||||
|
|
||||||
|
- Sender unsynced boot sends only sync requests.
|
||||||
|
- ACK time bootstrap unlocks normal sender sampling.
|
||||||
|
- Sparse present-mask encode/decode round-trip matches C++.
|
||||||
|
- Receiver reconstructs timestamps correctly for gaps.
|
||||||
|
- Duplicate batch handling updates counters and suppresses duplicate publish/log.
|
||||||
|
- Web UI shows `epoch (HH:MM:SS TZ)` local time.
|
||||||
|
- SD CSV header/fields match expected order.
|
||||||
|
- History endpoint reads only current CSV layout successfully.
|
||||||
|
- MQTT state/fault payload fields match existing names and semantics.
|
||||||
Reference in New Issue
Block a user