4.3 KiB
DD3-LoRa-Bridge-MultiSender
Firmware for LilyGO T3 v1.6.1 (ESP32 + SX1276 + SSD1306) that runs as either:
Sender(PINGPIO14HIGH): reads multiple IEC 62056-21 meters, batches data, 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), in addition to LoRa PHY CRC. - Sender batches up to
30samples and retransmits on missing ACK (BATCH_MAX_RETRIES=2, policyKeep). - Receiver handles AP fallback when STA config is missing/invalid and exposes a config/status web UI.
LoRa Frame Protocol (Current)
Frame format on-air:
[msg_kind:1][device_short_id:2][payload...][crc16:2]
msg_kind:
0=BatchUp1=AckDown
BatchUp
BatchUp is chunked in transport (batch_id, chunk_index, chunk_count, total_len) and then decoded via payload_codec.
Payload header contains:
- fixed magic/schema fields (
kMagic=0xDDB3,kSchema=2) schema_id- sender/batch/time/error metadata
Supported payload schemas in this branch:
schema_id=1(EnergyMulti): integer kWh for up to 3 meters (energy1_kwh,energy2_kwh,energy3_kwh)schema_id=0(legacy): older energy/power delta encoding path remains decode-compatible
n == 0 is used as sync request (no meter samples).
AckDown (7 bytes)
[flags:1][batch_id_be:2][epoch_utc_be:4]
flags bit0:time_valid- Receiver sends ACK repeatedly (
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- only sync requests every
SYNC_REQUEST_INTERVAL_MS(15s) - no normal sampling/transmit until valid ACK time received
This prevents publishing/storing pre-threshold timestamps.
Multi-Meter Sender Behavior
Implemented in src/meter_driver.cpp + sender path in src/main.cpp:
- Meter protocol: IEC 62056-21 ASCII, Mode D style framing (
/ ... !) - UART settings:
9600 7E1 - Parsed OBIS:
1-0:1.8.0 - Conversion: floor to integer kWh (
floorf)
Meter count is build-dependent (include/config.h):
- Debug builds (
SERIAL_DEBUG_MODE=1):METER_COUNT=2 - Prod builds (
SERIAL_DEBUG_MODE=0):METER_COUNT=3
Default RX pins:
- Meter1:
GPIO34(Serial2) - Meter2:
GPIO25(Serial1) - Meter3:
GPIO3(Serial, prod only because debug serial is disabled)
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 to
MeterData, 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
For EnergyMulti samples, state JSON includes:
id,tsenergy1_kwh,energy2_kwh, optionalenergy3_kwhbat_v,bat_pct- optional link fields:
rssi,snr - fault/reject fields:
err_last,rx_reject,rx_reject_text(+ non-zero counters)
Home Assistant discovery publishing is enabled (ENABLE_HA_DISCOVERY=true) but still advertises legacy keys (e_kwh, p_w, p1_w, p2_w, p3_w) in src/mqtt_client.cpp.
Web UI, Wi-Fi, Storage
- STA config is stored in Preferences (
wifi_manager). - If STA/MQTT config is unavailable, receiver starts AP mode with SSID prefix
DD3-Bridge-. - Web auth defaults are
admin/admin(WEB_AUTH_DEFAULT_USER/PASS). - SD logging is 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 plain JSON test frames and publishes to smartmeter/<device_id>/test.