Date: 2026-01-09 Status: ✅ FULLY OPERATIONAL - Both nodes + data pipeline complete
A complete 2-node LoRaWAN sensor network using STM32WL55JC microcontrollers with native SubGHz radio, plus a full data pipeline to Grafana visualization.
┌──────────────┐ ┌──────────────┐
│ LoRa-1 │ │ LoRa-2 │
│ SHT41 │ │ BME680 │
│ (4 bytes) │ │ (12 bytes) │
└──────┬───────┘ └──────┬───────┘
│ LoRaWAN AU915 │
└─────────┬──────────┘
▼
┌─────────────────┐
│ RAK7268V2 │
│ Gateway │
│ 10.10.10.254 │
└────────┬────────┘
│ MQTT :1883
▼
┌─────────────────┐
│ mqtt_to_influx │
│ Python Bridge │
└────────┬────────┘
│
▼
┌─────────────────┐
│ InfluxDB │
│ bucket:lorawan │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Grafana │
│ 10 panels │
└─────────────────┘
- MCU: STM32WL55JC (Cortex-M4 @ 48 MHz, HSE + PLL clock)
- Sensor: SHT41 temperature/humidity (I2C 0x44)
- Display: SSD1306 OLED 128x32 (I2C 0x3C)
- Radio: Integrated SubGHz SX126x LoRa radio
- Board: NUCLEO-WL55JC1 (probe ID: 003E00463234510A33353533)
- Framework: Embassy async v0.7.0 (Rust embedded async)
- LoRa PHY:
lora-phyv3.0 (SX126x driver) - LoRaWAN MAC:
lorawan-devicev0.12 (OTAA, Class A) - Region: AU915 Sub-band 2 (915.2-916.6 MHz, channels 8-15)
- ✅ LoRaWAN OTAA join (~7 seconds)
- ✅ SHT41 sensor reading (integer math only, no FPU)
- ✅ OLED display: TX count, temp/humidity, SNR/RSSI
- ✅ 4-byte uplink payload every ~30 seconds
- ✅ LED flash on transmission
- ✅ DevEUI:
23ce1bfeff091fac
| Bytes | Type | Description | Decode |
|---|---|---|---|
| 0-1 | i16 BE | Temperature × 100 | value / 100 = °C |
| 2-3 | u16 BE | Humidity × 100 | value / 100 = % |
Example: 27°C, 66% → [0x0A, 0x8C, 0x19, 0xC8] → base64 CowZyA==
- MCU: STM32WL55JC (Cortex-M4 @ 48 MHz, HSE + PLL clock)
- Sensor: BME680 environmental (I2C 0x76) - temp, humidity, pressure, gas
- Display: SH1106 OLED 128x64 (I2C 0x3C)
- Radio: Integrated SubGHz SX126x LoRa radio
- Board: NUCLEO-WL55JC1 (probe ID: 0026003A3234510A33353533)
- Framework: Embassy async v0.7.0 (Rust embedded async)
- LoRa PHY:
lora-phyv3.0 (SX126x driver) - LoRaWAN MAC:
lorawan-devicev0.12 (OTAA, Class A) - Region: AU915 Sub-band 2 (915.2-916.6 MHz, channels 8-15)
- ✅ LoRaWAN OTAA join (~7 seconds)
- ✅ BME680 sensor reading (integer math only, no FPU)
- ✅ OLED display: TX count, all readings, SNR/RSSI
- ✅ 12-byte uplink payload every ~30 seconds
- ✅ LED flash on transmission
- ✅ DevEUI:
24ce1bfeff091fac
| Bytes | Type | Description | Decode |
|---|---|---|---|
| 0-1 | i16 BE | Temperature × 100 | value / 100 = °C |
| 2-3 | u16 BE | Humidity × 100 | value / 100 = % |
| 4-5 | u16 BE | Pressure × 10 | value / 10 = hPa |
| 6-7 | u16 BE | Gas Resistance | kOhm |
| 8-11 | - | Padding | (unused) |
Python script running in Docker container:
- Subscribes to:
application/TOT/device/+/rxon gateway MQTT - Decodes: Base64 LoRaWAN payloads (auto-detects 4 or 12 byte format)
- Writes to: InfluxDB
lorawanbucket
Key configuration:
GATEWAY_MQTT_HOST = "10.10.10.254"
GATEWAY_MQTT_PORT = 1883
INFLUXDB_HOST = "wk7-influxdb"
INFLUXDB_BUCKET = "lorawan"- Bucket:
lorawan - Measurement:
lorawan_sensor - Tags:
node(lora1/lora2),sensor(sht41/bme680),dev_eui - Fields: temperature, humidity, pressure, gas_resistance, rssi, snr
10 panels displaying:
- Temperature (both nodes, time series)
- Humidity (both nodes, time series)
- Pressure (LoRa-2 only, time series)
- Gas Resistance (LoRa-2 only, time series)
- RSSI (both nodes, time series)
- SNR (both nodes, time series) 7-10. Stat panels (latest values)
Problem: Gateway parsed DevEUI as gateway's own EUI (ac1f09fffe1bce23)
Root Cause: LoRaWAN transmits EUIs in little-endian (LSB first), but lorawan-device crate expects arrays in transmission order.
Solution: Reverse DevEUI and AppEUI bytes (but NOT AppKey):
// Gateway shows: 23ce1bfeff091fac
const DEV_EUI: [u8; 8] = [0xAC, 0x1F, 0x09, 0xFF, 0xFE, 0x1B, 0xCE, 0x23]; // REVERSEDProblem: STM32WL55 has NO FPU - using f32/f64 causes HardFault
Solution: Integer-only math for sensor conversion:
// SHT41: T = -45 + (175 × raw) / 65535
let temp_celsius: i16 = -45 + ((175 * temp_raw as i32) / 65535) as i16;
// BME680: Similar integer-only conversions
let pressure_hpa: u16 = (raw_pressure / 100) as u16; // Pa to hPaChallenge: Both sensor and display need I2C2
Solution: Embassy async + unsafe peripheral stealing:
loop {
let mut i2c = unsafe { I2c::new_blocking(I2C2::steal(), ...) };
// Read sensor
let mut display = Ssd1306::new(I2CDisplayInterface::new(i2c), ...);
// Update display
// Both dropped here, hardware released
Timer::after_secs(2).await;
}Required: NUCLEO-WL55JC1 needs GPIO control for antenna routing
Solution: Custom iv.rs module implementing InterfaceVariant:
- PC3, PC4, PC5 control TX/RX paths
- High-power PA enabled for AU915
Problem: RAK gateway MQTT broker uses MQTT 3.1 protocol
Solution: Use paho-mqtt which handles protocol negotiation automatically
- Built-in LoRa Server (NOT ChirpStack)
- Application: "TOT" (ID: 1)
- Devices: Auto-add enabled
- MQTT Broker: 10.10.10.254:1883 (no auth)
# Uplink data
application/TOT/device/{DevEUI}/rx
# Join events
application/TOT/device/{DevEUI}/join{
"applicationID": "1",
"applicationName": "TOT",
"devEUI": "23ce1bfeff091fac",
"deviceName": "STM_Nodes",
"timestamp": 1767899338,
"fCnt": 3,
"fPort": 1,
"data": "CowZyA==",
"rxInfo": [{
"gatewayID": "ac1f09fffe1bce23",
"loRaSNR": 11.5,
"rssi": -17
}],
"txInfo": {
"frequency": 916600000,
"dr": 0
}
}cd firmware/lora-1
cargo build --release
cargo run --release # Build + flash + RTT logscd firmware/lora-2
cargo build --release
cargo run --release # Build + flash + RTT logs# Start InfluxDB + Grafana (from wk7)
cd ~/dev/4-month-plan/wk7-mqtt-influx
docker compose up -d
# Start MQTT bridge
cd ~/dev/4-month-plan/wk10-lorawan
docker compose up -dpython3 decode_payload.py CowZyA==
# Output:
# Temperature: 27.00°C
# Humidity: 66.00%| Metric | LoRa-1 | LoRa-2 |
|---|---|---|
| Join Time | ~7s | ~7s |
| Uplink Interval | ~30s | ~30s |
| Payload Size | 4 bytes | 12 bytes |
| Air Time | ~1.3s | ~1.5s |
| Sensor Read | ~10ms | ~15ms |
| Display Update | ~50ms | ~80ms |
- RSSI: -15 to -80 dBm
- SNR: 10-14 dB
- End-to-End Latency: <2 seconds
- Flash: ~28KB (11% of 256KB)
- RAM: ~8KB (12.5% of 64KB)
firmware/lora-1/Cargo.toml- Dependenciesfirmware/lora-1/src/main.rs- LoRaWAN + SHT41 + SSD1306firmware/lora-1/src/iv.rs- RF switch controlfirmware/lora-2/Cargo.toml- Dependenciesfirmware/lora-2/src/main.rs- LoRaWAN + BME680 + SH1106firmware/lora-2/src/iv.rs- RF switch control
mqtt_to_influx.py- MQTT bridge scriptdocker-compose.yml- Bridge container configgrafana/lorawan-dashboard.json- Dashboard export
README.md- System overviewUSERGUIDE.md- Complete deployment guideHARDWARE_CONFIG.md- Hardware specsLORAWAN_CREDENTIALS.md- Device credentialsCLAUDE.md- Development guidanceTROUBLESHOOTING_WL55.md- Common issuesdecode_payload.py- Payload decoder utility
- Byte order matters! LoRaWAN EUI little-endian transmission caught us off-guard
- No FPU - Integer math only, required careful sensor conversion
- Embassy async - Made peripheral sharing elegant with
unsafestealing - Working reference invaluable - Having reference projects saved hours
- Baby steps work - Incremental changes with testing prevented cascading failures
- MQTT 3.1 - RAK gateway requires older protocol (handled by paho-mqtt)
- Documentation pays off - CLAUDE.md guided entire implementation
Status: ✅ Production-ready LoRaWAN sensor network with full data pipeline Last Updated: 2026-01-09