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
+488
-25
@@ -3,19 +3,21 @@
|
||||
*
|
||||
* Reads DS18B20 temperature sensors in the e-coat paint tank and sends
|
||||
* data to ZNET Web for real-time monitoring and ML feature collection.
|
||||
* Also receives data from remote LoRa sensor nodes (DHT22, BME680).
|
||||
*
|
||||
* Hardware:
|
||||
* - Meshnology ESP32 LoRa V3 (Heltec-compatible)
|
||||
* - 2x DS18B20 waterproof temperature probes (1-Wire)
|
||||
* - Built-in 0.96" OLED display
|
||||
* - Built-in SX1262 LoRa radio (for future sensor nodes)
|
||||
* - Built-in SX1262 LoRa radio
|
||||
*
|
||||
* Features:
|
||||
* - Dual sensor reading with validation and averaging
|
||||
* - OLED display for local temperature visibility
|
||||
* - HTTP POST to ZNET Web API
|
||||
* - Offline buffering when network is unavailable
|
||||
* - LoRa receive for future remote sensor nodes
|
||||
* - LoRa receive from remote sensor nodes
|
||||
* - Automatic node registration and ACK responses
|
||||
* - WiFiManager for easy WiFi setup
|
||||
*/
|
||||
|
||||
@@ -27,13 +29,41 @@
|
||||
#include <DallasTemperature.h>
|
||||
#include <SSD1306Wire.h>
|
||||
#include <WiFiManager.h>
|
||||
#include <RadioLib.h>
|
||||
#include <SPI.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "lora_protocol.h"
|
||||
#include "lora_packet.h"
|
||||
|
||||
// ============================================================================
|
||||
// GLOBALS
|
||||
// ============================================================================
|
||||
|
||||
// LoRa radio (SX1262)
|
||||
SX1262 radio = new Module(LORA_SS, LORA_DIO1, LORA_RST, LORA_BUSY);
|
||||
volatile bool loraReceived = false;
|
||||
int8_t lastLoraRssi = 0;
|
||||
|
||||
// Remote node registry
|
||||
#define MAX_REMOTE_NODES 16
|
||||
struct RemoteNode {
|
||||
uint16_t node_id;
|
||||
uint8_t sensor_type;
|
||||
char name[9];
|
||||
uint8_t last_seq;
|
||||
uint32_t last_seen;
|
||||
uint16_t battery_mv;
|
||||
bool active;
|
||||
};
|
||||
RemoteNode remoteNodes[MAX_REMOTE_NODES];
|
||||
int remoteNodeCount = 0;
|
||||
|
||||
// LoRa statistics
|
||||
uint32_t loraPacketsReceived = 0;
|
||||
uint32_t loraPacketsForwarded = 0;
|
||||
uint32_t loraPacketsCrcFail = 0;
|
||||
|
||||
// OneWire and DS18B20
|
||||
OneWire oneWire(ONEWIRE_PIN);
|
||||
DallasTemperature sensors(&oneWire);
|
||||
@@ -79,16 +109,35 @@ int bufferCount = 0;
|
||||
void setupWiFi();
|
||||
void setupSensors();
|
||||
void setupDisplay();
|
||||
void setupLoRa();
|
||||
void readTemperatures();
|
||||
void updateDisplay();
|
||||
void postToApi();
|
||||
void postLoRaReadingToApi(const lora_packet_t* pkt, const char* sensorType,
|
||||
float temp_f, float humidity, float pressure,
|
||||
uint16_t gas_kohm, uint8_t iaq, uint16_t battery_mv,
|
||||
int8_t rssi);
|
||||
void sendBufferedReadings();
|
||||
void checkLoRa();
|
||||
void processLoRaPacket(uint8_t* data, uint8_t len);
|
||||
void handleSensorData(const lora_packet_t* pkt);
|
||||
void handleRegistration(const lora_packet_t* pkt);
|
||||
void sendAck(uint16_t node_id, uint8_t seq);
|
||||
void sendRegistrationAck(uint16_t node_id, uint8_t seq, uint16_t assigned_id);
|
||||
RemoteNode* findNode(uint16_t node_id);
|
||||
RemoteNode* addNode(uint16_t node_id, uint8_t sensor_type, const char* name);
|
||||
uint16_t allocateNodeId(uint8_t sensor_type);
|
||||
float celsiusToFahrenheit(float celsius);
|
||||
bool isValidTemperature(float temp_f);
|
||||
String formatAddress(DeviceAddress addr);
|
||||
void displaySplash();
|
||||
void displayError(const char* message);
|
||||
|
||||
// LoRa interrupt handler
|
||||
ICACHE_RAM_ATTR void onLoRaReceive() {
|
||||
loraReceived = true;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SETUP
|
||||
// ============================================================================
|
||||
@@ -99,15 +148,21 @@ void setup() {
|
||||
|
||||
Serial.println("\n\n");
|
||||
Serial.println("=========================================");
|
||||
Serial.println(" ZNET Temperature Sensor v1.0");
|
||||
Serial.println(" E-Coat Paint Tank Monitor");
|
||||
Serial.println(" ZNET Temperature Sensor v1.2");
|
||||
Serial.println(" E-Coat Paint Tank Gateway");
|
||||
Serial.println("=========================================");
|
||||
|
||||
// Initialize node registry
|
||||
memset(remoteNodes, 0, sizeof(remoteNodes));
|
||||
|
||||
// Initialize display first for visual feedback
|
||||
setupDisplay();
|
||||
displaySplash();
|
||||
|
||||
// Initialize sensors
|
||||
// Initialize LoRa radio
|
||||
setupLoRa();
|
||||
|
||||
// Initialize local sensors
|
||||
setupSensors();
|
||||
|
||||
// Connect to WiFi
|
||||
@@ -126,7 +181,10 @@ void loop() {
|
||||
// Check WiFi connection
|
||||
wifiConnected = WiFi.status() == WL_CONNECTED;
|
||||
|
||||
// Read temperatures at interval
|
||||
// Check for LoRa packets (non-blocking)
|
||||
checkLoRa();
|
||||
|
||||
// Read local temperatures at interval
|
||||
if (now - lastTempRead >= TEMP_READ_INTERVAL_MS || lastTempRead == 0) {
|
||||
readTemperatures();
|
||||
lastTempRead = now;
|
||||
@@ -313,11 +371,13 @@ void updateDisplay() {
|
||||
// Title bar
|
||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display.setFont(ArialMT_Plain_10);
|
||||
display.drawString(0, 0, "ZNET Tank Temp");
|
||||
display.drawString(0, 0, "ZNET Gateway");
|
||||
|
||||
// WiFi status indicator
|
||||
// WiFi and LoRa status
|
||||
display.setTextAlignment(TEXT_ALIGN_RIGHT);
|
||||
display.drawString(128, 0, wifiConnected ? "WiFi OK" : "NO WiFi");
|
||||
char status[24];
|
||||
snprintf(status, sizeof(status), "%s L%d", wifiConnected ? "W" : "-", remoteNodeCount);
|
||||
display.drawString(128, 0, status);
|
||||
|
||||
// Horizontal line
|
||||
display.drawLine(0, 12, 128, 12);
|
||||
@@ -329,32 +389,39 @@ void updateDisplay() {
|
||||
if (sensor1Valid) {
|
||||
char buf[20];
|
||||
snprintf(buf, sizeof(buf), "%.1f°F", temperature1_f);
|
||||
display.drawString(64, 18, buf);
|
||||
display.drawString(64, 16, buf);
|
||||
} else {
|
||||
display.drawString(64, 18, "---");
|
||||
display.drawString(64, 16, "---");
|
||||
}
|
||||
|
||||
display.setFont(ArialMT_Plain_10);
|
||||
display.drawString(64, 36, "Primary");
|
||||
display.drawString(64, 34, "Tank Primary");
|
||||
|
||||
// Secondary sensor (smaller, below)
|
||||
if (sensorCount >= 2) {
|
||||
display.setFont(ArialMT_Plain_10);
|
||||
if (sensor2Valid) {
|
||||
char buf[20];
|
||||
snprintf(buf, sizeof(buf), "Backup: %.1f°F", temperature2_f);
|
||||
display.drawString(64, 50, buf);
|
||||
} else {
|
||||
display.drawString(64, 50, "Backup: ---");
|
||||
}
|
||||
if (sensorCount >= 2 && sensor2Valid) {
|
||||
char buf[20];
|
||||
snprintf(buf, sizeof(buf), "Backup: %.1f°F", temperature2_f);
|
||||
display.drawString(64, 46, buf);
|
||||
}
|
||||
|
||||
// Buffer indicator (if offline)
|
||||
// Status line at bottom
|
||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
char buf[32];
|
||||
|
||||
if (bufferCount > 0) {
|
||||
display.setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
char buf[20];
|
||||
snprintf(buf, sizeof(buf), "Buf:%d", bufferCount);
|
||||
display.drawString(0, 54, buf);
|
||||
} else if (loraPacketsReceived > 0) {
|
||||
snprintf(buf, sizeof(buf), "RX:%lu", loraPacketsReceived);
|
||||
} else {
|
||||
buf[0] = '\0';
|
||||
}
|
||||
display.drawString(0, 54, buf);
|
||||
|
||||
// RSSI of last LoRa packet
|
||||
if (loraPacketsReceived > 0) {
|
||||
display.setTextAlignment(TEXT_ALIGN_RIGHT);
|
||||
snprintf(buf, sizeof(buf), "%ddBm", lastLoraRssi);
|
||||
display.drawString(128, 54, buf);
|
||||
}
|
||||
|
||||
display.display();
|
||||
@@ -438,6 +505,402 @@ void sendBufferedReadings() {
|
||||
bufferHead = 0;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LORA SETUP AND RECEIVE
|
||||
// ============================================================================
|
||||
|
||||
void setupLoRa() {
|
||||
Serial.println("Initializing LoRa radio (SX1262)...");
|
||||
|
||||
// Initialize SPI for LoRa
|
||||
SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS);
|
||||
|
||||
// Initialize radio
|
||||
// RadioLib SX1262.begin(freq, bw_kHz, sf, cr, syncWord, power, preambleLen, tcxoVoltage)
|
||||
int state = radio.begin(
|
||||
LORA_FREQUENCY,
|
||||
LORA_BANDWIDTH / 1000.0, // Convert Hz to kHz
|
||||
LORA_SPREADING_FACTOR,
|
||||
LORA_CODING_RATE,
|
||||
LORA_SYNC_WORD,
|
||||
14, // TX power (dBm)
|
||||
8, // Preamble length
|
||||
0 // TCXO voltage (0 = use default)
|
||||
);
|
||||
|
||||
if (state == RADIOLIB_ERR_NONE) {
|
||||
Serial.println("LoRa init success!");
|
||||
} else {
|
||||
Serial.printf("LoRa init failed, code %d\n", state);
|
||||
displayError("LoRa init failed!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up receive interrupt
|
||||
radio.setDio1Action(onLoRaReceive);
|
||||
|
||||
// Start receiving
|
||||
state = radio.startReceive();
|
||||
if (state != RADIOLIB_ERR_NONE) {
|
||||
Serial.printf("LoRa receive start failed: %d\n", state);
|
||||
} else {
|
||||
Serial.println("LoRa listening on 915 MHz...");
|
||||
}
|
||||
}
|
||||
|
||||
void checkLoRa() {
|
||||
// Check if packet received via interrupt
|
||||
if (!loraReceived) return;
|
||||
|
||||
loraReceived = false;
|
||||
|
||||
// Read packet
|
||||
uint8_t rxBuffer[LORA_MAX_PACKET_SIZE];
|
||||
size_t rxLen = 0;
|
||||
|
||||
int state = radio.readData(rxBuffer, rxLen);
|
||||
|
||||
if (state == RADIOLIB_ERR_NONE) {
|
||||
lastLoraRssi = radio.getRSSI();
|
||||
loraPacketsReceived++;
|
||||
|
||||
Serial.printf("LoRa RX: %d bytes, RSSI %d dBm\n", rxLen, lastLoraRssi);
|
||||
|
||||
// Process the packet
|
||||
processLoRaPacket(rxBuffer, rxLen);
|
||||
} else if (state == RADIOLIB_ERR_CRC_MISMATCH) {
|
||||
loraPacketsCrcFail++;
|
||||
Serial.println("LoRa CRC error");
|
||||
} else {
|
||||
Serial.printf("LoRa RX error: %d\n", state);
|
||||
}
|
||||
|
||||
// Restart receive mode
|
||||
radio.startReceive();
|
||||
}
|
||||
|
||||
void processLoRaPacket(uint8_t* data, uint8_t len) {
|
||||
lora_packet_t pkt;
|
||||
|
||||
if (!lora_packet_parse(&pkt, data, len)) {
|
||||
Serial.println("Invalid packet format");
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("Packet from 0x%04X, type %d, seq %d\n",
|
||||
pkt.node_id, pkt.type, pkt.sequence);
|
||||
|
||||
switch (pkt.type) {
|
||||
case LORA_PKT_SENSOR_DATA:
|
||||
handleSensorData(&pkt);
|
||||
break;
|
||||
|
||||
case LORA_PKT_REGISTER:
|
||||
handleRegistration(&pkt);
|
||||
break;
|
||||
|
||||
case LORA_PKT_ALERT:
|
||||
// TODO: Handle alerts with higher priority
|
||||
handleSensorData(&pkt); // For now, treat like sensor data
|
||||
break;
|
||||
|
||||
case LORA_PKT_PONG:
|
||||
Serial.printf("PONG from 0x%04X\n", pkt.node_id);
|
||||
break;
|
||||
|
||||
default:
|
||||
Serial.printf("Unknown packet type: %d\n", pkt.type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void handleSensorData(const lora_packet_t* pkt) {
|
||||
const uint8_t* payload = lora_packet_payload(pkt);
|
||||
uint8_t payload_len = lora_packet_payload_len(pkt);
|
||||
|
||||
if (payload_len < 1) {
|
||||
Serial.println("Empty payload");
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t sensor_type = payload[0];
|
||||
|
||||
// Update node registry
|
||||
RemoteNode* node = findNode(pkt->node_id);
|
||||
if (node) {
|
||||
node->last_seen = millis();
|
||||
node->last_seq = pkt->sequence;
|
||||
}
|
||||
|
||||
// Parse based on sensor type and forward to API
|
||||
switch (sensor_type) {
|
||||
case SENSOR_TYPE_DHT22: {
|
||||
lora_dht22_data_t data;
|
||||
if (lora_parse_dht22(payload, payload_len, &data)) {
|
||||
float temp_f = celsiusToFahrenheit(data.temp_c);
|
||||
Serial.printf("DHT22 from 0x%04X: %.1f°F, %.1f%% RH\n",
|
||||
pkt->node_id, temp_f, data.humidity_pct);
|
||||
|
||||
if (node) node->battery_mv = data.battery_mv;
|
||||
|
||||
postLoRaReadingToApi(pkt, "dht22", temp_f, data.humidity_pct,
|
||||
0, 0, 0, data.battery_mv, data.rssi_dbm);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SENSOR_TYPE_BME680: {
|
||||
lora_bme680_data_t data;
|
||||
if (lora_parse_bme680(payload, payload_len, &data)) {
|
||||
float temp_f = celsiusToFahrenheit(data.temp_c);
|
||||
Serial.printf("BME680 from 0x%04X: %.1f°F, %.1f%%, %.0f hPa, IAQ %d\n",
|
||||
pkt->node_id, temp_f, data.humidity_pct,
|
||||
data.pressure_hpa, data.iaq_index);
|
||||
|
||||
if (node) node->battery_mv = data.battery_mv;
|
||||
|
||||
postLoRaReadingToApi(pkt, "bme680", temp_f, data.humidity_pct,
|
||||
data.pressure_hpa, data.gas_kohm,
|
||||
data.iaq_index, data.battery_mv, data.rssi_dbm);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case SENSOR_TYPE_DS18B20: {
|
||||
lora_ds18b20_data_t data;
|
||||
if (lora_parse_ds18b20(payload, payload_len, &data)) {
|
||||
float temp_f = celsiusToFahrenheit(data.temp_c);
|
||||
Serial.printf("DS18B20 from 0x%04X: %.2f°F\n",
|
||||
pkt->node_id, temp_f);
|
||||
|
||||
if (node) node->battery_mv = data.battery_mv;
|
||||
|
||||
postLoRaReadingToApi(pkt, "ds18b20", temp_f, 0, 0, 0, 0,
|
||||
data.battery_mv, data.rssi_dbm);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
Serial.printf("Unknown sensor type: 0x%02X\n", sensor_type);
|
||||
break;
|
||||
}
|
||||
|
||||
// Send ACK if requested
|
||||
if (pkt->ack_requested) {
|
||||
sendAck(pkt->node_id, pkt->sequence);
|
||||
}
|
||||
}
|
||||
|
||||
void handleRegistration(const lora_packet_t* pkt) {
|
||||
const uint8_t* payload = lora_packet_payload(pkt);
|
||||
uint8_t payload_len = lora_packet_payload_len(pkt);
|
||||
|
||||
lora_register_data_t reg;
|
||||
if (!lora_parse_register(payload, payload_len, ®)) {
|
||||
Serial.println("Invalid registration packet");
|
||||
return;
|
||||
}
|
||||
|
||||
Serial.printf("Registration from 0x%04X: '%s', type %d, FW %d.%d\n",
|
||||
pkt->node_id, reg.node_name, reg.sensor_type,
|
||||
reg.fw_major, reg.fw_minor);
|
||||
|
||||
// Check if node already registered
|
||||
RemoteNode* existing = findNode(pkt->node_id);
|
||||
if (existing && pkt->node_id != NODE_ID_AUTO_REQUEST) {
|
||||
// Re-registration, update info
|
||||
existing->last_seen = millis();
|
||||
strncpy(existing->name, reg.node_name, 8);
|
||||
existing->name[8] = '\0';
|
||||
|
||||
sendRegistrationAck(pkt->node_id, pkt->sequence, pkt->node_id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Allocate new node ID if requested
|
||||
uint16_t assigned_id = reg.proposed_id;
|
||||
if (assigned_id == NODE_ID_AUTO_REQUEST) {
|
||||
assigned_id = allocateNodeId(reg.sensor_type);
|
||||
Serial.printf("Auto-assigned ID: 0x%04X\n", assigned_id);
|
||||
}
|
||||
|
||||
// Add to registry
|
||||
RemoteNode* node = addNode(assigned_id, reg.sensor_type, reg.node_name);
|
||||
if (node) {
|
||||
sendRegistrationAck(pkt->node_id, pkt->sequence, assigned_id);
|
||||
Serial.printf("Node 0x%04X registered as '%s'\n", assigned_id, node->name);
|
||||
} else {
|
||||
Serial.println("Node registry full!");
|
||||
// TODO: Send NAK
|
||||
}
|
||||
}
|
||||
|
||||
void postLoRaReadingToApi(const lora_packet_t* pkt, const char* sensorType,
|
||||
float temp_f, float humidity, float pressure,
|
||||
uint16_t gas_kohm, uint8_t iaq, uint16_t battery_mv,
|
||||
int8_t rssi) {
|
||||
if (!wifiConnected) {
|
||||
Serial.println("No WiFi - can't forward LoRa reading");
|
||||
// TODO: Buffer LoRa readings
|
||||
return;
|
||||
}
|
||||
|
||||
HTTPClient http;
|
||||
http.begin(ZNET_API_URL);
|
||||
http.addHeader("Content-Type", "application/json");
|
||||
http.setTimeout(HTTP_TIMEOUT_MS);
|
||||
|
||||
// Build JSON payload
|
||||
JsonDocument doc;
|
||||
doc["device_id"] = DEVICE_ID;
|
||||
doc["device_name"] = DEVICE_NAME;
|
||||
|
||||
JsonArray readings = doc["readings"].to<JsonArray>();
|
||||
JsonObject r = readings.add<JsonObject>();
|
||||
|
||||
// Sensor ID includes LoRa prefix and node ID
|
||||
char sensor_id[32];
|
||||
snprintf(sensor_id, sizeof(sensor_id), "lora_0x%04X", pkt->node_id);
|
||||
|
||||
r["sensor_id"] = sensor_id;
|
||||
r["sensor_type"] = sensorType;
|
||||
r["temperature_f"] = round(temp_f * 100) / 100.0;
|
||||
r["is_valid"] = true;
|
||||
r["rssi_dbm"] = rssi;
|
||||
r["battery_mv"] = battery_mv;
|
||||
|
||||
// Add optional fields based on sensor type
|
||||
if (humidity > 0) {
|
||||
r["humidity_pct"] = round(humidity * 10) / 10.0;
|
||||
}
|
||||
if (pressure > 0) {
|
||||
r["pressure_hpa"] = round(pressure * 10) / 10.0;
|
||||
}
|
||||
if (gas_kohm > 0) {
|
||||
r["gas_resistance_kohm"] = gas_kohm;
|
||||
}
|
||||
if (iaq > 0) {
|
||||
r["iaq_index"] = iaq;
|
||||
}
|
||||
|
||||
String jsonString;
|
||||
serializeJson(doc, jsonString);
|
||||
|
||||
int httpCode = http.POST(jsonString);
|
||||
|
||||
if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_CREATED) {
|
||||
loraPacketsForwarded++;
|
||||
Serial.printf("LoRa reading forwarded (0x%04X)\n", pkt->node_id);
|
||||
} else {
|
||||
Serial.printf("LoRa forward failed: %d\n", httpCode);
|
||||
}
|
||||
|
||||
http.end();
|
||||
}
|
||||
|
||||
void sendAck(uint16_t node_id, uint8_t seq) {
|
||||
lora_packet_t ack;
|
||||
lora_packet_init(&ack, LORA_PKT_ACK, node_id, seq, false, false, false);
|
||||
lora_packet_finalize(&ack);
|
||||
|
||||
int state = radio.transmit(ack.data, ack.length);
|
||||
if (state == RADIOLIB_ERR_NONE) {
|
||||
Serial.printf("ACK sent to 0x%04X seq %d\n", node_id, seq);
|
||||
} else {
|
||||
Serial.printf("ACK send failed: %d\n", state);
|
||||
}
|
||||
|
||||
// Return to receive mode
|
||||
radio.startReceive();
|
||||
}
|
||||
|
||||
void sendRegistrationAck(uint16_t node_id, uint8_t seq, uint16_t assigned_id) {
|
||||
lora_packet_t ack;
|
||||
lora_packet_init(&ack, LORA_PKT_REGISTER_ACK, node_id, seq, false, false, false);
|
||||
lora_packet_add_register_ack(&ack, assigned_id, 0, TEMP_READ_INTERVAL_MS / 1000);
|
||||
lora_packet_finalize(&ack);
|
||||
|
||||
int state = radio.transmit(ack.data, ack.length);
|
||||
if (state == RADIOLIB_ERR_NONE) {
|
||||
Serial.printf("Registration ACK sent to 0x%04X -> 0x%04X\n", node_id, assigned_id);
|
||||
} else {
|
||||
Serial.printf("Registration ACK send failed: %d\n", state);
|
||||
}
|
||||
|
||||
// Return to receive mode
|
||||
radio.startReceive();
|
||||
}
|
||||
|
||||
RemoteNode* findNode(uint16_t node_id) {
|
||||
for (int i = 0; i < remoteNodeCount; i++) {
|
||||
if (remoteNodes[i].active && remoteNodes[i].node_id == node_id) {
|
||||
return &remoteNodes[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RemoteNode* addNode(uint16_t node_id, uint8_t sensor_type, const char* name) {
|
||||
// Find empty slot
|
||||
RemoteNode* node = nullptr;
|
||||
for (int i = 0; i < MAX_REMOTE_NODES; i++) {
|
||||
if (!remoteNodes[i].active) {
|
||||
node = &remoteNodes[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!node) return nullptr;
|
||||
|
||||
node->node_id = node_id;
|
||||
node->sensor_type = sensor_type;
|
||||
strncpy(node->name, name, 8);
|
||||
node->name[8] = '\0';
|
||||
node->last_seq = 0;
|
||||
node->last_seen = millis();
|
||||
node->battery_mv = 0;
|
||||
node->active = true;
|
||||
|
||||
remoteNodeCount++;
|
||||
return node;
|
||||
}
|
||||
|
||||
uint16_t allocateNodeId(uint8_t sensor_type) {
|
||||
// Determine ID range based on sensor type
|
||||
uint16_t base_id;
|
||||
switch (sensor_type) {
|
||||
case SENSOR_TYPE_DHT22:
|
||||
base_id = NODE_ID_AMBIENT_START;
|
||||
break;
|
||||
case SENSOR_TYPE_BME680:
|
||||
base_id = NODE_ID_ENVIRO_START;
|
||||
break;
|
||||
case SENSOR_TYPE_DS18B20:
|
||||
base_id = NODE_ID_TANK_START;
|
||||
break;
|
||||
default:
|
||||
base_id = NODE_ID_AUTO_START;
|
||||
break;
|
||||
}
|
||||
|
||||
// Find next available ID in range
|
||||
for (uint16_t id = base_id; id < base_id + 0xFF; id++) {
|
||||
if (!findNode(id)) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to auto range
|
||||
for (uint16_t id = NODE_ID_AUTO_START; id < NODE_ID_AUTO_END; id++) {
|
||||
if (!findNode(id)) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
return 0xFFFE; // Last resort
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// UTILITY FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user