2026-01-20 01:39:06 +01:00
2026-01-20 01:39:06 +01:00
2026-01-23 17:00:51 +01:00
2026-01-20 01:39:06 +01:00
2026-01-20 01:39:06 +01:00
2026-01-21 21:20:05 +01:00

DD3 LoRa Bridge (Multi-Sender)

Unified firmware for LilyGO T3 v1.6.1 (ESP32 + SX1276 + SSD1306) that runs as Sender or Receiver based on a GPIO jumper. Senders read DD3 smart meter values, compress JSON, and transmit over LoRa. The receiver validates packets, publishes to MQTT, provides a web UI, and shows per-sender status on the OLED.

Hardware

Board: LilyGO T3 LoRa32 v1.6.1 (ESP32 + SX1276 + SSD1306 128x64 + LiPo) Variants:

  • SX1276 433 MHz module (default build)
  • SX1276 868 MHz module (use 868 build environments)

Pin Mapping

  • LoRa (SX1276)
    • SCK: GPIO5
    • MISO: GPIO19
    • MOSI: GPIO27
    • NSS/CS: GPIO18
    • RST: GPIO23
    • DIO0: GPIO26
  • OLED (SSD1306)
    • SDA: GPIO21
    • SCL: GPIO22
    • RST: not used (SSD1306 init uses -1 reset pin)
    • I2C address: 0x3C
  • Battery ADC: GPIO35 (via on-board divider)
  • Role select: GPIO13 (INPUT_PULLDOWN)
    • LOW = Sender
    • HIGH = Receiver
  • OLED control: GPIO14 (INPUT_PULLDOWN)
    • HIGH = force OLED on
    • LOW = allow auto-off after timeout
  • Smart meter UART RX: GPIO34 (input-only, always connected)

Notes on GPIOs

  • GPIO34/35/36/39 are input-only and have no internal pullups/pulldowns.
  • Strap pins (GPIO0/2/4/5/12/15) can affect boot; avoid for role or control jumpers.

Firmware Roles

Sender (battery-powered)

  • Reads DD3 smart meter via optical IR (UART 9600 7E1).
  • Extracts OBIS values:
    • Energy total: 1-0:1.8.0*255
    • Total power: 1-0:16.7.0*255
    • Phase power: 36.7 / 56.7 / 76.7
    • Phase voltage: 32.7 / 52.7 / 72.7
  • Reads battery voltage and estimates SoC.
  • Builds JSON payload, compresses, wraps in LoRa packet, transmits.
  • Deep sleeps between cycles.
  • Listens for LoRa time sync packets to set UTC clock.
  • OLED shows status + meter data pages.

Sender flow (pseudo-code):

void sender_cycle() {
  meter_read(data);         // SML/OBIS -> MeterData
  read_battery(data);       // VBAT + SoC
  data.ts_utc = time_get_utc_or_uptime();

  json = meterDataToJson(data);
  compressed = compressBuffer(json);
  lora_send(packet(MeterData, compressed));

  display_set_last_meter(data);
  display_set_last_read(ok);
  display_set_last_tx(ok);
  display_tick();

  lora_receive_time_sync(); // optional
  deep_sleep(SENDER_WAKE_INTERVAL_SEC);
}

Key sender functions:

bool meter_read(MeterData &data);        // parse SML frame, set OBIS fields
void read_battery(MeterData &data);      // ADC -> volts + percent
bool meterDataToJson(const MeterData&, String&);
bool compressBuffer(const uint8_t*, size_t, uint8_t*, size_t, size_t&);
bool lora_send(const LoraPacket &pkt);   // add header + CRC16 and transmit

Receiver (USB-powered)

  • WiFi STA connect using stored config; if not available/fails, starts AP.
  • NTP sync (UTC) and local display in Europe/Berlin.
  • Receives LoRa packets, verifies CRC16, decompresses, parses JSON.
  • Publishes meter JSON to MQTT.
  • Web UI:
    • AP mode: status + WiFi/MQTT config.
    • STA mode: status + per-sender pages.
  • OLED cycles through receiver status and per-sender pages.

Receiver loop (pseudo-code):

void receiver_loop() {
  if (lora_receive(pkt) && pkt.type == MeterData) {
    json = decompressBuffer(pkt.payload);
    if (jsonToMeterData(json, data)) {
      update_sender_status(data);
      mqtt_publish_state(data);
    }
  }

  if (time_to_send_timesync()) {
    time_send_timesync(self_short_id);
  }

  mqtt_loop();
  web_server_loop();
  display_set_receiver_status(...);
  display_tick();
}

Key receiver functions:

bool lora_receive(LoraPacket &pkt, uint32_t timeout_ms);
bool jsonToMeterData(const String &json, MeterData &data);
bool mqtt_publish_state(const MeterData &data);
void web_server_loop();                   // AP or STA UI
void time_send_timesync(uint16_t self_id);

Test Mode (compile-time)

Enabled by -DENABLE_TEST_MODE (see platformio.ini test environment).

  • Sender: sends 4-digit test code every ~30s in JSON.
  • Receiver: shows last test code per sender and publishes to /test topic.
  • Normal behavior is excluded from test builds.

Test sender (pseudo-code):

void test_sender_loop() {
  code = random_4_digits();
  json = {id, role:"sender", test_code: code, ts};
  lora_send(packet(TestCode, compress(json)));
  display_set_test_code(code);
}

