Add LoRa sensor network support

- Design LoRa packet protocol with CRC16 validation
- Add protocol header files (lora_protocol.h, lora_packet.h)
- Create battery-powered remote node firmware template
  - Support for DHT22, BME680, DS18B20 sensors
  - Deep sleep for battery conservation
  - Automatic gateway registration
- Add LoRa receive to gateway firmware
  - RadioLib SX1262 integration
  - Node registry for tracking up to 16 nodes
  - HTTP forwarding of received readings
  - ACK responses to remote nodes
- Create comprehensive setup guide with wiring diagrams

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
leehughes
2026-01-24 17:50:53 -06:00
co-authored by Claude Opus 4.5
parent 265a53768e
commit 5554341b49
11 changed files with 3199 additions and 34 deletions
+409
View File
@@ -0,0 +1,409 @@
# ZNET LoRa Sensor Network Protocol
## Overview
This document specifies the LoRa packet protocol for communication between remote sensor nodes and the ZNET gateway. The protocol is designed for:
- **Efficiency**: Minimal packet size for LoRa's limited bandwidth
- **Reliability**: CRC16 checksums and sequence numbers for error detection
- **Flexibility**: Support for multiple sensor types
- **Battery life**: Low power design with configurable sleep intervals
## Network Architecture
```
┌─────────────────┐ LoRa 915MHz ┌─────────────────┐
│ Remote Node 1 │◄───────────────────►│ │
│ (DHT22) │ │ Gateway │
├─────────────────┤ LoRa 915MHz │ ESP32 LoRa V3 │ WiFi ┌──────────┐
│ Remote Node 2 │◄───────────────────►│ │◄─────────────►│ ZNET Web │
│ (BME680) │ │ tank_gateway_1│ │ Backend │
├─────────────────┤ LoRa 915MHz │ │ └──────────┘
│ Remote Node 3 │◄───────────────────►│ │
│ (DHT22) │ └─────────────────┘
└─────────────────┘
```
## Packet Format
### General Structure
All packets follow this structure:
```
┌───────┬────────┬──────────┬─────────┬────────────┬──────┐
│ SYNC │ HEADER │ NODE ID │ SEQ │ PAYLOAD │ CRC │
│ 2B │ 1B │ 2B │ 1B │ 0-64B │ 2B │
└───────┴────────┴──────────┴─────────┴────────────┴──────┘
```
| Field | Size | Description |
|-------|------|-------------|
| SYNC | 2 bytes | Magic bytes `0x5A 0x4E` ("ZN" for ZNET) |
| HEADER | 1 byte | Packet type and flags |
| NODE ID | 2 bytes | Unique node identifier (0x0001-0xFFFE) |
| SEQ | 1 byte | Sequence number (0-255, wraps) |
| PAYLOAD | 0-64 bytes | Type-specific data |
| CRC | 2 bytes | CRC16-CCITT of all preceding bytes |
### Header Byte
```
Bit 7-4: Packet Type (0-15)
Bit 3: ACK Request (1 = wants acknowledgment)
Bit 2: Battery Low (1 = battery < 20%)
Bit 1: First Boot (1 = node just powered on)
Bit 0: Reserved (0)
```
### Packet Types
| Type | Value | Direction | Description |
|------|-------|-----------|-------------|
| SENSOR_DATA | 0x0 | Node → Gateway | Sensor readings |
| ACK | 0x1 | Gateway → Node | Acknowledgment |
| NAK | 0x2 | Gateway → Node | Negative ack (resend) |
| CONFIG_REQ | 0x3 | Node → Gateway | Request configuration |
| CONFIG_RESP | 0x4 | Gateway → Node | Configuration response |
| PING | 0x5 | Gateway → Node | Check if node alive |
| PONG | 0x6 | Node → Gateway | Response to ping |
| ALERT | 0x7 | Node → Gateway | Critical alert (immediate) |
| TIME_SYNC | 0x8 | Gateway → Node | Time synchronization |
| FIRMWARE_INFO | 0x9 | Node → Gateway | Firmware version info |
| REGISTER | 0xA | Node → Gateway | Node registration |
| REGISTER_ACK | 0xB | Gateway → Node | Registration accepted |
| Reserved | 0xC-0xF | - | Future use |
## Sensor Data Payload
### DHT22 Sensor (Type 0x01)
Temperature and humidity sensor for ambient conditions.
```
Offset Size Field
0 1 Sensor Type = 0x01
1 2 Temperature (°C × 10, signed int16, big-endian)
3 2 Humidity (% × 10, uint16, big-endian)
5 1 Battery Voltage (mV ÷ 20, 0-255 = 0-5100mV)
6 1 RSSI (signed int8, dBm)
─────────────
Total: 7 bytes
```
**Example**: 25.5°C, 65.0% humidity, 3.7V battery, -45 dBm RSSI
```
01 00 FF 02 8A B9 D3
│ └──┴── └──┴── │ └─ RSSI: -45 dBm
│ │ │ └─── Battery: 185 × 20 = 3700mV
│ │ └──────── Humidity: 650 / 10 = 65.0%
│ └────────────── Temperature: 255 / 10 = 25.5°C
└──────────────────── Sensor type: DHT22
```
### BME680 Sensor (Type 0x02)
Environmental sensor with gas resistance (VOC proxy).
```
Offset Size Field
0 1 Sensor Type = 0x02
1 2 Temperature (°C × 10, signed int16, big-endian)
3 2 Humidity (% × 10, uint16, big-endian)
5 2 Pressure (hPa - 900, uint16, big-endian)
7 2 Gas Resistance (kΩ, uint16, big-endian)
9 1 IAQ Index (0-500 air quality index, uint8)
10 1 Battery Voltage (mV ÷ 20)
11 1 RSSI (signed int8, dBm)
─────────────
Total: 12 bytes
```
**Notes**:
- Pressure is stored as offset from 900 hPa (range 900-965 hPa typical)
- IAQ Index: 0-50 = Good, 51-100 = Moderate, 101-150 = Poor, 151+ = Unhealthy
- Gas resistance correlates inversely with VOC presence
### DS18B20 Sensor (Type 0x03)
Waterproof temperature probe (if remote nodes need them).
```
Offset Size Field
0 1 Sensor Type = 0x03
1 2 Temperature (°C × 100, signed int16, big-endian)
3 1 Battery Voltage (mV ÷ 20)
4 1 RSSI (signed int8, dBm)
─────────────
Total: 5 bytes
```
**Note**: DS18B20 has 0.0625°C resolution, so we use × 100 for precision.
### Multi-Sensor Payload (Type 0x10)
For nodes with multiple sensors attached.
```
Offset Size Field
0 1 Sensor Type = 0x10
1 1 Sensor Count (1-4)
2 1 Sensor 1 Type
3 N Sensor 1 Data (type-specific, without type byte)
... (repeat for each sensor)
Last 1 Battery Voltage (mV ÷ 20)
Last+1 1 RSSI
```
## Configuration Payload
### CONFIG_REQ (Node → Gateway)
```
Offset Size Field
0 2 Current interval (seconds)
2 1 Firmware version major
3 1 Firmware version minor
4 8 Node name (null-padded ASCII)
```
### CONFIG_RESP (Gateway → Node)
```
Offset Size Field
0 2 Report interval (seconds, 0 = use default)
2 2 Warning threshold high (°C × 10)
4 2 Alert threshold high (°C × 10)
6 2 Warning threshold low (°C × 10, 0x8000 = disabled)
8 2 Alert threshold low (°C × 10, 0x8000 = disabled)
10 1 Flags:
Bit 0: Alerts enabled
Bit 1: ACK required
Bit 2-7: Reserved
```
## Registration Process
When a node powers on, it should register with the gateway:
```
1. Node sends REGISTER packet:
┌────────────────────────────────────────┐
│ Payload: │
│ 0-1: Proposed Node ID (or 0xFFFF) │
│ 2: Sensor Type │
│ 3: Firmware Version Major │
│ 4: Firmware Version Minor │
│ 5-12: Node Name (8 chars, null-pad) │
└────────────────────────────────────────┘
2. Gateway responds with REGISTER_ACK:
┌────────────────────────────────────────┐
│ Payload: │
│ 0-1: Assigned Node ID │
│ 2: Status (0=OK, 1=ID conflict) │
│ 3-4: Report interval (seconds) │
└────────────────────────────────────────┘
```
## Alert Packet
For critical conditions requiring immediate attention:
```
Offset Size Field
0 1 Alert Type:
0x01 = Temperature high
0x02 = Temperature low
0x03 = Humidity high
0x04 = Humidity low
0x05 = Battery critical
0x06 = Sensor failure
0x07 = VOC alert
1 2 Alert Value (type-specific)
3 2 Threshold Value
5 1 Duration (seconds this condition persisted)
```
## CRC16-CCITT Calculation
**Polynomial**: 0x1021
**Initial value**: 0xFFFF
**No final XOR**
```c
uint16_t crc16_ccitt(const uint8_t* data, size_t len) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; i++) {
crc ^= (uint16_t)data[i] << 8;
for (int j = 0; j < 8; j++) {
if (crc & 0x8000) {
crc = (crc << 1) ^ 0x1021;
} else {
crc <<= 1;
}
}
}
return crc;
}
```
## Timing and Duty Cycle
### LoRa Parameters (US 915 MHz)
| Parameter | Value | Notes |
|-----------|-------|-------|
| Frequency | 915.0 MHz | US ISM band |
| Bandwidth | 125 kHz | Standard LoRa |
| Spreading Factor | 9 | Balance of range/speed |
| Coding Rate | 4/7 | Forward error correction |
| Preamble | 8 symbols | Standard |
| Sync Word | 0x12 | Private network |
| TX Power | 14 dBm | Legal limit |
### Transmission Timing
| Packet Size | Air Time (SF9) | Duty Cycle @ 1% |
|-------------|----------------|-----------------|
| 15 bytes | ~51 ms | 1 packet / 5.1 sec |
| 20 bytes | ~67 ms | 1 packet / 6.7 sec |
| 30 bytes | ~97 ms | 1 packet / 9.7 sec |
**Recommended intervals**:
- Normal operation: 30-60 seconds
- Battery saving: 5-10 minutes
- Alert condition: 10 seconds (temporary)
### Node Sleep Schedule
```
┌────────────────────────────────────────────────────────┐
│ Wake Read TX RX Window Sleep │
│ (5ms) (100ms) (100ms) (500ms) (59s) │
│ ├──────────┼──────────┼──────────┼────────────┤ │
│ │ │ │ │ │ │
└───┴──────────┴──────────┴──────────┴────────────┴──────┘
```
## Node ID Assignment
| Range | Purpose |
|-------|---------|
| 0x0000 | Reserved (broadcast) |
| 0x0001-0x00FF | Tank sensors (DS18B20) |
| 0x0100-0x01FF | Ambient sensors (DHT22) |
| 0x0200-0x02FF | Environmental (BME680) |
| 0x0300-0x0FFF | Reserved for expansion |
| 0x1000-0xFFFE | Auto-assigned |
| 0xFFFF | Reserved (request auto-assign) |
## Example Packets
### DHT22 Sensor Data
Node 0x0101 reports 23.5°C, 55.0% humidity, 3.8V battery:
```
Hex: 5A 4E 08 01 01 42 01 00 EB 02 26 BE D3 XX XX
└──┴── │ └──┴── │ └─────────────────┴── CRC
│ │ └─ Seq: 0x42 (66)
│ └───── Node ID: 0x0101
└───────── Header: Type=0 (SENSOR_DATA), ACK_REQ=1
Payload breakdown:
01 - DHT22 type
00 EB - Temperature: 235 / 10 = 23.5°C
02 26 - Humidity: 550 / 10 = 55.0%
BE - Battery: 190 × 20 = 3800mV
D3 - RSSI: -45 dBm
```
### Gateway ACK
Gateway acknowledges sequence 0x42 from node 0x0101:
```
Hex: 5A 4E 10 01 01 42 XX XX
└──┴── │ └──┴── │ └── CRC
│ │ └─ Seq: 0x42 (echoed)
│ └───── Node ID: 0x0101
└───────── Header: Type=1 (ACK)
```
### BME680 Alert (High VOC)
Node 0x0201 sends VOC alert (IAQ = 175):
```
Hex: 5A 4E 78 02 01 15 07 00 AF 00 96 05 XX XX
└──┴── │ └──┴── │ │ └──┴── └──┴── └── CRC
│ │ │ │ │ └─ Threshold: 150
│ │ │ │ └──────── Alert value: 175
│ │ │ └────────────── Alert type: 0x07 (VOC)
│ │ └───────────────── Seq: 0x15 (21)
│ └───────────────────── Node ID: 0x0201
└─────────────────────────── Header: Type=7 (ALERT), ACK_REQ=1, BAT_LOW=1
```
## Implementation Notes
### Gateway Responsibilities
1. **Receive and decode** all incoming LoRa packets
2. **Validate CRC** and discard corrupt packets
3. **Track sequence numbers** per node for duplicate detection
4. **Send ACKs** for packets with ACK_REQ flag
5. **Forward data** to ZNET Web via HTTP POST
6. **Store node registry** with last-seen timestamps
7. **Send alerts** for nodes not reporting (timeout)
### Remote Node Responsibilities
1. **Sleep** between readings to conserve battery
2. **Read sensors** and validate readings
3. **Build and transmit** LoRa packet
4. **Listen for ACK** in RX window (if ACK_REQ set)
5. **Retry** up to 3 times if no ACK received
6. **Track battery** voltage and set BAT_LOW flag
7. **Register** with gateway on first boot
## Backend API Integration
The gateway should POST remote sensor data to the same endpoint as local sensors:
```json
{
"device_id": "tank_gateway_1",
"device_name": "E-Coat Tank Gateway",
"readings": [
{
"sensor_id": "lora_0x0101",
"sensor_type": "dht22",
"temperature_f": 74.3,
"humidity_pct": 55.0,
"is_valid": true,
"rssi_dbm": -45,
"battery_mv": 3800
},
{
"sensor_id": "lora_0x0201",
"sensor_type": "bme680",
"temperature_f": 76.1,
"humidity_pct": 48.5,
"pressure_hpa": 1013.2,
"gas_resistance_kohm": 150.5,
"iaq_index": 85,
"is_valid": true,
"rssi_dbm": -52,
"battery_mv": 3650
}
]
}
```
## Version History
| Version | Date | Changes |
|---------|------|---------|
| 1.0 | 2026-01-24 | Initial protocol specification |
+315
View File
@@ -0,0 +1,315 @@
# ZNET Temperature Sensor System - Setup Guide
Complete guide for setting up the ZNET temperature monitoring system with gateway and remote sensor nodes.
## System Overview
```
┌──────────────────────────────────────────────────────────────────────────┐
│ ZNET Temperature System │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────┐ LoRa 915MHz ┌────────────┐ │
│ │ Remote #1 │◄──────────────────────────►│ │ │
│ │ DHT22 │ │ Gateway │ WiFi │
│ │ ambient │ LoRa 915MHz │ ESP32 │◄──────┐ │
│ └────────────┘◄──────────────────────────►│ LoRa V3 │ │ │
│ ┌────────────┐ │ │ │ │
│ │ Remote #2 │ LoRa 915MHz │ DS18B20 x2 │ │ │
│ │ BME680 │◄──────────────────────────►│ (in tank) │ │ │
│ │ VOC/air │ └─────┬──────┘ │ │
│ └────────────┘ │ │ │
│ │ OLED │ │
│ │ Display │ │
│ ▼ ▼ │
│ ┌──────────────────────────┐ │
│ │ ZNET Web Backend │ │
│ │ (FastAPI + PostgreSQL)│ │
│ └──────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
```
## Hardware Required
### Gateway (1x per tank)
| Part | Model | Qty | Purpose | Notes |
|------|-------|-----|---------|-------|
| MCU | Meshnology ESP32 LoRa V3 | 1 | Main controller | Built-in OLED, LoRa |
| Sensor | DS18B20 waterproof | 2 | Tank temperature | Stainless steel, 1m cable |
| Resistor | 4.7kΩ 1/4W | 1 | 1-Wire pullup | Between DATA and VCC |
| Enclosure | IP65 junction box | 1 | Protection | At least 120x80x50mm |
| Cable gland | PG9 | 2 | Wire entry | For sensor cables |
| Power | 5V 2A USB adapter | 1 | Gateway power | Or 3.7V LiPo with USB charging |
### Remote Nodes (optional, 0-16 per gateway)
| Part | Model | Qty | Purpose | Notes |
|------|-------|-----|---------|-------|
| MCU | Heltec WiFi LoRa 32 V3 | 1 | Node controller | Or TTGO LoRa32 |
| Sensor | DHT22 or BME680 | 1 | Temp/humidity/VOC | Choose based on need |
| Battery | 3.7V LiPo 1000mAh | 1 | Power | With JST connector |
| Enclosure | Weatherproof box | 1 | Protection | IP54 minimum |
## Wiring Diagrams
### Gateway Wiring (ESP32 LoRa V3 + DS18B20 x2)
```
ESP32 LoRa V3
┌─────────────────────┐
│ │
│ GPIO7 ─────┬───────┼──► DS18B20 #1 DATA (yellow)
│ │ │
│ └───────┼──► DS18B20 #2 DATA (yellow)
│ │
│ 3.3V ──────┬───────┼──► DS18B20 #1 VCC (red)
│ │ │
│ └───────┼──► DS18B20 #2 VCC (red)
│ │
│ GND ───────┬───────┼──► DS18B20 #1 GND (black)
│ │ │
│ └───────┼──► DS18B20 #2 GND (black)
│ │
└─────────────────────┘
Add 4.7kΩ resistor between GPIO7 and 3.3V (pullup)
DS18B20 Color Code:
- Red: VCC (3.3V)
- Black: GND
- Yellow: DATA (1-Wire)
```
### Remote Node Wiring (ESP32 LoRa V3 + DHT22)
```
ESP32 LoRa V3
┌─────────────────────┐
│ │ DHT22 Module
│ GPIO7 ─────────────┼──────► DATA
│ │
│ 3.3V ──────────────┼──────► VCC
│ │
│ GND ───────────────┼──────► GND
│ │
│ │
│ VBAT ◄─────────────┼────── LiPo + (red)
│ │
│ GND ◄──────────────┼────── LiPo - (black)
│ │
└─────────────────────┘
DHT22 Module (4-pin with PCB):
- VCC: 3.3V
- DATA: GPIO7 (10kΩ pullup usually built-in)
- NC: Not connected
- GND: Ground
```
### Remote Node Wiring (ESP32 + BME680 via I2C)
```
ESP32 LoRa V3
┌─────────────────────┐
│ │ BME680 Module
│ GPIO21 (SDA)───────┼──────► SDA
│ │
│ GPIO22 (SCL)───────┼──────► SCL
│ │
│ 3.3V ──────────────┼──────► VCC
│ │
│ GND ───────────────┼──────► GND
│ │
│ VBAT ◄─────────────┼────── LiPo + (red)
│ │
│ GND ◄──────────────┼────── LiPo - (black)
│ │
└─────────────────────┘
BME680 I2C Address: 0x76 (default) or 0x77
```
## Firmware Installation
### Prerequisites
```bash
# Install PlatformIO
pip install platformio
# Clone firmware repository
cd ~/Nextcloud/Dev
git clone https://git.ecoat.us/leehughes/znet-temp-sensor.git
cd znet-temp-sensor
```
### Flash Gateway
1. **Connect ESP32 via USB**
2. **Configure settings** - Edit `include/config.h`:
```cpp
// Set your ZNET Web API endpoint
#define ZNET_API_URL "http://192.168.1.100:8000/api/v1/sensors/readings"
// Set unique device ID
#define DEVICE_ID "tank_gateway_tulsa"
#define DEVICE_NAME "Tulsa E-Coat Tank Gateway"
```
3. **Build and upload**:
```bash
pio run --target upload
```
4. **Monitor serial output**:
```bash
pio device monitor
```
5. **Configure WiFi**:
- On first boot, gateway creates WiFi network: `ZNET-TempSensor`
- Connect to it with password: `znettemp123`
- Browser opens captive portal - enter your WiFi credentials
- Gateway reboots and connects
### Flash Remote Node
1. **Connect ESP32 via USB**
2. **Configure node** - Edit `remote-node/include/config.h`:
```cpp
// Set unique node ID (or 0xFFFF for auto-assign)
#define NODE_ID 0x0101
// Human-readable name (max 8 chars)
#define NODE_NAME "Ambient1"
// Report interval (seconds)
#define REPORT_INTERVAL_SEC 60
```
3. **Build and upload** (choose variant):
```bash
cd remote-node
# For DHT22 sensor:
pio run -e heltec_v3_dht22 --target upload
# For BME680 sensor:
pio run -e heltec_v3_bme680 --target upload
```
4. **Verify operation**:
- Check serial output for successful registration
- Gateway should show increased node count on OLED
## Backend Setup
### Database Migration
```bash
cd ~/Nextcloud/Dev/znet-web/backend
# Run migration to create temperature tables
uv run alembic upgrade head
```
### Verify API Endpoint
```bash
# Check sensors endpoint is working
curl http://localhost:8000/api/v1/sensors/
# Test posting a reading
curl -X POST http://localhost:8000/api/v1/sensors/readings \
-H "Content-Type: application/json" \
-d '{
"device_id": "test_gateway",
"device_name": "Test Gateway",
"readings": [
{
"sensor_id": "tank_primary",
"temperature_f": 85.5,
"is_valid": true
}
]
}'
```
## Testing
### Test Gateway Locally
1. Power on gateway
2. Verify OLED shows temperature readings
3. Check serial output for API POST success:
```
Reading temperatures...
Sensor 1: 85.50°F (valid)
Sensor 2: 85.30°F (valid)
Posting to ZNET Web API...
API POST success (code 200)
```
### Test Remote Node
1. Power on remote node
2. Watch gateway serial output:
```
LoRa RX: 15 bytes, RSSI -45 dBm
Packet from 0x0101, type 0, seq 42
DHT22 from 0x0101: 74.3°F, 55.0% RH
LoRa reading forwarded (0x0101)
ACK sent to 0x0101 seq 42
```
### Test WebSocket Updates
1. Open ZNET Web dashboard in browser
2. Verify temperature displays update without page refresh
3. Check browser console for WebSocket messages
## Troubleshooting
### Gateway Issues
| Problem | Cause | Solution |
|---------|-------|----------|
| "No sensors found" | Wiring issue | Check DATA line, pullup resistor |
| "WiFi failed" | Wrong credentials | Reset and reconfigure via portal |
| "API POST failed" | Network/server issue | Check URL, firewall, server logs |
| OLED blank | I2C issue | Check SDA/SCL connections |
### Remote Node Issues
| Problem | Cause | Solution |
|---------|-------|----------|
| "LoRa init failed" | SPI issue | Check pin definitions match board |
| No ACK received | Out of range | Move closer, increase SF |
| Short battery life | Wake too often | Increase REPORT_INTERVAL_SEC |
| Sensor read fails | Bad wiring | Check connections, I2C address |
### LoRa Range Issues
| Symptom | Solution |
|---------|----------|
| RSSI < -100 dBm | Move nodes closer or add gain antenna |
| Many CRC errors | Reduce spreading factor (SF) |
| ACK timeouts | Increase ACK_TIMEOUT_MS |
## Production Deployment Checklist
- [ ] Gateway powered via stable 5V supply (not USB from laptop)
- [ ] Tank sensors fully submerged in paint bath
- [ ] Gateway enclosure sealed against moisture/fumes
- [ ] WiFi signal strength verified at gateway location
- [ ] API endpoint accessible from gateway network
- [ ] Remote nodes battery charged and secured
- [ ] Remote node enclosures sealed
- [ ] All sensors reading within expected range
- [ ] Alert thresholds configured by lab manager
- [ ] WebSocket updates verified in browser
- [ ] Backup sensors agree within 5°F