Files

288 lines
8.3 KiB
Markdown

# 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)**:
```cpp
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**:
```cpp
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)**:
```cpp
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**:
```cpp
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)**:
```cpp
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)**:
```cpp
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):
```json
{
"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:
```cpp
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:
```bash
pio run -e lilygo-t3-v1-6-1 -t upload --upload-port COMx
```
Test mode:
```bash
pio run -e lilygo-t3-v1-6-1-test -t upload --upload-port COMx
```
868 MHz builds:
```bash
pio run -e lilygo-t3-v1-6-1-868 -t upload --upload-port COMx
```
868 MHz test mode:
```bash
pio run -e lilygo-t3-v1-6-1-868-test -t upload --upload-port COMx
```