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
+157
View File
@@ -0,0 +1,157 @@
# ZNET LoRa Remote Sensor Node
Battery-powered sensor nodes that transmit temperature, humidity, and air quality data to the ZNET gateway via LoRa.
## Supported Hardware
| Board | Chip | LoRa | Notes |
|-------|------|------|-------|
| Heltec WiFi LoRa 32 V3 | ESP32-S3 | SX1262 | Recommended - built-in OLED |
| TTGO LoRa32 V2.1 | ESP32 | SX1276 | Budget option |
| DIY ESP32 + SX1276 | ESP32 | SX1276 | Full customization |
## Supported Sensors
| Sensor | Data | Use Case |
|--------|------|----------|
| DHT22 | Temp + Humidity | Ambient monitoring |
| BME680 | Temp + Humidity + Pressure + Gas | Air quality monitoring |
| DS18B20 | Temperature (high precision) | Remote tank monitoring |
## Quick Start
### 1. Install PlatformIO
```bash
pip install platformio
```
### 2. Configure Node
Edit `include/config.h`:
```cpp
// Set unique node ID or use 0xFFFF for auto-assign
#define NODE_ID 0x0101
// Node name (max 8 chars)
#define NODE_NAME "Ambient1"
// Report interval (seconds)
#define REPORT_INTERVAL_SEC 60
```
### 3. Build & Upload
```bash
cd remote-node
# For Heltec V3 with DHT22:
pio run -e heltec_v3_dht22 --target upload
# For Heltec V3 with BME680:
pio run -e heltec_v3_bme680 --target upload
# For TTGO board:
pio run -e ttgo_lora32_v21 --target upload
```
### 4. Monitor Output
```bash
pio device monitor
```
## Wiring
### DHT22 Sensor
```
ESP32 GPIO7 ─────────── DHT22 DATA
ESP32 3.3V ──────────── DHT22 VCC
ESP32 GND ───────────── DHT22 GND
10kΩ pullup between DATA and VCC
```
### BME680 Sensor (I2C)
```
ESP32 SDA (21) ──────── BME680 SDA
ESP32 SCL (22) ──────── BME680 SCL
ESP32 3.3V ──────────── BME680 VCC
ESP32 GND ───────────── BME680 GND
```
### Battery Connection
```
LiPo + ─────┬───────── ESP32 VBAT
│
┌┴┐
│ │ 100kΩ
└┬┘
├───────── ADC Pin (Battery monitoring)
┌┴┐
│ │ 100kΩ
└┬┘
│
LiPo - ─────┴───────── ESP32 GND
```
## Power Consumption
| State | Current | Notes |
|-------|---------|-------|
| Deep Sleep | ~10 μA | ESP32 deep sleep |
| Active (reading) | ~15 mA | Sensor reading |
| TX | ~80 mA | LoRa transmission |
| **Average @ 60s** | **~1 mA** | Typical |
### Battery Life Estimates (1000mAh LiPo)
| Interval | Battery Life |
|----------|--------------|
| 30 sec | ~2 weeks |
| 60 sec | ~1 month |
| 5 min | ~6 months |
| 10 min | ~1 year |
## Protocol
See [../docs/LORA_PROTOCOL.md](../docs/LORA_PROTOCOL.md) for full LoRa packet specification.
### Node ID Ranges
| Range | Purpose |
|-------|---------|
| 0x0001-0x00FF | Tank sensors |
| 0x0100-0x01FF | Ambient (DHT22) |
| 0x0200-0x02FF | Environmental (BME680) |
| 0xFFFF | Auto-assign |
## Troubleshooting
### No LoRa transmission
1. Check LoRa frequency matches gateway (915 MHz for US)
2. Verify SPI pins are correct for your board
3. Check serial monitor for "LoRa init failed" errors
### Sensor read fails
1. Check wiring connections
2. Verify correct GPIO pins in config.h
3. For I2C sensors, run I2C scanner to verify address
### Short battery life
1. Increase `REPORT_INTERVAL_SEC`
2. Disable OLED (`ENABLE_OLED 0`)
3. Disable serial debug in production (`ENABLE_SERIAL_DEBUG 0`)
### Not receiving ACKs
1. Verify gateway is running and receiving
2. Check sync word matches (0x12)
3. Reduce spreading factor if range is short (better reliability)
+154
View File
@@ -0,0 +1,154 @@
/**
* ZNET LoRa Remote Sensor Node Configuration
*
* Edit these values for your specific node deployment.
*/
#ifndef CONFIG_H
#define CONFIG_H
// ============================================================================
// NODE IDENTIFICATION
// ============================================================================
// Unique node ID (see docs/LORA_PROTOCOL.md for ID ranges)
// - 0x0100-0x01FF: Ambient sensors (DHT22)
// - 0x0200-0x02FF: Environmental (BME680)
// - 0xFFFF: Request auto-assignment from gateway
#ifndef NODE_ID
#define NODE_ID 0xFFFF // Auto-assign
#endif
// Human-readable node name (max 8 chars)
#ifndef NODE_NAME
#define NODE_NAME "Remote1"
#endif
// Firmware version
#define FW_VERSION_MAJOR 1
#define FW_VERSION_MINOR 0
// ============================================================================
// SENSOR CONFIGURATION
// ============================================================================
// Sensor type is set by platformio.ini build flags:
// -DSENSOR_TYPE_DHT22 or -DSENSOR_TYPE_BME680
// DHT22 data pin (if not set by build flags)
#ifndef DHT_PIN
#define DHT_PIN 7
#endif
// BME680 I2C address (0x76 or 0x77)
#ifndef BME680_I2C_ADDR
#define BME680_I2C_ADDR 0x76
#endif
// DS18B20 1-Wire pin (if using DS18B20 variant)
#ifndef ONEWIRE_PIN
#define ONEWIRE_PIN 7
#endif
// ============================================================================
// TIMING CONFIGURATION
// ============================================================================
// Report interval in seconds (default: 60 sec)
// Can be overridden by gateway via CONFIG_RESP packet
#ifndef REPORT_INTERVAL_SEC
#define REPORT_INTERVAL_SEC 60
#endif
// How long to wait for ACK (milliseconds)
#define ACK_TIMEOUT_MS 3000
// Maximum retries if no ACK received
#define MAX_RETRIES 3
// How long to listen for incoming packets after TX (milliseconds)
#define RX_WINDOW_MS 500
// ============================================================================
// LORA CONFIGURATION
// ============================================================================
// Frequency (US: 915 MHz, EU: 868 MHz, AS: 923 MHz)
#ifndef LORA_FREQUENCY
#define LORA_FREQUENCY 915.0
#endif
// Spreading factor (7-12, higher = longer range, slower)
#define LORA_SPREADING_FACTOR 9
// Bandwidth (kHz)
#define LORA_BANDWIDTH 125E3
// Coding rate (5-8)
#define LORA_CODING_RATE 7
// Sync word (must match gateway: 0x12)
#define LORA_SYNC_WORD 0x12
// TX power (dBm, max 20 for US)
#define LORA_TX_POWER 14
// ============================================================================
// BATTERY MONITORING
// ============================================================================
// Battery voltage thresholds (millivolts)
#define BATTERY_FULL_MV 4200 // Fully charged LiPo
#define BATTERY_NOMINAL_MV 3700 // Nominal LiPo
#define BATTERY_LOW_MV 3400 // Low battery warning threshold
#define BATTERY_CRITICAL_MV 3200 // Critical - send alert
// ADC calibration
// Voltage divider ratio (if using voltage divider for ADC)
// Set to 1.0 if reading directly (not recommended for LiPo)
#define BATTERY_DIVIDER_RATIO 2.0
// ============================================================================
// ALERT THRESHOLDS (can be overridden by gateway)
// ============================================================================
// Temperature thresholds (Celsius)
#define ALERT_TEMP_HIGH_C 35.0 // Too hot
#define ALERT_TEMP_LOW_C 10.0 // Too cold
// Humidity thresholds (percent)
#define ALERT_HUMIDITY_HIGH 85.0 // Too humid
#define ALERT_HUMIDITY_LOW 20.0 // Too dry
// IAQ threshold (BME680 only)
#define ALERT_IAQ_THRESHOLD 150 // Unhealthy air
// Duration before alert is sent (seconds)
#define ALERT_PERSIST_SEC 30
// ============================================================================
// DEEP SLEEP CONFIGURATION
// ============================================================================
// Use deep sleep between readings (recommended for battery)
#define ENABLE_DEEP_SLEEP 1
// Wake up sources
#define WAKE_ON_TIMER 1 // Wake on interval timer
#define WAKE_ON_GPIO 0 // Wake on external GPIO (for alerts)
#define WAKE_GPIO_PIN 0 // GPIO pin for external wake
// ============================================================================
// DEBUG CONFIGURATION
// ============================================================================
// Enable serial debug output (disable in production to save power)
#define ENABLE_SERIAL_DEBUG 1
// Enable OLED display (disable to save power if not needed)
#define ENABLE_OLED 1
// Blink LED on TX (for debugging)
#define ENABLE_TX_LED 1
#endif // CONFIG_H
+120
View File
@@ -0,0 +1,120 @@
; ZNET LoRa Remote Sensor Node
; Battery-powered sensor nodes for temperature/humidity/VOC monitoring
;
; Supported boards:
; - Heltec WiFi LoRa 32 V3
; - TTGO LoRa32 (SX1276)
; - DIY ESP32 + SX1262/SX1276 modules
[platformio]
default_envs = heltec_v3
[env]
platform = espressif32
framework = arduino
monitor_speed = 115200
upload_speed = 921600
; Common libraries for all variants
lib_deps =
sandeepmistry/LoRa@^0.8.0
adafruit/Adafruit Unified Sensor@^1.1.14
adafruit/DHT sensor library@^1.4.6
adafruit/Adafruit BME680 Library@^2.0.4
paulstoffregen/OneWire@^2.3.8
milesburton/DallasTemperature@^3.11.0
build_flags =
-DCORE_DEBUG_LEVEL=1
-DARDUINO_USB_CDC_ON_BOOT=1
; Common settings for battery optimization
board_build.partitions = min_spiffs.csv
; ============================================================================
; Heltec WiFi LoRa 32 V3 (Recommended - built-in OLED + SX1262)
; ============================================================================
[env:heltec_v3]
board = heltec_wifi_lora_32_V3
build_flags =
${env.build_flags}
-DLORA_BOARD_HELTEC_V3
-DLORA_FREQUENCY=915.0
; SX1262 pins for Heltec V3
-DLORA_SCK=9
-DLORA_MISO=11
-DLORA_MOSI=10
-DLORA_SS=8
-DLORA_RST=12
-DLORA_DIO1=14
-DLORA_BUSY=13
; OLED pins
-DOLED_SDA=17
-DOLED_SCL=18
-DOLED_RST=21
; Battery ADC
-DBATTERY_ADC_PIN=1
-DBATTERY_ADC_EN_PIN=37
lib_deps =
${env.lib_deps}
thingpulse/ESP8266 and ESP32 OLED driver for SSD1306 displays@^4.4.0
jgromes/RadioLib@^6.4.0
; ============================================================================
; TTGO LoRa32 V2.1 (SX1276)
; ============================================================================
[env:ttgo_lora32_v21]
board = ttgo-lora32-v21
build_flags =
${env.build_flags}
-DLORA_BOARD_TTGO_V21
-DLORA_FREQUENCY=915.0
; SX1276 pins for TTGO
-DLORA_SCK=5
-DLORA_MISO=19
-DLORA_MOSI=27
-DLORA_SS=18
-DLORA_RST=23
-DLORA_DIO0=26
; OLED pins
-DOLED_SDA=21
-DOLED_SCL=22
-DOLED_RST=16
; Battery ADC
-DBATTERY_ADC_PIN=35
lib_deps =
${env.lib_deps}
thingpulse/ESP8266 and ESP32 OLED driver for SSD1306 displays@^4.4.0
; ============================================================================
; Generic ESP32 + SX1276 (DIY builds)
; ============================================================================
[env:esp32_sx1276]
board = esp32dev
build_flags =
${env.build_flags}
-DLORA_BOARD_GENERIC
-DLORA_FREQUENCY=915.0
; Define your own pins in config.h
; ============================================================================
; DHT22 sensor variant (ambient temp/humidity)
; ============================================================================
[env:heltec_v3_dht22]
extends = env:heltec_v3
build_flags =
${env:heltec_v3.build_flags}
-DSENSOR_TYPE_DHT22
-DDHT_PIN=7
; ============================================================================
; BME680 sensor variant (temp/humidity/pressure/VOC)
; ============================================================================
[env:heltec_v3_bme680]
extends = env:heltec_v3
build_flags =
${env:heltec_v3.build_flags}
-DSENSOR_TYPE_BME680
-DBME680_I2C_ADDR=0x76
+690
View File
@@ -0,0 +1,690 @@
/**
* ZNET LoRa Remote Sensor Node
*
* Battery-powered sensor node that transmits readings to the ZNET gateway
* via LoRa. Supports DHT22, BME680, and DS18B20 sensors.
*
* Operation:
* 1. Wake from deep sleep
* 2. Read sensor(s)
* 3. Build and transmit LoRa packet
* 4. Wait for ACK (if requested)
* 5. Enter deep sleep until next interval
*
* Power consumption targets:
* - Active: ~80mA (during TX)
* - Sleep: ~10uA (ESP32 deep sleep)
* - Average: ~1mA at 60-second interval
*/
#include <Arduino.h>
#include <SPI.h>
#include "config.h"
// Include shared protocol headers from parent project
#include "../include/lora_protocol.h"
#include "../include/lora_packet.h"
// ============================================================================
// BOARD-SPECIFIC INCLUDES
// ============================================================================
#ifdef LORA_BOARD_HELTEC_V3
#include <RadioLib.h>
#include <SSD1306Wire.h>
SX1262 radio = new Module(LORA_SS, LORA_DIO1, LORA_RST, LORA_BUSY);
SSD1306Wire display(0x3C, OLED_SDA, OLED_SCL);
#define USE_RADIOLIB
#else
#include <LoRa.h>
#ifdef ENABLE_OLED
#include <SSD1306Wire.h>
SSD1306Wire display(0x3C, OLED_SDA, OLED_SCL);
#endif
#endif
// ============================================================================
// SENSOR INCLUDES
// ============================================================================
#ifdef SENSOR_TYPE_DHT22
#include <DHT.h>
DHT dht(DHT_PIN, DHT22);
#endif
#ifdef SENSOR_TYPE_BME680
#include <Wire.h>
#include <Adafruit_BME680.h>
Adafruit_BME680 bme;
#endif
#ifdef SENSOR_TYPE_DS18B20
#include <OneWire.h>
#include <DallasTemperature.h>
OneWire oneWire(ONEWIRE_PIN);
DallasTemperature ds18b20(&oneWire);
#endif
// ============================================================================
// GLOBALS
// ============================================================================
// RTC memory survives deep sleep
RTC_DATA_ATTR uint8_t sequence_number = 0;
RTC_DATA_ATTR uint16_t assigned_node_id = NODE_ID;
RTC_DATA_ATTR bool is_registered = false;
RTC_DATA_ATTR uint16_t configured_interval = REPORT_INTERVAL_SEC;
RTC_DATA_ATTR uint32_t boot_count = 0;
// Current sensor readings
float current_temp_c = 0;
float current_humidity = 0;
float current_pressure = 0;
uint16_t current_gas_kohm = 0;
uint8_t current_iaq = 0;
uint16_t battery_mv = 0;
int8_t last_rssi = 0;
bool sensor_valid = false;
// Flags
bool first_boot = false;
bool battery_low = false;
bool ack_received = false;
// ============================================================================
// FUNCTION DECLARATIONS
// ============================================================================
void setupLoRa();
void setupSensor();
void setupDisplay();
void readSensor();
void readBattery();
void sendReading();
void sendRegistration();
void waitForAck();
void processIncoming(uint8_t* data, uint8_t len);
void enterDeepSleep();
void updateDisplay();
void debugPrint(const char* msg);
void debugPrintf(const char* fmt, ...);
// ============================================================================
// SETUP
// ============================================================================
void setup() {
boot_count++;
first_boot = (boot_count == 1);
#if ENABLE_SERIAL_DEBUG
Serial.begin(115200);
delay(100);
debugPrintf("\n=== ZNET Remote Node v%d.%d ===", FW_VERSION_MAJOR, FW_VERSION_MINOR);
debugPrintf("Boot #%d, Node ID: 0x%04X", boot_count, assigned_node_id);
#endif
// Initialize display first for visual feedback
#if ENABLE_OLED
setupDisplay();
#endif
// Initialize LoRa radio
setupLoRa();
// Initialize sensor
setupSensor();
// Read battery voltage
readBattery();
// Check if we need to register
if (!is_registered || first_boot) {
sendRegistration();
}
// Read sensor
readSensor();
// Send reading
if (sensor_valid) {
sendReading();
} else {
debugPrint("Sensor read failed - not sending");
}
// Update display
#if ENABLE_OLED
updateDisplay();
delay(2000); // Show reading for 2 seconds
#endif
// Enter deep sleep
#if ENABLE_DEEP_SLEEP
enterDeepSleep();
#else
// If deep sleep disabled, wait for next interval
debugPrintf("Waiting %d seconds...", configured_interval);
delay(configured_interval * 1000);
ESP.restart();
#endif
}
void loop() {
// Should never reach here with deep sleep enabled
delay(1000);
}
// ============================================================================
// LORA SETUP
// ============================================================================
void setupLoRa() {
debugPrint("Initializing LoRa...");
#ifdef USE_RADIOLIB
// RadioLib for SX1262 (Heltec V3)
SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS);
int state = radio.begin(LORA_FREQUENCY, LORA_BANDWIDTH / 1000.0,
LORA_SPREADING_FACTOR, LORA_CODING_RATE,
LORA_SYNC_WORD, LORA_TX_POWER);
if (state == RADIOLIB_ERR_NONE) {
debugPrint("LoRa init success (SX1262)");
} else {
debugPrintf("LoRa init failed: %d", state);
// Continue anyway, maybe it will work
}
#else
// Classic LoRa library for SX1276
LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0);
if (!LoRa.begin(LORA_FREQUENCY * 1E6)) {
debugPrint("LoRa init failed!");
return;
}
LoRa.setSpreadingFactor(LORA_SPREADING_FACTOR);
LoRa.setSignalBandwidth(LORA_BANDWIDTH);
LoRa.setCodingRate4(LORA_CODING_RATE);
LoRa.setSyncWord(LORA_SYNC_WORD);
LoRa.setTxPower(LORA_TX_POWER);
debugPrint("LoRa init success (SX1276)");
#endif
}
// ============================================================================
// SENSOR SETUP
// ============================================================================
void setupSensor() {
debugPrint("Initializing sensor...");
#ifdef SENSOR_TYPE_DHT22
dht.begin();
debugPrint("DHT22 initialized");
#endif
#ifdef SENSOR_TYPE_BME680
Wire.begin();
if (!bme.begin(BME680_I2C_ADDR)) {
debugPrint("BME680 init failed!");
return;
}
// Configure BME680
bme.setTemperatureOversampling(BME680_OS_8X);
bme.setHumidityOversampling(BME680_OS_2X);
bme.setPressureOversampling(BME680_OS_4X);
bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
bme.setGasHeater(320, 150); // 320°C for 150ms
debugPrint("BME680 initialized");
#endif
#ifdef SENSOR_TYPE_DS18B20
ds18b20.begin();
ds18b20.setResolution(12); // 12-bit for max precision
debugPrintf("DS18B20 initialized, found %d device(s)", ds18b20.getDeviceCount());
#endif
}
// ============================================================================
// DISPLAY SETUP
// ============================================================================
#if ENABLE_OLED
void setupDisplay() {
#ifdef OLED_RST
pinMode(OLED_RST, OUTPUT);
digitalWrite(OLED_RST, LOW);
delay(20);
digitalWrite(OLED_RST, HIGH);
delay(20);
#endif
display.init();
display.flipScreenVertically();
display.setFont(ArialMT_Plain_10);
display.clear();
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.drawString(64, 20, "ZNET Remote");
display.drawString(64, 35, "Starting...");
display.display();
}
#endif
// ============================================================================
// READ SENSOR
// ============================================================================
void readSensor() {
debugPrint("Reading sensor...");
sensor_valid = false;
#ifdef SENSOR_TYPE_DHT22
// DHT22 needs time to stabilize
delay(2000);
float h = dht.readHumidity();
float t = dht.readTemperature(); // Celsius
if (isnan(h) || isnan(t)) {
debugPrint("DHT22 read failed!");
return;
}
current_temp_c = t;
current_humidity = h;
sensor_valid = true;
debugPrintf("DHT22: %.1f°C, %.1f%%", current_temp_c, current_humidity);
#endif
#ifdef SENSOR_TYPE_BME680
if (!bme.performReading()) {
debugPrint("BME680 read failed!");
return;
}
current_temp_c = bme.temperature;
current_humidity = bme.humidity;
current_pressure = bme.pressure / 100.0; // Convert to hPa
current_gas_kohm = bme.gas_resistance / 1000; // Convert to kOhm
// Simple IAQ calculation (proper calculation requires BSEC library)
// This is a rough approximation based on gas resistance
if (current_gas_kohm > 300) {
current_iaq = 50; // Excellent
} else if (current_gas_kohm > 200) {
current_iaq = 100; // Good
} else if (current_gas_kohm > 100) {
current_iaq = 150; // Moderate
} else if (current_gas_kohm > 50) {
current_iaq = 200; // Poor
} else {
current_iaq = 250; // Very poor
}
sensor_valid = true;
debugPrintf("BME680: %.1f°C, %.1f%%, %.1f hPa, %d kOhm, IAQ %d",
current_temp_c, current_humidity, current_pressure,
current_gas_kohm, current_iaq);
#endif
#ifdef SENSOR_TYPE_DS18B20
ds18b20.requestTemperatures();
delay(750); // Wait for 12-bit conversion
current_temp_c = ds18b20.getTempCByIndex(0);
if (current_temp_c == DEVICE_DISCONNECTED_C) {
debugPrint("DS18B20 read failed!");
return;
}
sensor_valid = true;
debugPrintf("DS18B20: %.2f°C", current_temp_c);
#endif
}
// ============================================================================
// READ BATTERY
// ============================================================================
void readBattery() {
#ifdef BATTERY_ADC_PIN
#ifdef BATTERY_ADC_EN_PIN
// Enable ADC (some boards have enable pin)
pinMode(BATTERY_ADC_EN_PIN, OUTPUT);
digitalWrite(BATTERY_ADC_EN_PIN, HIGH);
delay(10);
#endif
// Read ADC (12-bit, 0-4095)
uint32_t adc_raw = 0;
for (int i = 0; i < 10; i++) {
adc_raw += analogRead(BATTERY_ADC_PIN);
delay(5);
}
adc_raw /= 10;
// Convert to millivolts
// ESP32 ADC: 0-3.3V maps to 0-4095
// With voltage divider, multiply by ratio
float voltage = (adc_raw / 4095.0) * 3.3 * BATTERY_DIVIDER_RATIO;
battery_mv = (uint16_t)(voltage * 1000);
#ifdef BATTERY_ADC_EN_PIN
digitalWrite(BATTERY_ADC_EN_PIN, LOW);
#endif
// Check thresholds
battery_low = (battery_mv < BATTERY_LOW_MV);
debugPrintf("Battery: %d mV %s", battery_mv, battery_low ? "(LOW!)" : "");
#else
// No ADC configured, assume full battery
battery_mv = BATTERY_NOMINAL_MV;
battery_low = false;
#endif
}
// ============================================================================
// SEND READING
// ============================================================================
void sendReading() {
debugPrint("Building packet...");
lora_packet_t pkt;
#ifdef SENSOR_TYPE_DHT22
lora_packet_init(&pkt, LORA_PKT_SENSOR_DATA, assigned_node_id,
sequence_number++, true, battery_low, first_boot);
lora_packet_add_dht22(&pkt, current_temp_c, current_humidity,
battery_mv, last_rssi);
#endif
#ifdef SENSOR_TYPE_BME680
lora_packet_init(&pkt, LORA_PKT_SENSOR_DATA, assigned_node_id,
sequence_number++, true, battery_low, first_boot);
lora_packet_add_bme680(&pkt, current_temp_c, current_humidity,
current_pressure, current_gas_kohm, current_iaq,
battery_mv, last_rssi);
#endif
#ifdef SENSOR_TYPE_DS18B20
lora_packet_init(&pkt, LORA_PKT_SENSOR_DATA, assigned_node_id,
sequence_number++, true, battery_low, first_boot);
lora_packet_add_ds18b20(&pkt, current_temp_c, battery_mv, last_rssi);
#endif
lora_packet_finalize(&pkt);
debugPrintf("Sending %d bytes, seq %d...", pkt.length, sequence_number - 1);
// Transmit
#ifdef USE_RADIOLIB
int state = radio.transmit(pkt.data, pkt.length);
if (state == RADIOLIB_ERR_NONE) {
debugPrint("TX success");
} else {
debugPrintf("TX failed: %d", state);
}
#else
LoRa.beginPacket();
LoRa.write(pkt.data, pkt.length);
LoRa.endPacket();
debugPrint("TX complete");
#endif
// Wait for ACK
waitForAck();
}
// ============================================================================
// SEND REGISTRATION
// ============================================================================
void sendRegistration() {
debugPrint("Sending registration...");
lora_packet_t pkt;
lora_packet_init(&pkt, LORA_PKT_REGISTER, NODE_ID, sequence_number++,
true, battery_low, true);
#ifdef SENSOR_TYPE_DHT22
lora_packet_add_register(&pkt, NODE_ID, SENSOR_TYPE_DHT22,
FW_VERSION_MAJOR, FW_VERSION_MINOR, NODE_NAME);
#endif
#ifdef SENSOR_TYPE_BME680
lora_packet_add_register(&pkt, NODE_ID, SENSOR_TYPE_BME680,
FW_VERSION_MAJOR, FW_VERSION_MINOR, NODE_NAME);
#endif
#ifdef SENSOR_TYPE_DS18B20
lora_packet_add_register(&pkt, NODE_ID, SENSOR_TYPE_DS18B20,
FW_VERSION_MAJOR, FW_VERSION_MINOR, NODE_NAME);
#endif
lora_packet_finalize(&pkt);
// Transmit
#ifdef USE_RADIOLIB
radio.transmit(pkt.data, pkt.length);
#else
LoRa.beginPacket();
LoRa.write(pkt.data, pkt.length);
LoRa.endPacket();
#endif
// Wait for registration ACK
waitForAck();
}
// ============================================================================
// WAIT FOR ACK
// ============================================================================
void waitForAck() {
debugPrintf("Waiting for ACK (%d ms)...", ACK_TIMEOUT_MS);
unsigned long start = millis();
ack_received = false;
#ifdef USE_RADIOLIB
// RadioLib receive
uint8_t rxBuffer[LORA_MAX_PACKET_SIZE];
size_t rxLen = 0;
while (millis() - start < ACK_TIMEOUT_MS) {
int state = radio.receive(rxBuffer, rxLen);
if (state == RADIOLIB_ERR_NONE) {
last_rssi = radio.getRSSI();
processIncoming(rxBuffer, rxLen);
if (ack_received) break;
}
delay(10);
}
#else
// Classic LoRa receive
LoRa.receive();
while (millis() - start < ACK_TIMEOUT_MS) {
int packetSize = LoRa.parsePacket();
if (packetSize > 0) {
uint8_t rxBuffer[LORA_MAX_PACKET_SIZE];
int i = 0;
while (LoRa.available() && i < LORA_MAX_PACKET_SIZE) {
rxBuffer[i++] = LoRa.read();
}
last_rssi = LoRa.packetRssi();
processIncoming(rxBuffer, i);
if (ack_received) break;
}
delay(10);
}
#endif
if (ack_received) {
debugPrint("ACK received!");
} else {
debugPrint("ACK timeout");
}
}
// ============================================================================
// PROCESS INCOMING PACKET
// ============================================================================
void processIncoming(uint8_t* data, uint8_t len) {
lora_packet_t pkt;
if (!lora_packet_parse(&pkt, data, len)) {
debugPrint("Invalid packet received");
return;
}
debugPrintf("RX type=%d from 0x%04X seq=%d", pkt.type, pkt.node_id, pkt.sequence);
switch (pkt.type) {
case LORA_PKT_ACK:
if (pkt.node_id == assigned_node_id) {
ack_received = true;
}
break;
case LORA_PKT_REGISTER_ACK: {
const uint8_t* payload = lora_packet_payload(&pkt);
lora_register_ack_data_t ack_data;
if (lora_parse_register_ack(payload, lora_packet_payload_len(&pkt), &ack_data)) {
if (ack_data.status == 0) {
assigned_node_id = ack_data.assigned_id;
configured_interval = ack_data.interval_sec;
is_registered = true;
ack_received = true;
debugPrintf("Registered as 0x%04X, interval %ds",
assigned_node_id, configured_interval);
}
}
break;
}
case LORA_PKT_CONFIG_RESP: {
// Update configuration from gateway
const uint8_t* payload = lora_packet_payload(&pkt);
if (lora_packet_payload_len(&pkt) >= 2) {
uint16_t new_interval = lora_read_be16(payload);
if (new_interval > 0 && new_interval != configured_interval) {
configured_interval = new_interval;
debugPrintf("Interval updated to %d s", configured_interval);
}
}
ack_received = true;
break;
}
default:
break;
}
}
// ============================================================================
// ENTER DEEP SLEEP
// ============================================================================
void enterDeepSleep() {
debugPrintf("Entering deep sleep for %d seconds...", configured_interval);
#if ENABLE_OLED
display.displayOff();
#endif
// Configure wake timer
esp_sleep_enable_timer_wakeup(configured_interval * 1000000ULL);
#if WAKE_ON_GPIO
esp_sleep_enable_ext0_wakeup((gpio_num_t)WAKE_GPIO_PIN, 1);
#endif
// Enter deep sleep
esp_deep_sleep_start();
}
// ============================================================================
// UPDATE DISPLAY
// ============================================================================
#if ENABLE_OLED
void updateDisplay() {
display.clear();
// Title
display.setTextAlignment(TEXT_ALIGN_LEFT);
display.setFont(ArialMT_Plain_10);
display.drawString(0, 0, "ZNET Remote");
// Node ID
display.setTextAlignment(TEXT_ALIGN_RIGHT);
char buf[16];
snprintf(buf, sizeof(buf), "0x%04X", assigned_node_id);
display.drawString(128, 0, buf);
// Line
display.drawLine(0, 12, 128, 12);
// Temperature
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.setFont(ArialMT_Plain_16);
if (sensor_valid) {
float temp_f = lora_c_to_f(current_temp_c);
snprintf(buf, sizeof(buf), "%.1f F", temp_f);
display.drawString(64, 16, buf);
} else {
display.drawString(64, 16, "---");
}
// Humidity (if available)
#if defined(SENSOR_TYPE_DHT22) || defined(SENSOR_TYPE_BME680)
display.setFont(ArialMT_Plain_10);
if (sensor_valid) {
snprintf(buf, sizeof(buf), "%.0f%% RH", current_humidity);
display.drawString(64, 36, buf);
}
#endif
// Battery
display.setTextAlignment(TEXT_ALIGN_LEFT);
snprintf(buf, sizeof(buf), "%dmV", battery_mv);
display.drawString(0, 54, buf);
// RSSI
display.setTextAlignment(TEXT_ALIGN_RIGHT);
snprintf(buf, sizeof(buf), "%ddBm", last_rssi);
display.drawString(128, 54, buf);
display.display();
}
#endif
// ============================================================================
// DEBUG HELPERS
// ============================================================================
void debugPrint(const char* msg) {
#if ENABLE_SERIAL_DEBUG
Serial.println(msg);
#endif
}
void debugPrintf(const char* fmt, ...) {
#if ENABLE_SERIAL_DEBUG
char buf[128];
va_list args;
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
Serial.println(buf);
#endif
}