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:
co-authored by
Claude Opus 4.5
parent
265a53768e
commit
5554341b49
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user