14 KiB
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:
SenderwhenGPIO14reads HIGH.ReceiverwhenGPIO14reads LOW.
- Device identity:
- derive
short_idfrom MAC bytes 4/5. - canonical
device_idformat:dd3-XXXXuppercase hex.
- derive
- LoRa transport:
- frame format:
[msg_kind][short_id_be][payload][crc16_ccitt]. - reject invalid CRC/msg-kind/length.
- frame format:
- Payload codec:
- schema
3withpresent_mask(30-bit sparse second map). - support
n==0sync-request packets.
- schema
- Time bootstrap guardrail:
- sender must not run normal sampling/transmit until valid ACK time received.
- accept ACK time only if
time_valid=1andepoch >= MIN_ACCEPTED_EPOCH_UTC.
- Sampling/transmit cadence:
- sender sample cadence 1 Hz.
- sender batch cadence 30 s.
- sync-request cadence 15 s while unsynced.
- sender retransmits reuse cached encoded payload bytes for same inflight batch.
- sender ACK receive window is adaptive from airtime + observed ACK RTT, with expanded second window on miss.
- Receiver behavior:
- decode/reconstruct sparse timestamps.
- ACK accepted batches promptly.
- reject unknown/mismatched sender identities before ACK and before SD/MQTT/web updates.
- update MQTT, web status, SD logging.
- Persistence:
- Wi-Fi/MQTT/NTP/web credentials in Preferences namespace
dd3cfg.
- Wi-Fi/MQTT/NTP/web credentials in Preferences namespace
- Web auth defaults:
WEB_AUTH_REQUIRE_STA=trueWEB_AUTH_REQUIRE_AP=true
- Web and display time rendering:
- local timezone from
TIMEZONE_TZ.
- local timezone from
- Sender diagnostics:
- structured sender diagnostics are emitted to serial debug output only.
- diagnostics do not change LoRa payload schema or remap payload fields.
- SD logging:
- CSV columns include both
ts_utcandts_hms_local. - history parser supports both current (
ts_utc,ts_hms_local,p_w,...) and legacy (ts_utc,p_w,...) layouts.
- CSV columns include both
3. Protocol and Data Contracts
LoraMsgKind:BatchUp=0AckDown=1
AckDownpayload fixed length7bytes:[flags:1][batch_id_be:2][epoch_utc_be:4]flags bit0 = time_valid- sender acceptance window is implementation-adaptive; payload format stays unchanged.
BatchInput:- fixed arrays length
30(energy_wh,p1_w,p2_w,p3_w) present_maskmust satisfy: only low 30 bits used andbit_count == n
- fixed arrays length
- Timestamp constraints:
- receiver rejects decoded data whose timestamps are below
MIN_ACCEPTED_EPOCH_UTC
- receiver rejects decoded data whose timestamps are below
- CSV header (current required layout):
ts_utc,ts_hms_local,p_w,p1_w,p2_w,p3_w,e_kwh,bat_v,bat_pct,rssi,snr,err_m,err_d,err_tx,err_last
- Home Assistant discovery contract:
- topic:
homeassistant/sensor/<device_id>/<key>/config unique_id:<device_id>_<key>device.identifiers:["<device_id>"]device.name:<device_id>device.model:DD3-LoRa-Bridgedevice.manufacturer:AcidBurns
- topic:
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
& < > " '.
- escape
String url_encode_component(const String&)- percent-encode non-safe characters.
bool sanitize_device_id(const String&, String&)- accept
XXXXordd3-XXXX; reject path traversal,%, invalid hex.
- accept
- Internal helpers to preserve behavior:
is_hex_charto_upper_hex4
src/meter_driver.cpp
void meter_init()- configure
Serial2at9600 7E1, RX pinPIN_METER_RX, RX buffer size8192on ESP32.
- configure
bool meter_poll_frame(const char *&, size_t&)- incremental frame collector with start
/, end!, timeout, overflow handling.
- incremental frame collector with start
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.
void meter_get_stats(MeterDriverStats&)- expose parser/UART counters for sender-local diagnostics.
- Internal parse helpers to preserve numeric behavior:
detect_obis_fieldparse_decimal_fixedparse_obis_ascii_payload_valueparse_obis_ascii_unit_scalehex_nibbleparse_obis_hex_payload_u32meter_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
0when not plausible. - updates "clock plausible" state independently from sync state.
- return epoch or
bool time_is_synced()- true only after explicit sync signals (NTP callback/status or trusted
time_set_utc).
- true only after explicit sync signals (NTP callback/status or trusted
void time_set_utc(uint32_t)- set system time and sync flags.
void time_get_local_hhmm(char*, size_t)- timezone-based local
HH:MMoutput.
- timezone-based local
uint32_t time_get_last_sync_utc()uint32_t time_get_last_sync_age_sec()- Internal behavior-critical helpers:
note_last_syncmark_syncedntp_sync_notification_cbensure_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_rejectcrc16_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_decodezigzag32,unzigzag32svarint_encode,svarint_decode
- Internal helpers:
write_u16_le,write_u32_leread_u16_le,read_u32_leensure_capacitybit_count32
- Optional self-test:
payload_codec_self_test(whenPAYLOAD_CODEC_TEST).
src/json_codec.cpp
bool meterDataToJson(const MeterData&, String&)- create MQTT state JSON with stable field semantics.
- Internal numeric formatting helpers:
round2round_to_i32short_id_from_device_idformat_float_2set_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_textmqtt_connectpublish_discovery_sensor- discovery payload uses canonical device identity fields and
manufacturer=AcidBurns
src/wifi_manager.cpp
void wifi_manager_init()bool wifi_load_config(WifiMqttConfig&)bool wifi_save_config(const WifiMqttConfig&)- returns
falsewhen any Preferences write/verify fails.
- returns
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.
- append/create per-day CSV under
- Internal behavior-critical helpers:
fault_textensure_dirformat_date_utcformat_hms_local
src/display_ui.cpp
Public display API that must remain behavior-equivalent:
display_power_downdisplay_initdisplay_set_roledisplay_set_self_idsdisplay_set_sender_statusesdisplay_set_last_meterdisplay_set_last_readdisplay_set_last_txdisplay_set_sender_queuedisplay_set_sender_batchesdisplay_set_last_errordisplay_set_receiver_statusdisplay_set_test_code(test mode)display_set_test_code_for_sender(test mode)display_tick
Internal rendering helpers to preserve behavior:
oled_set_powerage_secondsround_power_wrender_last_error_linerender_last_sync_linerender_sender_statusrender_sender_measurementrender_receiver_statusrender_receiver_sender
src/web_server.cpp
Public web API:
web_server_set_configweb_server_set_sender_faultsweb_server_set_last_batchweb_server_begin_apweb_server_begin_staweb_server_loop
Internal route/state functions to preserve behavior:
format_local_hmsformat_epoch_local_hmstimestamp_age_secondsround_power_wauth_requiredfault_textensure_authhtml_headerhtml_footerformat_faultssanitize_sd_download_pathcheckbox_checkedsanitize_history_device_idsanitize_download_filenamehistory_resethistory_date_from_epochhistory_open_next_filehistory_parse_linehistory_tickrender_sender_blockappend_sd_listinghandle_roothandle_wifi_gethandle_wifi_posthandle_senderhandle_manualhandle_history_starthandle_history_datahandle_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_printfbit_count32abs_diff_u32
-
Meter-time anchoring and ingest:
meter_time_update_snapshotset_last_meter_sampleparse_meter_frame_samplemeter_queue_push_latestmeter_reader_task_entrymeter_reader_startmeter_reader_pump
-
Sender/receiver state setup and shared state:
init_sender_statusesupdate_battery_cachebattery_sample_due
-
Queue and sample batching:
batch_queue_drop_oldestsender_note_rx_rejectsender_log_diagnosticsbatch_queue_peekbatch_queue_enqueuereset_build_countersappend_meter_samplelast_sample_ts
-
Fault tracking/publish:
note_faultage_secondscounters_changedpublish_faults_if_needed
-
Watchdog:
watchdog_initwatchdog_kick
-
Binary helpers and ID conversion:
write_u16_leread_u16_lewrite_u16_beread_u16_bewrite_u32_beread_u32_besender_id_from_short_idshort_id_from_sender_id
-
Numeric normalization/sanitization:
kwh_to_wh_from_floatfloat_to_i16_wfloat_to_i16_w_clampedbattery_mv_from_voltage
-
Timeout and airtime-driven scheduling:
compute_batch_rx_timeout_mscompute_batch_ack_timeout_ms
-
LoRa TX pipeline:
send_batch_payloadsend_batch_ackinvalidate_inflight_encode_cacheprepare_inflight_from_queuesend_inflight_batchsend_meter_batchsend_sync_requestresend_inflight_batchfinish_inflight_batch
-
LoRa RX reassembly/decode:
reset_batch_rxprocess_batch_packet
-
Role loop orchestration:
setupsender_loopreceiver_looploop
5. Rust Porting Constraints and Recommendations
- Preserve wire compatibility first:
- LoRa frame byte layout, CRC16, ACK format, payload schema v3.
- sender optimization changes must not alter payload field meanings.
- Preserve persistent storage keys:
- Preferences keys (
ssid,pass,mqhost,mqport,mquser,mqpass,ntp1,ntp2,webuser,webpass,valid).
- Preferences keys (
- 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 reader compatibility for both current and legacy layouts.
- 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 current and legacy CSV layouts successfully.
- MQTT state/fault payload fields match existing names and semantics.