- 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>
440 lines
13 KiB
C
440 lines
13 KiB
C
/**
|
|
* ZNET LoRa Packet Builder and Parser
|
|
*
|
|
* High-level functions for building and parsing LoRa packets.
|
|
* Use these in both gateway and remote node firmware.
|
|
*/
|
|
|
|
#ifndef LORA_PACKET_H
|
|
#define LORA_PACKET_H
|
|
|
|
#include "lora_protocol.h"
|
|
#include <string.h>
|
|
|
|
// ============================================================================
|
|
// PACKET BUFFER
|
|
// ============================================================================
|
|
|
|
typedef struct {
|
|
uint8_t data[LORA_MAX_PACKET_SIZE];
|
|
uint8_t length;
|
|
uint16_t node_id;
|
|
uint8_t sequence;
|
|
lora_packet_type_t type;
|
|
bool ack_requested;
|
|
bool battery_low;
|
|
bool first_boot;
|
|
bool valid;
|
|
} lora_packet_t;
|
|
|
|
// ============================================================================
|
|
// PACKET BUILDING
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Initialize packet buffer with header
|
|
*
|
|
* @param pkt Packet buffer to initialize
|
|
* @param type Packet type
|
|
* @param node_id Source/destination node ID
|
|
* @param seq Sequence number
|
|
* @param ack_req Request acknowledgment
|
|
* @param bat_low Battery low flag
|
|
* @param first First boot flag
|
|
*/
|
|
static inline void lora_packet_init(lora_packet_t* pkt, lora_packet_type_t type,
|
|
uint16_t node_id, uint8_t seq,
|
|
bool ack_req, bool bat_low, bool first) {
|
|
pkt->data[0] = LORA_SYNC_BYTE_1;
|
|
pkt->data[1] = LORA_SYNC_BYTE_2;
|
|
pkt->data[2] = lora_make_header(type, ack_req, bat_low, first);
|
|
lora_write_be16(&pkt->data[3], node_id);
|
|
pkt->data[5] = seq;
|
|
pkt->length = 6; // Header without payload or CRC
|
|
|
|
pkt->node_id = node_id;
|
|
pkt->sequence = seq;
|
|
pkt->type = type;
|
|
pkt->ack_requested = ack_req;
|
|
pkt->battery_low = bat_low;
|
|
pkt->first_boot = first;
|
|
pkt->valid = true;
|
|
}
|
|
|
|
/**
|
|
* Add DHT22 payload to packet
|
|
*/
|
|
static inline void lora_packet_add_dht22(lora_packet_t* pkt, float temp_c,
|
|
float humidity_pct, uint16_t battery_mv,
|
|
int8_t rssi_dbm) {
|
|
uint8_t* payload = &pkt->data[pkt->length];
|
|
|
|
payload[0] = SENSOR_TYPE_DHT22;
|
|
lora_write_be16s(&payload[1], lora_encode_temp_c10(temp_c));
|
|
lora_write_be16(&payload[3], lora_encode_humidity(humidity_pct));
|
|
payload[5] = lora_encode_battery(battery_mv);
|
|
payload[6] = (uint8_t)rssi_dbm;
|
|
|
|
pkt->length += 7;
|
|
}
|
|
|
|
/**
|
|
* Add BME680 payload to packet
|
|
*/
|
|
static inline void lora_packet_add_bme680(lora_packet_t* pkt, float temp_c,
|
|
float humidity_pct, float pressure_hpa,
|
|
uint16_t gas_kohm, uint8_t iaq_index,
|
|
uint16_t battery_mv, int8_t rssi_dbm) {
|
|
uint8_t* payload = &pkt->data[pkt->length];
|
|
|
|
payload[0] = SENSOR_TYPE_BME680;
|
|
lora_write_be16s(&payload[1], lora_encode_temp_c10(temp_c));
|
|
lora_write_be16(&payload[3], lora_encode_humidity(humidity_pct));
|
|
|
|
// Pressure offset from 900 hPa
|
|
uint16_t press_offset = (uint16_t)(pressure_hpa - 900.0f);
|
|
if (pressure_hpa < 900.0f) press_offset = 0;
|
|
lora_write_be16(&payload[5], press_offset);
|
|
|
|
lora_write_be16(&payload[7], gas_kohm);
|
|
payload[9] = iaq_index;
|
|
payload[10] = lora_encode_battery(battery_mv);
|
|
payload[11] = (uint8_t)rssi_dbm;
|
|
|
|
pkt->length += 12;
|
|
}
|
|
|
|
/**
|
|
* Add DS18B20 payload to packet (high precision)
|
|
*/
|
|
static inline void lora_packet_add_ds18b20(lora_packet_t* pkt, float temp_c,
|
|
uint16_t battery_mv, int8_t rssi_dbm) {
|
|
uint8_t* payload = &pkt->data[pkt->length];
|
|
|
|
payload[0] = SENSOR_TYPE_DS18B20;
|
|
lora_write_be16s(&payload[1], lora_encode_temp_c100(temp_c));
|
|
payload[3] = lora_encode_battery(battery_mv);
|
|
payload[4] = (uint8_t)rssi_dbm;
|
|
|
|
pkt->length += 5;
|
|
}
|
|
|
|
/**
|
|
* Add alert payload to packet
|
|
*/
|
|
static inline void lora_packet_add_alert(lora_packet_t* pkt, lora_alert_type_t type,
|
|
int16_t value, int16_t threshold,
|
|
uint8_t duration_sec) {
|
|
uint8_t* payload = &pkt->data[pkt->length];
|
|
|
|
payload[0] = type;
|
|
lora_write_be16s(&payload[1], value);
|
|
lora_write_be16s(&payload[3], threshold);
|
|
payload[5] = duration_sec;
|
|
|
|
pkt->length += 6;
|
|
}
|
|
|
|
/**
|
|
* Add registration payload to packet
|
|
*/
|
|
static inline void lora_packet_add_register(lora_packet_t* pkt, uint16_t proposed_id,
|
|
lora_sensor_type_t sensor_type,
|
|
uint8_t fw_major, uint8_t fw_minor,
|
|
const char* node_name) {
|
|
uint8_t* payload = &pkt->data[pkt->length];
|
|
|
|
lora_write_be16(&payload[0], proposed_id);
|
|
payload[2] = sensor_type;
|
|
payload[3] = fw_major;
|
|
payload[4] = fw_minor;
|
|
|
|
// Copy node name, null-padded
|
|
memset(&payload[5], 0, 8);
|
|
if (node_name) {
|
|
size_t len = strlen(node_name);
|
|
if (len > 8) len = 8;
|
|
memcpy(&payload[5], node_name, len);
|
|
}
|
|
|
|
pkt->length += 13;
|
|
}
|
|
|
|
/**
|
|
* Add registration ACK payload to packet
|
|
*/
|
|
static inline void lora_packet_add_register_ack(lora_packet_t* pkt, uint16_t assigned_id,
|
|
uint8_t status, uint16_t interval_sec) {
|
|
uint8_t* payload = &pkt->data[pkt->length];
|
|
|
|
lora_write_be16(&payload[0], assigned_id);
|
|
payload[2] = status;
|
|
lora_write_be16(&payload[3], interval_sec);
|
|
|
|
pkt->length += 5;
|
|
}
|
|
|
|
/**
|
|
* Finalize packet by adding CRC
|
|
*
|
|
* Call this after adding all payloads and before transmitting.
|
|
*/
|
|
static inline void lora_packet_finalize(lora_packet_t* pkt) {
|
|
uint16_t crc = lora_crc16(pkt->data, pkt->length);
|
|
lora_write_be16(&pkt->data[pkt->length], crc);
|
|
pkt->length += 2;
|
|
}
|
|
|
|
// ============================================================================
|
|
// PACKET PARSING
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Parse received packet
|
|
*
|
|
* @param pkt Output packet structure
|
|
* @param data Raw received data
|
|
* @param len Length of received data
|
|
* @return true if packet is valid
|
|
*/
|
|
static inline bool lora_packet_parse(lora_packet_t* pkt, const uint8_t* data, uint8_t len) {
|
|
pkt->valid = false;
|
|
|
|
// Minimum size check
|
|
if (len < LORA_MIN_PACKET_SIZE) {
|
|
return false;
|
|
}
|
|
|
|
// Sync bytes check
|
|
if (data[0] != LORA_SYNC_BYTE_1 || data[1] != LORA_SYNC_BYTE_2) {
|
|
return false;
|
|
}
|
|
|
|
// CRC check
|
|
uint16_t received_crc = lora_read_be16(&data[len - 2]);
|
|
uint16_t computed_crc = lora_crc16(data, len - 2);
|
|
if (received_crc != computed_crc) {
|
|
return false;
|
|
}
|
|
|
|
// Copy data
|
|
memcpy(pkt->data, data, len);
|
|
pkt->length = len;
|
|
|
|
// Parse header
|
|
pkt->type = lora_get_type(data[2]);
|
|
pkt->ack_requested = lora_ack_requested(data[2]);
|
|
pkt->battery_low = lora_battery_low(data[2]);
|
|
pkt->first_boot = (data[2] & LORA_HDR_FIRST_BOOT) != 0;
|
|
pkt->node_id = lora_read_be16(&data[3]);
|
|
pkt->sequence = data[5];
|
|
|
|
pkt->valid = true;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get pointer to payload data
|
|
*/
|
|
static inline const uint8_t* lora_packet_payload(const lora_packet_t* pkt) {
|
|
return &pkt->data[6]; // After header
|
|
}
|
|
|
|
/**
|
|
* Get payload length (excluding header and CRC)
|
|
*/
|
|
static inline uint8_t lora_packet_payload_len(const lora_packet_t* pkt) {
|
|
if (pkt->length < LORA_MIN_PACKET_SIZE) return 0;
|
|
return pkt->length - 8; // Total - header(6) - CRC(2)
|
|
}
|
|
|
|
// ============================================================================
|
|
// PAYLOAD PARSING
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Parse DHT22 payload
|
|
*/
|
|
typedef struct {
|
|
float temp_c;
|
|
float humidity_pct;
|
|
uint16_t battery_mv;
|
|
int8_t rssi_dbm;
|
|
bool valid;
|
|
} lora_dht22_data_t;
|
|
|
|
static inline bool lora_parse_dht22(const uint8_t* payload, uint8_t len,
|
|
lora_dht22_data_t* out) {
|
|
if (len < 7 || payload[0] != SENSOR_TYPE_DHT22) {
|
|
out->valid = false;
|
|
return false;
|
|
}
|
|
|
|
out->temp_c = lora_decode_temp_c10(lora_read_be16s(&payload[1]));
|
|
out->humidity_pct = lora_decode_humidity(lora_read_be16(&payload[3]));
|
|
out->battery_mv = lora_decode_battery(payload[5]);
|
|
out->rssi_dbm = (int8_t)payload[6];
|
|
out->valid = true;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Parse BME680 payload
|
|
*/
|
|
typedef struct {
|
|
float temp_c;
|
|
float humidity_pct;
|
|
float pressure_hpa;
|
|
uint16_t gas_kohm;
|
|
uint8_t iaq_index;
|
|
uint16_t battery_mv;
|
|
int8_t rssi_dbm;
|
|
bool valid;
|
|
} lora_bme680_data_t;
|
|
|
|
static inline bool lora_parse_bme680(const uint8_t* payload, uint8_t len,
|
|
lora_bme680_data_t* out) {
|
|
if (len < 12 || payload[0] != SENSOR_TYPE_BME680) {
|
|
out->valid = false;
|
|
return false;
|
|
}
|
|
|
|
out->temp_c = lora_decode_temp_c10(lora_read_be16s(&payload[1]));
|
|
out->humidity_pct = lora_decode_humidity(lora_read_be16(&payload[3]));
|
|
out->pressure_hpa = 900.0f + (float)lora_read_be16(&payload[5]);
|
|
out->gas_kohm = lora_read_be16(&payload[7]);
|
|
out->iaq_index = payload[9];
|
|
out->battery_mv = lora_decode_battery(payload[10]);
|
|
out->rssi_dbm = (int8_t)payload[11];
|
|
out->valid = true;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Parse DS18B20 payload
|
|
*/
|
|
typedef struct {
|
|
float temp_c;
|
|
uint16_t battery_mv;
|
|
int8_t rssi_dbm;
|
|
bool valid;
|
|
} lora_ds18b20_data_t;
|
|
|
|
static inline bool lora_parse_ds18b20(const uint8_t* payload, uint8_t len,
|
|
lora_ds18b20_data_t* out) {
|
|
if (len < 5 || payload[0] != SENSOR_TYPE_DS18B20) {
|
|
out->valid = false;
|
|
return false;
|
|
}
|
|
|
|
out->temp_c = lora_decode_temp_c100(lora_read_be16s(&payload[1]));
|
|
out->battery_mv = lora_decode_battery(payload[3]);
|
|
out->rssi_dbm = (int8_t)payload[4];
|
|
out->valid = true;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Parse alert payload
|
|
*/
|
|
typedef struct {
|
|
lora_alert_type_t type;
|
|
int16_t value;
|
|
int16_t threshold;
|
|
uint8_t duration_sec;
|
|
bool valid;
|
|
} lora_alert_data_t;
|
|
|
|
static inline bool lora_parse_alert(const uint8_t* payload, uint8_t len,
|
|
lora_alert_data_t* out) {
|
|
if (len < 6) {
|
|
out->valid = false;
|
|
return false;
|
|
}
|
|
|
|
out->type = (lora_alert_type_t)payload[0];
|
|
out->value = lora_read_be16s(&payload[1]);
|
|
out->threshold = lora_read_be16s(&payload[3]);
|
|
out->duration_sec = payload[5];
|
|
out->valid = true;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Parse registration payload
|
|
*/
|
|
typedef struct {
|
|
uint16_t proposed_id;
|
|
lora_sensor_type_t sensor_type;
|
|
uint8_t fw_major;
|
|
uint8_t fw_minor;
|
|
char node_name[9]; // 8 chars + null
|
|
bool valid;
|
|
} lora_register_data_t;
|
|
|
|
static inline bool lora_parse_register(const uint8_t* payload, uint8_t len,
|
|
lora_register_data_t* out) {
|
|
if (len < 13) {
|
|
out->valid = false;
|
|
return false;
|
|
}
|
|
|
|
out->proposed_id = lora_read_be16(&payload[0]);
|
|
out->sensor_type = (lora_sensor_type_t)payload[2];
|
|
out->fw_major = payload[3];
|
|
out->fw_minor = payload[4];
|
|
memcpy(out->node_name, &payload[5], 8);
|
|
out->node_name[8] = '\0';
|
|
out->valid = true;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Parse registration ACK payload
|
|
*/
|
|
typedef struct {
|
|
uint16_t assigned_id;
|
|
uint8_t status;
|
|
uint16_t interval_sec;
|
|
bool valid;
|
|
} lora_register_ack_data_t;
|
|
|
|
static inline bool lora_parse_register_ack(const uint8_t* payload, uint8_t len,
|
|
lora_register_ack_data_t* out) {
|
|
if (len < 5) {
|
|
out->valid = false;
|
|
return false;
|
|
}
|
|
|
|
out->assigned_id = lora_read_be16(&payload[0]);
|
|
out->status = payload[2];
|
|
out->interval_sec = lora_read_be16(&payload[3]);
|
|
out->valid = true;
|
|
return true;
|
|
}
|
|
|
|
// ============================================================================
|
|
// ACK/NAK HELPERS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Build ACK packet in response to received packet
|
|
*/
|
|
static inline void lora_build_ack(lora_packet_t* ack, const lora_packet_t* received) {
|
|
lora_packet_init(ack, LORA_PKT_ACK, received->node_id, received->sequence,
|
|
false, false, false);
|
|
lora_packet_finalize(ack);
|
|
}
|
|
|
|
/**
|
|
* Build NAK packet requesting retransmission
|
|
*/
|
|
static inline void lora_build_nak(lora_packet_t* nak, uint16_t node_id,
|
|
uint8_t expected_seq) {
|
|
lora_packet_init(nak, LORA_PKT_NAK, node_id, expected_seq,
|
|
false, false, false);
|
|
lora_packet_finalize(nak);
|
|
}
|
|
|
|
#endif // LORA_PACKET_H
|