Test receiver (pseudo-code):

void test_receiver_loop() {
  if (pkt.type == TestCode) {
    json = decompress(pkt.payload);
    update_sender_test_code(json);
    mqtt_publish_test(id, json);
  }
}

LoRa Protocol

Packet layout:

[0]    protocol_version (1)
[1]    role (0=sender, 1=receiver)
[2..3] device_id_short (uint16)
[4]    payload_type (0=meter, 1=test, 2=time_sync)
[5..N-3] compressed payload
[N-2..N-1] CRC16 (bytes 0..N-3)

LoRa radio settings:

  • Frequency: 433 MHz or 868 MHz (set by build env via LORA_FREQUENCY_HZ)
  • SF12, BW 125 kHz, CR 4/5, CRC on, Sync Word 0x34

Data Format

JSON payload (sender + MQTT):

{
  "id": "dd3-01",
  "ts": 1737200000,
  "energy_kwh": 1234.567,
  "p_total_w": 950.0,
  "p1_w": 500.0,
  "p2_w": 450.0,
  "p3_w": 0.0,
  "v1_v": 230.1,
  "v2_v": 229.8,
  "v3_v": 231.0,
  "bat_v": 3.92,
  "bat_pct": 78
}

Device IDs

  • Derived from WiFi STA MAC.
  • short_id = (MAC[4] << 8) | MAC[5]
  • device_id = dd3-%04X

Receiver expects known senders in include/config.h via:

constexpr uint8_t NUM_SENDERS = 1;
inline constexpr uint16_t EXPECTED_SENDER_IDS[NUM_SENDERS] = { 0xF19C };

OLED Behavior

  • After reset, OLED stays ON for 10 minutes regardless of switch.
  • After that:
    • GPIO14 HIGH: OLED forced ON.
    • GPIO14 LOW: start 10-minute auto-off timer.
  • Pages rotate every 10s.

Power & Battery

  • Sender disables WiFi/BLE, reads VBAT via ADC, uses linear SoC map:
    • 3.0 V = 0%
    • 4.2 V = 100%
  • Uses deep sleep between cycles (SENDER_WAKE_INTERVAL_SEC).

Web UI

  • AP SSID: DD3-Bridge-<short_id>
  • AP password: changeme123
  • Endpoints:
    • /: status overview
    • /wifi: WiFi/MQTT/NTP config (AP and STA)
    • /sender/<device_id>: per-sender details

MQTT

  • Topic: smartmeter/<deviceId>/state
  • QoS 0
  • Test mode: smartmeter/<deviceId>/test
  • Client ID: dd3-bridge-<device_id> (stable, derived from MAC)

NTP

  • NTP servers are configurable in the web UI (/wifi).
  • Defaults: pool.ntp.org and time.nist.gov.

Build Environments

  • lilygo-t3-v1-6-1: production build
  • lilygo-t3-v1-6-1-test: test build with ENABLE_TEST_MODE
  • lilygo-t3-v1-6-1-868: production build for 868 MHz modules
  • lilygo-t3-v1-6-1-868-test: test build for 868 MHz modules

Limits & Known Constraints

  • Compression: uses lightweight RLE (good for JSON but not optimal).
  • OBIS parsing: heuristic SML parser; may need tuning for some DD3 meters.
  • Payload size: JSON < 256 bytes (enforced by ArduinoJson static doc).
  • Battery ADC: uses simple linear calibration constant in power_manager.cpp.
  • OLED: no hardware reset line is used (matches working reference).

Files & Modules

  • include/config.h, src/config.cpp: pins, radio settings, sender IDs
  • include/data_model.h, src/data_model.cpp: MeterData + ID init
  • include/json_codec.h, src/json_codec.cpp: JSON encode/decode
  • include/compressor.h, src/compressor.cpp: RLE compression
  • include/lora_transport.h, src/lora_transport.cpp: LoRa packet + CRC
  • include/meter_driver.h, src/meter_driver.cpp: SML/OBIS parse
  • include/power_manager.h, src/power_manager.cpp: ADC + sleep
  • include/time_manager.h, src/time_manager.cpp: NTP + time sync
  • include/wifi_manager.h, src/wifi_manager.cpp: NVS config + WiFi
  • include/mqtt_client.h, src/mqtt_client.cpp: MQTT publish
  • include/web_server.h, src/web_server.cpp: AP/STA web pages
  • include/display_ui.h, src/display_ui.cpp: OLED pages + control
  • include/test_mode.h, src/test_mode.cpp: test sender/receiver
  • src/main.cpp: role detection and main loop

Quick Start

  1. Set role jumper on GPIO13:
    • LOW: sender
    • HIGH: receiver
  2. OLED control on GPIO14:
    • HIGH: always on
    • LOW: auto-off after 10 minutes
  3. Build and upload:
pio run -e lilygo-t3-v1-6-1 -t upload --upload-port COMx

Test mode:

pio run -e lilygo-t3-v1-6-1-test -t upload --upload-port COMx

868 MHz builds:

pio run -e lilygo-t3-v1-6-1-868 -t upload --upload-port COMx

868 MHz test mode:

pio run -e lilygo-t3-v1-6-1-868-test -t upload --upload-port COMx
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 1.1 MiB
Languages
C++ 92.2%
C 7.8%