Files
DD3-LoRa-Bridge-MultiSender/README.md

4.3 KiB

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.