3.9 KiB
DD3-LoRa-Bridge-MultiSender
Firmware for LilyGO T3 v1.6.1 (ESP32 + SX1276 + SSD1306) that runs as either:
Sender(PINGPIO14HIGH): reads one IEC 62056-21 meter, batches samples, sends over LoRa.Receiver(PINGPIO14LOW): 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
30samples and retries on missing ACK (BATCH_MAX_RETRIES=2, retry policyKeep). - Receiver uses STA mode when config is valid, otherwise AP fallback with web config.
LoRa Frame Protocol (Current)
On-air frame format:
[msg_kind:1][device_short_id:2][payload...][crc16:2]
msg_kind:
0=BatchUp1=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=0xDDB3kSchema=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=1andepoch >= 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.
Sender Meter Path
Implemented in src/meter_driver.cpp + sender loop in src/main.cpp:
- UART:
Serial2, RX pinGPIO34(PIN_METER_RX),9600 7E1 - Frame detection: starts at
'/', ends at'!', timeout protection included - 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*Whis automatically scaled to kWh
Sender samples every second and transmits batches every 30 seconds.
Receiver Behavior
For valid BatchUp decode:
- Reassemble chunks and decode payload.
- Send
AckDownimmediately. - Drop duplicate batches per sender (
batch_idtracking). - If
n==0: treat as sync request only. - Else convert samples, log to SD, update web UI, publish MQTT.
MQTT Topics and Payloads
State topic:
smartmeter/<device_id>/state
Fault topic (retained):
smartmeter/<device_id>/faults
State JSON fields (src/json_codec.cpp):
id,ts,e_kwhp_w,p1_w,p2_w,p3_wbat_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/<device_id>/<key>/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-1lilygo-t3-v1-6-1-testlilygo-t3-v1-6-1-868lilygo-t3-v1-6-1-868-testlilygo-t3-v1-6-1-payload-testlilygo-t3-v1-6-1-868-payload-testlilygo-t3-v1-6-1-prodlilygo-t3-v1-6-1-868-prod
Example:
~/.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/<device_id>/test.