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
+9 -6
View File
@@ -56,24 +56,27 @@
#define DISPLAY_UPDATE_INTERVAL_MS 1000
// ============================================================================
// LORA CONFIGURATION (for future remote sensor nodes)
// LORA CONFIGURATION (for remote sensor nodes)
// ============================================================================
// LoRa frequency (915 MHz for US)
// LoRa frequency (915 MHz for US, 868 MHz for EU)
#define LORA_FREQUENCY 915.0
// LoRa bandwidth (125 kHz)
#define LORA_BANDWIDTH 125.0
// LoRa bandwidth in kHz (125, 250, or 500)
#define LORA_BANDWIDTH 125000 // 125 kHz
// LoRa spreading factor (7-12, higher = longer range but slower)
#define LORA_SPREADING_FACTOR 9
// LoRa coding rate (5-8)
// LoRa coding rate (5-8, higher = more error correction)
#define LORA_CODING_RATE 7
// LoRa sync word (private network)
// LoRa sync word (must match remote nodes: 0x12)
#define LORA_SYNC_WORD 0x12
// Maximum remote nodes to track
#define MAX_LORA_NODES 16
// ============================================================================
// API RETRY CONFIGURATION
// ============================================================================
+439
View File
@@ -0,0 +1,439 @@
/**
* 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
+346
View File
@@ -0,0 +1,346 @@
/**
* ZNET LoRa Protocol Definition
*
* Packet format and constants for communication between remote
* sensor nodes and the ZNET gateway.
*
* See docs/LORA_PROTOCOL.md for full specification.
*/
#ifndef LORA_PROTOCOL_H
#define LORA_PROTOCOL_H
#include <stdint.h>
#include <stdbool.h>
// ============================================================================
// PROTOCOL CONSTANTS
// ============================================================================
// Sync bytes (magic number)
#define LORA_SYNC_BYTE_1 0x5A // 'Z'
#define LORA_SYNC_BYTE_2 0x4E // 'N'
// Maximum packet size
#define LORA_MAX_PACKET_SIZE 72 // 8 header + 64 payload max
#define LORA_MAX_PAYLOAD_SIZE 64
#define LORA_MIN_PACKET_SIZE 8 // Sync + Header + NodeID + Seq + CRC
// Header byte masks
#define LORA_HDR_TYPE_MASK 0xF0 // Upper nibble = packet type
#define LORA_HDR_TYPE_SHIFT 4
#define LORA_HDR_ACK_REQ 0x08 // Bit 3: Request acknowledgment
#define LORA_HDR_BAT_LOW 0x04 // Bit 2: Battery low warning
#define LORA_HDR_FIRST_BOOT 0x02 // Bit 1: First boot since power
#define LORA_HDR_RESERVED 0x01 // Bit 0: Reserved
// ============================================================================
// PACKET TYPES
// ============================================================================
typedef enum {
LORA_PKT_SENSOR_DATA = 0x0, // Node → Gateway: Sensor readings
LORA_PKT_ACK = 0x1, // Gateway → Node: Acknowledgment
LORA_PKT_NAK = 0x2, // Gateway → Node: Request resend
LORA_PKT_CONFIG_REQ = 0x3, // Node → Gateway: Request config
LORA_PKT_CONFIG_RESP = 0x4, // Gateway → Node: Configuration
LORA_PKT_PING = 0x5, // Gateway → Node: Alive check
LORA_PKT_PONG = 0x6, // Node → Gateway: Alive response
LORA_PKT_ALERT = 0x7, // Node → Gateway: Critical alert
LORA_PKT_TIME_SYNC = 0x8, // Gateway → Node: Time sync
LORA_PKT_FIRMWARE_INFO = 0x9, // Node → Gateway: FW version
LORA_PKT_REGISTER = 0xA, // Node → Gateway: Registration
LORA_PKT_REGISTER_ACK = 0xB, // Gateway → Node: Reg accepted
} lora_packet_type_t;
// ============================================================================
// SENSOR TYPES
// ============================================================================
typedef enum {
SENSOR_TYPE_DHT22 = 0x01, // Temperature + Humidity
SENSOR_TYPE_BME680 = 0x02, // Temp + Humidity + Pressure + Gas
SENSOR_TYPE_DS18B20 = 0x03, // Temperature only (high precision)
SENSOR_TYPE_MULTI = 0x10, // Multiple sensors
} lora_sensor_type_t;
// ============================================================================
// ALERT TYPES
// ============================================================================
typedef enum {
ALERT_TEMP_HIGH = 0x01,
ALERT_TEMP_LOW = 0x02,
ALERT_HUMIDITY_HIGH = 0x03,
ALERT_HUMIDITY_LOW = 0x04,
ALERT_BATTERY_CRITICAL = 0x05,
ALERT_SENSOR_FAILURE = 0x06,
ALERT_VOC_HIGH = 0x07,
} lora_alert_type_t;
// ============================================================================
// NODE ID RANGES
// ============================================================================
#define NODE_ID_BROADCAST 0x0000
#define NODE_ID_TANK_START 0x0001
#define NODE_ID_TANK_END 0x00FF
#define NODE_ID_AMBIENT_START 0x0100
#define NODE_ID_AMBIENT_END 0x01FF
#define NODE_ID_ENVIRO_START 0x0200
#define NODE_ID_ENVIRO_END 0x02FF
#define NODE_ID_AUTO_START 0x1000
#define NODE_ID_AUTO_END 0xFFFE
#define NODE_ID_AUTO_REQUEST 0xFFFF
// ============================================================================
// PACKET STRUCTURES
// ============================================================================
// Packet header (common to all packets)
typedef struct __attribute__((packed)) {
uint8_t sync[2]; // 0x5A 0x4E
uint8_t header; // Type (4 bits) + Flags (4 bits)
uint16_t node_id; // Big-endian
uint8_t sequence; // Sequence number
} lora_packet_header_t;
// DHT22 sensor payload (7 bytes)
typedef struct __attribute__((packed)) {
uint8_t sensor_type; // SENSOR_TYPE_DHT22
int16_t temp_c_x10; // Temperature °C × 10 (big-endian)
uint16_t humidity_x10; // Humidity % × 10 (big-endian)
uint8_t battery_mv20; // Battery mV ÷ 20
int8_t rssi_dbm; // RSSI in dBm
} lora_payload_dht22_t;
// BME680 sensor payload (12 bytes)
typedef struct __attribute__((packed)) {
uint8_t sensor_type; // SENSOR_TYPE_BME680
int16_t temp_c_x10; // Temperature °C × 10 (big-endian)
uint16_t humidity_x10; // Humidity % × 10 (big-endian)
uint16_t pressure_offset;// Pressure - 900 hPa (big-endian)
uint16_t gas_kohm; // Gas resistance kΩ (big-endian)
uint8_t iaq_index; // Air quality index 0-255
uint8_t battery_mv20; // Battery mV ÷ 20
int8_t rssi_dbm; // RSSI in dBm
} lora_payload_bme680_t;
// DS18B20 sensor payload (5 bytes)
typedef struct __attribute__((packed)) {
uint8_t sensor_type; // SENSOR_TYPE_DS18B20
int16_t temp_c_x100; // Temperature °C × 100 (big-endian)
uint8_t battery_mv20; // Battery mV ÷ 20
int8_t rssi_dbm; // RSSI in dBm
} lora_payload_ds18b20_t;
// Alert payload (6 bytes)
typedef struct __attribute__((packed)) {
uint8_t alert_type; // lora_alert_type_t
int16_t alert_value; // Type-specific value (big-endian)
int16_t threshold; // Threshold that was exceeded (big-endian)
uint8_t duration_sec; // How long condition persisted
} lora_payload_alert_t;
// Registration request payload (13 bytes)
typedef struct __attribute__((packed)) {
uint16_t proposed_id; // Proposed ID or 0xFFFF for auto
uint8_t sensor_type; // Primary sensor type
uint8_t fw_major; // Firmware major version
uint8_t fw_minor; // Firmware minor version
char node_name[8]; // Node name (null-padded)
} lora_payload_register_t;
// Registration ACK payload (5 bytes)
typedef struct __attribute__((packed)) {
uint16_t assigned_id; // Assigned node ID
uint8_t status; // 0=OK, 1=ID conflict, 2=rejected
uint16_t interval_sec; // Recommended report interval
} lora_payload_register_ack_t;
// Configuration request payload (12 bytes)
typedef struct __attribute__((packed)) {
uint16_t current_interval; // Current interval in seconds
uint8_t fw_major;
uint8_t fw_minor;
char node_name[8];
} lora_payload_config_req_t;
// Configuration response payload (11 bytes)
typedef struct __attribute__((packed)) {
uint16_t interval_sec; // 0 = use default
int16_t warn_high_c_x10; // Warning high threshold
int16_t alert_high_c_x10; // Alert high threshold
int16_t warn_low_c_x10; // 0x8000 = disabled
int16_t alert_low_c_x10; // 0x8000 = disabled
uint8_t flags; // Bit 0: alerts enabled, Bit 1: ACK required
} lora_payload_config_resp_t;
// ============================================================================
// CRC16-CCITT CALCULATION
// ============================================================================
/**
* Calculate CRC16-CCITT checksum
*
* @param data Pointer to data buffer
* @param len Length of data
* @return 16-bit CRC
*/
static inline uint16_t lora_crc16(const uint8_t* data, size_t len) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; i++) {
crc ^= (uint16_t)data[i] << 8;
for (int j = 0; j < 8; j++) {
if (crc & 0x8000) {
crc = (crc << 1) ^ 0x1021;
} else {
crc <<= 1;
}
}
}
return crc;
}
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
/**
* Build packet header byte
*/
static inline uint8_t lora_make_header(lora_packet_type_t type, bool ack_req,
bool bat_low, bool first_boot) {
uint8_t hdr = (type << LORA_HDR_TYPE_SHIFT) & LORA_HDR_TYPE_MASK;
if (ack_req) hdr |= LORA_HDR_ACK_REQ;
if (bat_low) hdr |= LORA_HDR_BAT_LOW;
if (first_boot) hdr |= LORA_HDR_FIRST_BOOT;
return hdr;
}
/**
* Extract packet type from header
*/
static inline lora_packet_type_t lora_get_type(uint8_t header) {
return (lora_packet_type_t)((header & LORA_HDR_TYPE_MASK) >> LORA_HDR_TYPE_SHIFT);
}
/**
* Check if ACK requested
*/
static inline bool lora_ack_requested(uint8_t header) {
return (header & LORA_HDR_ACK_REQ) != 0;
}
/**
* Check if battery low
*/
static inline bool lora_battery_low(uint8_t header) {
return (header & LORA_HDR_BAT_LOW) != 0;
}
/**
* Convert battery voltage to encoded value (mV ÷ 20)
*/
static inline uint8_t lora_encode_battery(uint16_t mv) {
if (mv > 5100) mv = 5100;
return (uint8_t)(mv / 20);
}
/**
* Decode battery value to mV
*/
static inline uint16_t lora_decode_battery(uint8_t encoded) {
return (uint16_t)encoded * 20;
}
/**
* Convert temperature to encoded value (°C × 10)
*/
static inline int16_t lora_encode_temp_c10(float temp_c) {
return (int16_t)(temp_c * 10.0f);
}
/**
* Decode temperature from encoded value
*/
static inline float lora_decode_temp_c10(int16_t encoded) {
return (float)encoded / 10.0f;
}
/**
* Convert temperature to high-precision encoded value (°C × 100)
*/
static inline int16_t lora_encode_temp_c100(float temp_c) {
return (int16_t)(temp_c * 100.0f);
}
/**
* Decode high-precision temperature
*/
static inline float lora_decode_temp_c100(int16_t encoded) {
return (float)encoded / 100.0f;
}
/**
* Convert humidity to encoded value (% × 10)
*/
static inline uint16_t lora_encode_humidity(float humidity_pct) {
return (uint16_t)(humidity_pct * 10.0f);
}
/**
* Decode humidity
*/
static inline float lora_decode_humidity(uint16_t encoded) {
return (float)encoded / 10.0f;
}
/**
* Convert Celsius to Fahrenheit
*/
static inline float lora_c_to_f(float celsius) {
return celsius * 9.0f / 5.0f + 32.0f;
}
/**
* Convert Fahrenheit to Celsius
*/
static inline float lora_f_to_c(float fahrenheit) {
return (fahrenheit - 32.0f) * 5.0f / 9.0f;
}
// ============================================================================
// BYTE ORDER HELPERS (for big-endian protocol)
// ============================================================================
/**
* Write uint16_t as big-endian
*/
static inline void lora_write_be16(uint8_t* buf, uint16_t val) {
buf[0] = (val >> 8) & 0xFF;
buf[1] = val & 0xFF;
}
/**
* Read uint16_t from big-endian
*/
static inline uint16_t lora_read_be16(const uint8_t* buf) {
return ((uint16_t)buf[0] << 8) | buf[1];
}
/**
* Write int16_t as big-endian
*/
static inline void lora_write_be16s(uint8_t* buf, int16_t val) {
lora_write_be16(buf, (uint16_t)val);
}
/**
* Read int16_t from big-endian
*/
static inline int16_t lora_read_be16s(const uint8_t* buf) {
return (int16_t)lora_read_be16(buf);
}
#endif // LORA_PROTOCOL_H