DD3-LoRa-Bridge-MultiSender

Firmware for LilyGO T3 v1.6.1 (ESP32 + SX1276 + SSD1306) that runs as either:

  • Sender (PIN GPIO14 HIGH): reads multiple IEC 62056-21 meters, batches data, sends over LoRa.
  • Receiver (PIN GPIO14 LOW): 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 30 samples and retransmits on missing ACK (BATCH_MAX_RETRIES=2, policy Keep).
  • 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 = BatchUp
  • 1 = 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=1 and epoch >= 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:

  1. Reassemble chunks and decode payload.
  2. Send AckDown immediately.
  3. Drop duplicate batches per sender (batch_id tracking).
  4. If n==0: treat as sync request only.
  5. 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, ts
  • energy1_kwh, energy2_kwh, optional energy3_kwh
  • bat_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-1
  • lilygo-t3-v1-6-1-test
  • lilygo-t3-v1-6-1-868
  • lilygo-t3-v1-6-1-868-test
  • lilygo-t3-v1-6-1-payload-test
  • lilygo-t3-v1-6-1-868-payload-test
  • lilygo-t3-v1-6-1-prod
  • lilygo-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.

Description
Unified Firmware for the LilyGO T3 v1.6.1 433MHz Version. Sender will read DD3 Smart Meter Data and send it to reciever as well as display on OLED. Reciever will publish to mqtt and show on a local website as well as OLED.
Readme 2.1 MiB
Languages
C++ 94.2%
C 5.8%