Add support for LoRaWanBridge

This commit is contained in:
Theo Arends 2024-03-23 15:20:22 +01:00
parent 6337c59fab
commit f10218a257
8 changed files with 1396 additions and 231 deletions

View File

@ -5,16 +5,20 @@ All notable changes to this project will be documented in this file.
## [13.4.0.3] ## [13.4.0.3]
### Added ### Added
- Zigbee added for attributes of type `uint48` used by energy monitoring - Zigbee support for attributes of type `uint48` used by energy monitoring (#20992)
- Support for LoRaWanBridge
### Breaking Changed ### Breaking Changed
### Changed ### Changed
- LVGL library from v9.0.0 to v9.1.0 - ESP32 LVGL library from v9.0.0 to v9.1.0 (#21008)
### Fixed ### Fixed
- Berry fix walrus bug when assigning to self - BTHome, prep BLE5 (#20989)
- Scripter google char memory leak (#20995)
- HASPmota demo and robotocondensed fonts (#21014)
- Berry walrus bug when assigning to self (#21015)
### Removed ### Removed
@ -29,6 +33,8 @@ All notable changes to this project will be documented in this file.
- Berry `string.startswith`, `string.endswith` and `%q` format (#20909) - Berry `string.startswith`, `string.endswith` and `%q` format (#20909)
- LVGL `lv.draw_label_dsc` and `lv_bar.get_indic_area` (#20936) - LVGL `lv.draw_label_dsc` and `lv_bar.get_indic_area` (#20936)
- HASPmota support for scale, percentages (#20974) - HASPmota support for scale, percentages (#20974)
- Support for ESP32-S3 120Mhz (#20973)
- Support for MCP23S08 (#20971)
### Breaking Changed ### Breaking Changed
- Drop support for old (insecure) fingerprint format (#20842) - Drop support for old (insecure) fingerprint format (#20842)
@ -40,16 +46,18 @@ All notable changes to this project will be documented in this file.
- LVGL improved readability of montserrat-10 (#20900) - LVGL improved readability of montserrat-10 (#20900)
- HASPmota moved to a distinct library `lv_haspmota` (#20929) - HASPmota moved to a distinct library `lv_haspmota` (#20929)
- HASPmota solidify server-side (#20938) - HASPmota solidify server-side (#20938)
- Refactor Platformio script `post_esp32.py` (#20966)
### Fixed ### Fixed
- Berry bug when parsing ternary operator (#20839) - Berry bug when parsing ternary operator (#20839)
- HASPmota widgets line, btnmatrix, qrcode, bar, checkbox (#20881) - HASPmota widgets line, btnmatrix, qrcode, bar, checkbox (#20881)
- Filesystem save of JSON settings data - Filesystem save of JSON settings data
- Berry fix walrus with member or index (#20939) - Berry fix walrus with member or index (#20939)
- TuyaV2 suppressed dimmer updates from MQTT (#20950)
## [13.4.0.1] 20240229 ## [13.4.0.1] 20240229
### Added ### Added
- Experimental support for LoRa - Support for LoRa
- HASPmota `p<x>b<y>.delete` to delete an object (#20735) - HASPmota `p<x>b<y>.delete` to delete an object (#20735)
- LVGL and HASPmota typicons font (#20742) - LVGL and HASPmota typicons font (#20742)
- HASPmota more attributes (#20744) - HASPmota more attributes (#20744)

View File

@ -118,12 +118,15 @@ The latter links can be used for OTA upgrades too like ``OtaUrl https://ota.tasm
## Changelog v13.4.0.3 ## Changelog v13.4.0.3
### Added ### Added
- Experimental support for LoRa - Support for LoRa and LoRaWanBridge
- Support for AMS5915/AMS6915 temperature and pressure sensors [#20814](https://github.com/arendst/Tasmota/issues/20814) - Support for AMS5915/AMS6915 temperature and pressure sensors [#20814](https://github.com/arendst/Tasmota/issues/20814)
- Show calculated heat index if temperature and humidity is available with ``#define USE_HEAT_INDEX`` [#4771](https://github.com/arendst/Tasmota/issues/4771) - Show calculated heat index if temperature and humidity is available with ``#define USE_HEAT_INDEX`` [#4771](https://github.com/arendst/Tasmota/issues/4771)
- IR support data larger than 64 bits [#20831](https://github.com/arendst/Tasmota/issues/20831) - IR support data larger than 64 bits [#20831](https://github.com/arendst/Tasmota/issues/20831)
- TasMesh support for LWT messages [#20392](https://github.com/arendst/Tasmota/issues/20392) - TasMesh support for LWT messages [#20392](https://github.com/arendst/Tasmota/issues/20392)
- QMC5883l check for overflow and scale reading [#20643](https://github.com/arendst/Tasmota/issues/20643) - QMC5883l check for overflow and scale reading [#20643](https://github.com/arendst/Tasmota/issues/20643)
- Support for MCP23S08 [#20971](https://github.com/arendst/Tasmota/issues/20971)
- Support for ESP32-S3 120Mhz [#20973](https://github.com/arendst/Tasmota/issues/20973)
- Zigbee support for attributes of type `uint48` used by energy monitoring [#20992](https://github.com/arendst/Tasmota/issues/20992)
- Berry explicit error log when memory allocation fails [#20807](https://github.com/arendst/Tasmota/issues/20807) - Berry explicit error log when memory allocation fails [#20807](https://github.com/arendst/Tasmota/issues/20807)
- Berry `path.rename()` [#20840](https://github.com/arendst/Tasmota/issues/20840) - Berry `path.rename()` [#20840](https://github.com/arendst/Tasmota/issues/20840)
- Berry `string.startswith`, `string.endswith` and `%q` format [#20909](https://github.com/arendst/Tasmota/issues/20909) - Berry `string.startswith`, `string.endswith` and `%q` format [#20909](https://github.com/arendst/Tasmota/issues/20909)
@ -140,6 +143,8 @@ The latter links can be used for OTA upgrades too like ``OtaUrl https://ota.tasm
### Changed ### Changed
- ESP32 Core3 platform update from 2024.01.12 to 2024.02.10 [#20730](https://github.com/arendst/Tasmota/issues/20730) - ESP32 Core3 platform update from 2024.01.12 to 2024.02.10 [#20730](https://github.com/arendst/Tasmota/issues/20730)
- ESP32 LVGL library from v9.0.0 to v9.1.0 [#21008](https://github.com/arendst/Tasmota/issues/21008)
- Refactor Platformio script `post_esp32.py` [#20966](https://github.com/arendst/Tasmota/issues/20966)
- NeoPool webUI pH alarms (4 & 5) completed (#20743)[#20743](https://github.com/arendst/Tasmota/issues/20743) - NeoPool webUI pH alarms (4 & 5) completed (#20743)[#20743](https://github.com/arendst/Tasmota/issues/20743)
- Prevent shutter MQTT broadcast with activated ShutterLock [#20827](https://github.com/arendst/Tasmota/issues/20827) - Prevent shutter MQTT broadcast with activated ShutterLock [#20827](https://github.com/arendst/Tasmota/issues/20827)
- Berry class `int64` made immutable [#20727](https://github.com/arendst/Tasmota/issues/20727) - Berry class `int64` made immutable [#20727](https://github.com/arendst/Tasmota/issues/20727)
@ -157,11 +162,16 @@ The latter links can be used for OTA upgrades too like ``OtaUrl https://ota.tasm
### Fixed ### Fixed
- Filesystem save of JSON settings data - Filesystem save of JSON settings data
- Shutter inverted using internal commands [#20752](https://github.com/arendst/Tasmota/issues/20752) - Shutter inverted using internal commands [#20752](https://github.com/arendst/Tasmota/issues/20752)
- TuyaV2 suppressed dimmer updates from MQTT [#20950](https://github.com/arendst/Tasmota/issues/20950)
- Scripter google char memory leak [#20995](https://github.com/arendst/Tasmota/issues/20995)
- ESP32 PWM activity on unconfigured PWM GPIOs [#20732](https://github.com/arendst/Tasmota/issues/20732) - ESP32 PWM activity on unconfigured PWM GPIOs [#20732](https://github.com/arendst/Tasmota/issues/20732)
- BTHome, prep BLE5 [#20989](https://github.com/arendst/Tasmota/issues/20989)
- Berry Memory leak in `import re` [#20823](https://github.com/arendst/Tasmota/issues/20823) - Berry Memory leak in `import re` [#20823](https://github.com/arendst/Tasmota/issues/20823)
- Berry bug when parsing ternary operator [#20839](https://github.com/arendst/Tasmota/issues/20839) - Berry bug when parsing ternary operator [#20839](https://github.com/arendst/Tasmota/issues/20839)
- Berry fix walrus with member or index [#20939](https://github.com/arendst/Tasmota/issues/20939) - Berry walrus with member or index [#20939](https://github.com/arendst/Tasmota/issues/20939)
- Berry walrus bug when assigning to self [#21015](https://github.com/arendst/Tasmota/issues/21015)
- HASPmota PSRAM memory leak [#20818](https://github.com/arendst/Tasmota/issues/20818) - HASPmota PSRAM memory leak [#20818](https://github.com/arendst/Tasmota/issues/20818)
- HASPmota widgets line, btnmatrix, qrcode, bar, checkbox [#20881](https://github.com/arendst/Tasmota/issues/20881) - HASPmota widgets line, btnmatrix, qrcode, bar, checkbox [#20881](https://github.com/arendst/Tasmota/issues/20881)
- HASPmota demo and robotocondensed fonts [#21014](https://github.com/arendst/Tasmota/issues/21014)
### Removed ### Removed

View File

@ -8,70 +8,154 @@
#ifdef USE_SPI #ifdef USE_SPI
#ifdef USE_SPI_LORA #ifdef USE_SPI_LORA
#ifdef USE_LORAWAN_BRIDGE
#define USE_LORAWAN
#endif
/*********************************************************************************************\ /*********************************************************************************************\
* LoRa defines and global struct * LoRa defines and global struct
\*********************************************************************************************/ \*********************************************************************************************/
//#define USE_LORA_DEBUG //#define USE_LORA_DEBUG
#define LORA_MAX_PACKET_LENGTH 252 // Max packet length allowed (keeping room for control bytes) #define XDRV_73_KEY "drvset73"
#define TAS_LORA_REMOTE_COMMAND 0x17 // Header defining remote LoRaCommand
#ifndef TAS_LORA_FREQUENCY #ifndef TAS_LORA_FREQUENCY
#define TAS_LORA_FREQUENCY 868.0 // Allowed values range from 150.0 to 960.0 MHz #define TAS_LORA_FREQUENCY 868.0 // Allowed values range from 150.0 to 960.0 MHz
#endif #endif
#ifndef TAS_LORA_BANDWIDTH #ifndef TAS_LORA_BANDWIDTH
#define TAS_LORA_BANDWIDTH 125.0 // Allowed values are 7.8, 10.4, 15.6, 20.8, 31.25, 41.7, 62.5, 125.0, 250.0 and 500.0 kHz #define TAS_LORA_BANDWIDTH 125.0 // Allowed values are 7.8, 10.4, 15.6, 20.8, 31.25, 41.7, 62.5, 125.0, 250.0 and 500.0 kHz
#endif #endif
#ifndef TAS_LORA_SPREADING_FACTOR #ifndef TAS_LORA_SPREADING_FACTOR
#define TAS_LORA_SPREADING_FACTOR 9 // Allowed values range from 5 to 12 #define TAS_LORA_SPREADING_FACTOR 9 // Allowed values range from 5 to 12
#endif #endif
#ifndef TAS_LORA_CODING_RATE #ifndef TAS_LORA_CODING_RATE
#define TAS_LORA_CODING_RATE 7 // Allowed values range from 5 to 8 #define TAS_LORA_CODING_RATE 7 // Allowed values range from 5 to 8
#endif #endif
#ifndef TAS_LORA_SYNC_WORD #ifndef TAS_LORA_SYNC_WORD
#define TAS_LORA_SYNC_WORD 0x12 // Allowed values range from 1 to 255 #define TAS_LORA_SYNC_WORD 0x12 // Allowed values range from 1 to 255
#endif #endif
#ifndef TAS_LORA_OUTPUT_POWER #ifndef TAS_LORA_OUTPUT_POWER
#define TAS_LORA_OUTPUT_POWER 10 // Allowed values range from 1 to 20 #define TAS_LORA_OUTPUT_POWER 10 // Allowed values range from 1 to 20
#endif #endif
#ifndef TAS_LORA_PREAMBLE_LENGTH #ifndef TAS_LORA_PREAMBLE_LENGTH
#define TAS_LORA_PREAMBLE_LENGTH 8 // Allowed values range from 1 to 65535 #define TAS_LORA_PREAMBLE_LENGTH 8 // Allowed values range from 1 to 65535
#endif #endif
#ifndef TAS_LORA_CURRENT_LIMIT #ifndef TAS_LORA_CURRENT_LIMIT
#define TAS_LORA_CURRENT_LIMIT 60.0 // Overcurrent Protection - OCP in mA #define TAS_LORA_CURRENT_LIMIT 60.0 // Overcurrent Protection - OCP in mA
#endif #endif
#ifndef TAS_LORA_HEADER #ifndef TAS_LORA_HEADER
#define TAS_LORA_HEADER 0 // Explicit (0) or Implicit (1 to 4) Header #define TAS_LORA_HEADER 0 // Explicit (0) or Implicit (1 to 4) Header
#endif #endif
#ifndef TAS_LORA_CRC_BYTES #ifndef TAS_LORA_CRC_BYTES
#define TAS_LORA_CRC_BYTES 2 // No (0) or Number (1 to 4) of CRC bytes #define TAS_LORA_CRC_BYTES 2 // No (0) or Number (1 to 4) of CRC bytes
#endif #endif
#ifndef TAS_LORAWAN_FREQUENCY
#define TAS_LORAWAN_FREQUENCY 868.1 // Allowed values range from 150.0 to 960.0 MHz
#endif
#ifndef TAS_LORAWAN_BANDWIDTH
#define TAS_LORAWAN_BANDWIDTH 125.0 // Allowed values are 7.8, 10.4, 15.6, 20.8, 31.25, 41.7, 62.5, 125.0, 250.0 and 500.0 kHz
#endif
#ifndef TAS_LORAWAN_SPREADING_FACTOR
#define TAS_LORAWAN_SPREADING_FACTOR 9 // Allowed values range from 5 to 12
#endif
#ifndef TAS_LORAWAN_CODING_RATE
#define TAS_LORAWAN_CODING_RATE 5 // Allowed values range from 5 to 8
#endif
#ifndef TAS_LORAWAN_SYNC_WORD
#define TAS_LORAWAN_SYNC_WORD 0x34 // Allowed values range from 1 to 255
#endif
#ifndef TAS_LORAWAN_OUTPUT_POWER
#define TAS_LORAWAN_OUTPUT_POWER 10 // Allowed values range from 1 to 20
#endif
#ifndef TAS_LORAWAN_PREAMBLE_LENGTH
#define TAS_LORAWAN_PREAMBLE_LENGTH 8 // Allowed values range from 1 to 65535
#endif
#ifndef TAS_LORAWAN_CURRENT_LIMIT
#define TAS_LORAWAN_CURRENT_LIMIT 60.0 // Overcurrent Protection - OCP in mA
#endif
#ifndef TAS_LORAWAN_HEADER
#define TAS_LORAWAN_HEADER 0 // Explicit (0) or Implicit (1 to 4) Header
#endif
#ifndef TAS_LORAWAN_CRC_BYTES
#define TAS_LORAWAN_CRC_BYTES 2 // No (0) or Number (1 to 4) of CRC bytes
#endif
#define TAS_LORA_MAX_PACKET_LENGTH 252 // Max packet length allowed (keeping room for control bytes)
#define TAS_LORA_REMOTE_COMMAND 0x17 // Header defining remote LoRaCommand
#define TAS_LORAWAN_JOINNONCE 0x00E50631 // Tasmota node 1 JoinNonce
#define TAS_LORAWAN_NETID 0x00000000 // Tasmota private network
#define TAS_LORAWAN_RECEIVE_DELAY1 1000 // LoRaWan Receive delay 1
#define TAS_LORAWAN_RECEIVE_DELAY2 1000 // LoRaWan Receive delay 2
#define TAS_LORAWAN_JOIN_ACCEPT_DELAY1 5000 // LoRaWan Join accept delay 1
#define TAS_LORAWAN_JOIN_ACCEPT_DELAY2 1000 // LoRaWan Join accept delay 2
#define TAS_LORAWAN_ENDNODES 4 // Max number od supported endnodes
#define TAS_LORAWAN_AES128_KEY_SIZE 16 // Size in bytes
enum TasLoraFlags { TAS_LORAWAN_BRIDGE_ENABLED,
TAS_LORAWAN_JOIN_ENABLED,
TAS_LORAWAN_DECODE_ENABLED
};
enum TasLoraWanFlags { TAS_LORAWAN_LINK_ADR_REQ
};
typedef struct {
uint32_t DevEUIh;
uint32_t DevEUIl;
uint32_t FCntUp;
uint32_t FCntDown;
String name;
uint16_t DevNonce;
uint16_t flags;
uint8_t AppKey[TAS_LORAWAN_AES128_KEY_SIZE];
} tEndNode;
// Global structure containing driver saved variables
struct {
uint32_t crc32; // To detect file changes
float frequency; // 868.0 MHz
float bandwidth; // 125.0 kHz
float current_limit; // 60.0 mA (Overcurrent Protection (OCP))
uint16_t preamble_length; // 8 symbols
uint8_t sync_word; // 0x12
uint8_t spreading_factor; // 9
uint8_t coding_rate; // 7
uint8_t output_power; // 10 dBm
uint8_t implicit_header; // 0
uint8_t crc_bytes; // 2 bytes
uint8_t flags;
#ifdef USE_LORAWAN_BRIDGE
tEndNode end_node[TAS_LORAWAN_ENDNODES]; // End node parameters
#endif // USE_LORAWAN_BRIDGE
} LoraSettings;
struct { struct {
bool (* Config)(void); bool (* Config)(void);
bool (* Available)(void); bool (* Available)(void);
int (* Receive)(char*); int (* Receive)(char*);
bool (* Send)(uint8_t*, uint32_t); bool (* Send)(uint8_t*, uint32_t, bool);
uint32_t receive_time;
float rssi; float rssi;
float snr; float snr;
float frequency; // 868.0 MHz uint8_t packet_size; // Max is 255 (LORA_MAX_PACKET_LENGTH)
float bandwidth; // 125.0 kHz volatile bool receivedFlag; // flag to indicate that a packet was received
float current_limit; // 60.0 mA (Overcurrent Protection (OCP))
uint16_t preamble_length; // 8 symbols
uint8_t sync_word; // 0x12
uint8_t spreading_factor; // 9
uint8_t coding_rate; // 7
uint8_t output_power; // 10 dBm
uint8_t implicit_header; // 0
uint8_t crc_bytes; // 2 bytes
uint8_t packet_size; // Max is 255 (LORA_MAX_PACKET_LENGTH)
volatile bool receivedFlag; // flag to indicate that a packet was received
volatile bool enableInterrupt; // disable interrupt when it's not needed
bool sendFlag; bool sendFlag;
bool raw; bool raw;
bool present; bool present;
} Lora; } Lora;
#ifdef USE_LORAWAN_BRIDGE
struct {
uint32_t device_address;
uint32_t send_buffer_step;
size_t send_buffer_len;
uint8_t send_buffer[64];
bool rx;
} Lorawan;
#endif // USE_LORAWAN_BRIDGE
#endif // USE_SPI_LORA #endif // USE_SPI_LORA
#endif // USE_SPI #endif // USE_SPI

View File

@ -30,125 +30,76 @@
SX1262 LoRaRadio = nullptr; // Select LoRa support SX1262 LoRaRadio = nullptr; // Select LoRa support
void LoraOnReceiveSx126x(void) { void LoraOnReceiveSx126x(void) {
if (!Lora.enableInterrupt) { return; } // check if the interrupt is enabled // This is called after EVERY type of enabled interrupt so chk for valid receivedFlag in LoraAvailableSx126x()
if (!Lora.sendFlag && !Lora.receivedFlag && !Lora.receive_time) {
Lora.receive_time = millis();
}
Lora.receivedFlag = true; // we got a packet, set the flag Lora.receivedFlag = true; // we got a packet, set the flag
} }
bool LoraAvailableSx126x(void) { bool LoraAvailableSx126x(void) {
return (Lora.receivedFlag); // check if the flag is set if (Lora.receivedFlag && Lora.sendFlag) {
Lora.receivedFlag = false; // Reset receive flag as it was caused by send interrupt
Lora.sendFlag = false;
LoRaRadio.startReceive(); // Put module back to listen mode
AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("S6X: Receive restarted"));
}
return (Lora.receivedFlag); // Check if the receive flag is set
} }
int LoraReceiveSx126x(char* data) { int LoraReceiveSx126x(char* data) {
Lora.enableInterrupt = false; // disable the interrupt service routine while processing the data Lora.receivedFlag = false; // Reset flag
Lora.receivedFlag = false; // reset flag int packet_size = LoRaRadio.getPacketLength();
int state = LoRaRadio.readData((uint8_t*)data, TAS_LORA_MAX_PACKET_LENGTH -1);
int packet_size = 0; // LoRaWan downlink frames are sent without CRC, which will raise error on SX126x. We can ignore that error
int state = LoRaRadio.readData((uint8_t*)data, LORA_MAX_PACKET_LENGTH -1); if (RADIOLIB_ERR_CRC_MISMATCH == state) {
state = RADIOLIB_ERR_NONE;
AddLog(LOG_LEVEL_DEBUG, PSTR("S6X: Ignoring CRC error"));
}
if (RADIOLIB_ERR_NONE == state) { if (RADIOLIB_ERR_NONE == state) {
if (!Lora.sendFlag) { Lora.rssi = LoRaRadio.getRSSI();
packet_size = LoRaRadio.getPacketLength(); Lora.snr = LoRaRadio.getSNR();
#ifdef USE_LORA_DEBUG } else {
AddLog(LOG_LEVEL_DEBUG, PSTR("LOR: Packet %d, Rcvd %32_H"), packet_size, data); packet_size = 0; // Some other error occurred
#endif AddLog(LOG_LEVEL_DEBUG, PSTR("S6X: Receive error %d"), state);
}
} }
else if (RADIOLIB_ERR_CRC_MISMATCH == state) {
// packet was received, but is malformed
AddLog(LOG_LEVEL_DEBUG, PSTR("LOR: CRC error"));
}
else {
// some other error occurred
AddLog(LOG_LEVEL_DEBUG, PSTR("LOR: Receive error %d"), state);
}
LoRaRadio.startReceive(); // put module back to listen mode
Lora.sendFlag = false;
Lora.enableInterrupt = true; // we're ready to receive more packets, enable interrupt service routine
Lora.rssi = LoRaRadio.getRSSI();
Lora.snr = LoRaRadio.getSNR();
return packet_size; return packet_size;
} }
bool LoraSendSx126x(uint8_t* data, uint32_t len) { bool LoraSendSx126x(uint8_t* data, uint32_t len, bool invert) {
Lora.sendFlag = true; Lora.sendFlag = true; // Use this flag as LoRaRadio.transmit enable send interrupt
#ifdef USE_LORA_DEBUG if (invert) {
AddLog(LOG_LEVEL_DEBUG, PSTR("LOR: Len %d, Send %*_H"), len, len + 2, data); LoRaRadio.invertIQ(true);
#endif
/*
int state = LoRaRadio.startTransmit(data, len);
return (RADIOLIB_ERR_NONE == state);
*/
// https://learn.circuit.rocks/battery-powered-lora-sensor-node
uint32_t retry_CAD = 0;
uint32_t retry_send = 0;
bool send_success = false;
while (!send_success) {
#ifdef USE_LORA_DEBUG
time_t lora_time = millis();
#endif
// Check 200ms for an opportunity to send
while (LoRaRadio.scanChannel() != RADIOLIB_CHANNEL_FREE) {
retry_CAD++;
if (retry_CAD == 20) {
// LoRa channel is busy too long, give up
#ifdef USE_LORA_DEBUG
AddLog(LOG_LEVEL_DEBUG, PSTR("LOR: Channel is too busy, give up"));
#endif
retry_send++;
break;
}
}
#ifdef USE_LORA_DEBUG
AddLog(LOG_LEVEL_DEBUG, PSTR("LOR: CAD finished after %ldms tried %d times"), (millis() - lora_time), retry_CAD);
#endif
if (retry_CAD < 20) {
// Channel is free, start sending
#ifdef USE_LORA_DEBUG
lora_time = millis();
#endif
int status = LoRaRadio.transmit(data, len);
#ifdef USE_LORA_DEBUG
AddLog(LOG_LEVEL_DEBUG, PSTR("LOR: Transmit finished after %ldms with status %d"), (millis() - lora_time), status);
#endif
if (status == RADIOLIB_ERR_NONE) {
send_success = true;
}
else {
retry_send++;
}
}
if (retry_send == 3) {
#ifdef USE_LORA_DEBUG
AddLog(LOG_LEVEL_DEBUG, PSTR("LOR: Failed 3 times to send data, giving up"));
#endif
send_success = true;
}
} }
return send_success; int state = LoRaRadio.transmit(data, len);
if (invert) {
LoRaRadio.invertIQ(false);
}
return (RADIOLIB_ERR_NONE == state);
} }
bool LoraConfigSx126x(void) { bool LoraConfigSx126x(void) {
LoRaRadio.setCodingRate(Lora.coding_rate); LoRaRadio.setCodingRate(LoraSettings.coding_rate);
LoRaRadio.setSyncWord(Lora.sync_word); LoRaRadio.setSyncWord(LoraSettings.sync_word);
LoRaRadio.setPreambleLength(Lora.preamble_length); LoRaRadio.setPreambleLength(LoraSettings.preamble_length);
LoRaRadio.setCurrentLimit(Lora.current_limit); LoRaRadio.setCurrentLimit(LoraSettings.current_limit);
LoRaRadio.setCRC(Lora.crc_bytes); LoRaRadio.setCRC(LoraSettings.crc_bytes);
LoRaRadio.setSpreadingFactor(Lora.spreading_factor); LoRaRadio.setSpreadingFactor(LoraSettings.spreading_factor);
LoRaRadio.setBandwidth(Lora.bandwidth); LoRaRadio.setBandwidth(LoraSettings.bandwidth);
LoRaRadio.setFrequency(Lora.frequency); LoRaRadio.setFrequency(LoraSettings.frequency);
LoRaRadio.setOutputPower(Lora.output_power); LoRaRadio.setOutputPower(LoraSettings.output_power);
if (Lora.implicit_header) { if (LoraSettings.implicit_header) {
LoRaRadio.implicitHeader(Lora.implicit_header); LoRaRadio.implicitHeader(LoraSettings.implicit_header);
} else { } else {
LoRaRadio.explicitHeader(); LoRaRadio.explicitHeader();
} }
LoRaRadio.invertIQ(false);
return true; return true;
} }
bool LoraInitSx126x(void) { bool LoraInitSx126x(void) {
LoRaRadio = new Module(Pin(GPIO_LORA_CS), Pin(GPIO_LORA_DI1), Pin(GPIO_LORA_RST), Pin(GPIO_LORA_BUSY)); LoRaRadio = new Module(Pin(GPIO_LORA_CS), Pin(GPIO_LORA_DI1), Pin(GPIO_LORA_RST), Pin(GPIO_LORA_BUSY));
if (RADIOLIB_ERR_NONE == LoRaRadio.begin(Lora.frequency)) { if (RADIOLIB_ERR_NONE == LoRaRadio.begin(LoraSettings.frequency)) {
LoraConfigSx126x(); LoraConfigSx126x();
LoRaRadio.setDio1Action(LoraOnReceiveSx126x); LoRaRadio.setDio1Action(LoraOnReceiveSx126x);
if (RADIOLIB_ERR_NONE == LoRaRadio.startReceive()) { if (RADIOLIB_ERR_NONE == LoRaRadio.startReceive()) {

View File

@ -27,30 +27,17 @@
* - LoRa_DIO0 * - LoRa_DIO0
\*********************************************************************************************/ \*********************************************************************************************/
//#define USE_LORA_SX127X_CAD
#include <LoRa.h> // extern LoRaClass LoRa; #include <LoRa.h> // extern LoRaClass LoRa;
#ifdef USE_LORA_SX127X_CAD
void LoraOnCadDoneSx127x(boolean signalDetected) {
if (signalDetected) { // detect preamble
#ifdef USE_LORA_DEBUG
AddLog(LOG_LEVEL_DEBUG, PSTR("LOR: Signal detected"));
#endif
LoRa.receive(); // put the radio into continuous receive mode
} else {
LoRa.channelActivityDetection(); // try next activity dectection
}
}
#endif // USE_LORA_SX127X_CAD
// this function is called when a complete packet is received by the module
void LoraOnReceiveSx127x(int packet_size) { void LoraOnReceiveSx127x(int packet_size) {
// This function is called when a complete packet is received by the module
#ifdef USE_LORA_DEBUG #ifdef USE_LORA_DEBUG
AddLog(LOG_LEVEL_DEBUG, PSTR("LOR: Packet size %d"), packet_size); // AddLog(LOG_LEVEL_DEBUG, PSTR("S7X: Packet size %d"), packet_size);
#endif #endif
if (0 == packet_size) { return; } // if there's no packet, return if (0 == packet_size) { return; } // if there's no packet, return
if (!Lora.enableInterrupt) { return; } // check if the interrupt is enabled if (!Lora.receive_time) {
Lora.receive_time = millis();
}
Lora.packet_size = packet_size; // we got a packet, set the flag Lora.packet_size = packet_size; // we got a packet, set the flag
} }
@ -59,73 +46,64 @@ bool LoraAvailableSx127x(void) {
} }
int LoraReceiveSx127x(char* data) { int LoraReceiveSx127x(char* data) {
Lora.enableInterrupt = false; // disable the interrupt service routine while processing the data
int packet_size = 0; int packet_size = 0;
while (LoRa.available()) { // read packet up to LORA_MAX_PACKET_LENGTH while (LoRa.available()) { // read packet up to LORA_MAX_PACKET_LENGTH
char sdata = LoRa.read(); char sdata = LoRa.read();
if (packet_size < LORA_MAX_PACKET_LENGTH -1) { if (packet_size < TAS_LORA_MAX_PACKET_LENGTH -1) {
data[packet_size++] = sdata; data[packet_size++] = sdata;
} }
} }
// if (Lora.sendFlag) { packet_size = 0; }
// Lora.sendFlag = false;
Lora.packet_size = 0; // reset flag
Lora.enableInterrupt = true; // we're ready to receive more packets, enable interrupt service routine
Lora.rssi = LoRa.packetRssi(); Lora.rssi = LoRa.packetRssi();
Lora.snr = LoRa.packetSnr(); Lora.snr = LoRa.packetSnr();
#ifdef USE_LORA_SX127X_CAD Lora.packet_size = 0; // reset flag
LoRa.channelActivityDetection(); // put the radio into CAD mode
#endif // USE_LORA_SX127X_CAD
return packet_size; return packet_size;
} }
bool LoraSendSx127x(uint8_t* data, uint32_t len) { bool LoraSendSx127x(uint8_t* data, uint32_t len, bool invert) {
// Lora.sendFlag = true; if (invert) {
LoRa.beginPacket(Lora.implicit_header); // start packet LoRa.enableInvertIQ(); // active invert I and Q signals
LoRa.write(data, len); // send message }
LoRa.beginPacket(LoraSettings.implicit_header); // start packet
LoRa.write(data, len); // send message
LoRa.endPacket(); // finish packet and send it LoRa.endPacket(); // finish packet and send it
if (invert) {
LoRa.disableInvertIQ(); // normal mode
}
LoRa.receive(); // go back into receive mode LoRa.receive(); // go back into receive mode
return true; return true;
} }
bool LoraConfigSx127x(void) { bool LoraConfigSx127x(void) {
LoRa.setFrequency(Lora.frequency * 1000 * 1000); LoRa.setFrequency(LoraSettings.frequency * 1000 * 1000);
LoRa.setSignalBandwidth(Lora.bandwidth * 1000); LoRa.setSignalBandwidth(LoraSettings.bandwidth * 1000);
LoRa.setSpreadingFactor(Lora.spreading_factor); LoRa.setSpreadingFactor(LoraSettings.spreading_factor);
LoRa.setCodingRate4(Lora.coding_rate); LoRa.setCodingRate4(LoraSettings.coding_rate);
LoRa.setSyncWord(Lora.sync_word); LoRa.setSyncWord(LoraSettings.sync_word);
LoRa.setTxPower(Lora.output_power); LoRa.setTxPower(LoraSettings.output_power);
LoRa.setPreambleLength(Lora.preamble_length); LoRa.setPreambleLength(LoraSettings.preamble_length);
LoRa.setOCP(Lora.current_limit); LoRa.setOCP(LoraSettings.current_limit);
if (Lora.crc_bytes) { if (LoraSettings.crc_bytes) {
LoRa.enableCrc(); LoRa.enableCrc();
} else { } else {
LoRa.disableCrc(); LoRa.disableCrc();
} }
/* /*
if (Lora.implicit_header) { if (LoraSettings.implicit_header) {
LoRa.implicitHeaderMode(); LoRa.implicitHeaderMode();
} else { } else {
LoRa.explicitHeaderMode(); LoRa.explicitHeaderMode();
} }
*/ */
LoRa.disableInvertIQ(); // normal mode
return true; return true;
} }
bool LoraInitSx127x(void) { bool LoraInitSx127x(void) {
LoRa.setPins(Pin(GPIO_LORA_CS), Pin(GPIO_LORA_RST), Pin(GPIO_LORA_DI0)); LoRa.setPins(Pin(GPIO_LORA_CS), Pin(GPIO_LORA_RST), Pin(GPIO_LORA_DI0));
if (LoRa.begin(Lora.frequency * 1000 * 1000)) { if (LoRa.begin(LoraSettings.frequency * 1000 * 1000)) {
LoraConfigSx127x(); LoraConfigSx127x();
LoRa.onReceive(LoraOnReceiveSx127x); LoRa.onReceive(LoraOnReceiveSx127x);
#ifdef USE_LORA_SX127X_CAD
LoRa.onCadDone(LoraOnCadDoneSx127x); // register the channel activity dectection callback
LoRa.channelActivityDetection();
#else
LoRa.receive(); LoRa.receive();
#endif // USE_LORA_SX127X_CAD
return true; return true;
} }
return false; return false;

View File

@ -0,0 +1,243 @@
/*
xdrv_73_4_lorawan_cryptography.ino - LoRaWan cryptography support for Tasmota
SPDX-FileCopyrightText: 2024 Theo Arends
SPDX-License-Identifier: GPL-3.0-only
*/
#ifdef USE_SPI_LORA
#ifdef USE_LORAWAN
/*********************************************************************************************\
* LoRaWan cryptography
\*********************************************************************************************/
//#define USE_LORAWAN_TEST
#include <utils/Cryptography.h>
uint32_t LoraWanGenerateMIC(uint8_t* msg, size_t len, uint8_t* key) {
if((msg == NULL) || (len == 0)) {
return(0);
}
RadioLibAES128Instance.init(key);
uint8_t cmac[TAS_LORAWAN_AES128_KEY_SIZE];
RadioLibAES128Instance.generateCMAC(msg, len, cmac);
return(((uint32_t)cmac[0]) | ((uint32_t)cmac[1] << 8) | ((uint32_t)cmac[2] << 16) | ((uint32_t)cmac[3]) << 24);
}
// deriveLegacySKey derives a session key
void _LoraWanDeriveLegacySKey(uint8_t* key, uint8_t t, uint32_t jn, uint32_t nid, uint16_t dn, uint8_t* derived) {
uint8_t buf[TAS_LORAWAN_AES128_KEY_SIZE] = { 0 };
buf[0] = t;
buf[1] = jn;
buf[2] = jn >> 8;
buf[3] = jn >> 16;
buf[4] = nid;
buf[5] = nid >> 8;
buf[6] = nid >> 16;
buf[7] = dn;
buf[8] = dn >> 8;
// AddLog(LOG_LEVEL_DEBUG, PSTR("DBG: buf %16_H"), buf);
RadioLibAES128Instance.init(key);
RadioLibAES128Instance.encryptECB(buf, TAS_LORAWAN_AES128_KEY_SIZE, derived);
}
// DeriveLegacyAppSKey derives the LoRaWAN Application Session Key
// - If a LoRaWAN 1.0 device joins a LoRaWAN 1.0/1.1 network, the AppKey is used as "key"
// - If a LoRaWAN 1.1 device joins a LoRaWAN 1.0 network, the NwkKey is used as "key"
void _LoraWanDeriveLegacyAppSKey(uint8_t* key, uint32_t jn, uint32_t nid, uint16_t dn, uint8_t* AppSKey) {
_LoraWanDeriveLegacySKey(key, 0x02, jn, nid, dn, AppSKey);
// AddLog(LOG_LEVEL_DEBUG, PSTR("DBG: key %16_H, AppSKey %16_H"), key, AppSKey);
}
void LoraWanDeriveLegacyAppSKey(uint32_t node, uint8_t* AppSKey) {
_LoraWanDeriveLegacyAppSKey(LoraSettings.end_node[node].AppKey, TAS_LORAWAN_JOINNONCE +node, TAS_LORAWAN_NETID, LoraSettings.end_node[node].DevNonce, AppSKey);
}
// DeriveLegacyNwkSKey derives the LoRaWAN 1.0 Network Session Key. AppNonce is entered as JoinNonce.
// - If a LoRaWAN 1.0 device joins a LoRaWAN 1.0/1.1 network, the AppKey is used as "key"
// - If a LoRaWAN 1.1 device joins a LoRaWAN 1.0 network, the NwkKey is used as "key"
void _LoraWanDeriveLegacyNwkSKey(uint8_t* appKey, uint32_t jn, uint32_t nid, uint16_t dn, uint8_t* NwkSKey) {
_LoraWanDeriveLegacySKey(appKey, 0x01, jn, nid, dn, NwkSKey);
// AddLog(LOG_LEVEL_DEBUG, PSTR("DBG: appKey %16_H, NwkSKey %16_H"), appKey, NwkSKey);
}
void LoraWanDeriveLegacyNwkSKey(uint32_t node, uint8_t* NwkSKey) {
_LoraWanDeriveLegacyNwkSKey(LoraSettings.end_node[node].AppKey, TAS_LORAWAN_JOINNONCE +node, TAS_LORAWAN_NETID, LoraSettings.end_node[node].DevNonce, NwkSKey);
}
#ifdef USE_LORAWAN_TEST
/*-------------------------------------------------------------------------------------------*/
void LoraWanTestKeyDerivation(void) {
uint8_t key[] = {0xBE, 0xC4, 0x99, 0xC6, 0x9E, 0x9C, 0x93, 0x9E, 0x41, 0x3B, 0x66, 0x39, 0x61, 0x63, 0x6C, 0x61};
uint16_t dn = 0x7369; // dn := types.DevNonce{0x73, 0x69}
uint32_t nid = 0x00020101; // nid := types.NetID{0x02, 0x01, 0x01}
uint32_t jn = 0x00AE3B1C; // jn := types.JoinNonce{0xAE, 0x3B, 0x1C}
uint8_t appSKey[TAS_LORAWAN_AES128_KEY_SIZE];
_LoraWanDeriveLegacyAppSKey(key, jn, nid, dn, appSKey);
AddLog(LOG_LEVEL_DEBUG, PSTR("TST: LoraWanDeriveLegacyAppSKey %16_H"), appSKey);
// a.So(appSKey, should.Equal, types.AES128Key{0x8C, 0x1E, 0x05, 0x43, 0xA2, 0x29, 0x08, 0x8D, 0xE6, 0xF8, 0x4E, 0x74, 0xBB, 0x46, 0xBD, 0x62})
uint8_t nwkSKey[TAS_LORAWAN_AES128_KEY_SIZE];
_LoraWanDeriveLegacyNwkSKey(key, jn, nid, dn, nwkSKey);
AddLog(LOG_LEVEL_DEBUG, PSTR("TST: LoraWanDeriveLegacyNwkSKey %16_H"), nwkSKey);
// a.So(nwkSKey, should.Equal, types.AES128Key{0x0D, 0xB9, 0x24, 0xEE, 0x6A, 0xF9, 0x06, 0x98, 0xE0, 0x5F, 0xC7, 0xCE, 0x48, 0x30, 0x3C, 0x01})
}
#endif // USE_LORAWAN_TEST
/*********************************************************************************************/
//func EncryptMessage(key types.AES128Key, dir uint8, addr types.DevAddr, fCnt uint32, payload []byte, opts ...EncryptionOption) ([]byte, error) {
void LoraWanProcessAES(uint8_t* in, size_t len, uint8_t* key, uint8_t* out, uint32_t addr, uint32_t fcnt, uint8_t dir, uint8_t ctrId, bool counter) {
// figure out how many encryption blocks are there
size_t numBlocks = len / TAS_LORAWAN_AES128_KEY_SIZE;
if (len % TAS_LORAWAN_AES128_KEY_SIZE) {
numBlocks++;
}
// generate the encryption blocks
uint8_t encBuffer[TAS_LORAWAN_AES128_KEY_SIZE] = { 0 };
uint8_t encBlock[TAS_LORAWAN_AES128_KEY_SIZE] = { 0 };
encBlock[0] = 0x01;
encBlock[4] = ctrId;
encBlock[5] = dir;
encBlock[6] = addr;
encBlock[7] = addr >> 8;
encBlock[8] = addr >> 16;
encBlock[9] = addr >> 24;
encBlock[10] = fcnt;
encBlock[11] = fcnt >> 8;
encBlock[12] = fcnt >> 16;
encBlock[13] = fcnt >> 24;
// now encrypt the input
// on downlink frames, this has a decryption effect because server actually "decrypts" the plaintext
size_t remLen = len;
for (size_t i = 0; i < numBlocks; i++) {
if (counter) {
encBlock[15] = i + 1;
}
// encrypt the buffer
RadioLibAES128Instance.init(key);
RadioLibAES128Instance.encryptECB(encBlock, TAS_LORAWAN_AES128_KEY_SIZE, encBuffer);
// now xor the buffer with the input
size_t xorLen = remLen;
if (xorLen > TAS_LORAWAN_AES128_KEY_SIZE) {
xorLen = TAS_LORAWAN_AES128_KEY_SIZE;
}
for (uint8_t j = 0; j < xorLen; j++) {
out[i * TAS_LORAWAN_AES128_KEY_SIZE + j] = in[i * TAS_LORAWAN_AES128_KEY_SIZE + j] ^ encBuffer[j];
}
remLen -= xorLen;
}
}
// DecryptUplink decrypts an uplink payload
// - The payload contains the FRMPayload bytes
// - For FPort>0, the AppSKey is used
// - For FPort=0, the NwkSEncKey/NwkSKey is used
void LoraWanDecryptUplink(uint8_t* key, uint32_t addr, uint32_t fCnt, uint8_t* payload, size_t payload_len, uint32_t opts, uint8_t* res) {
LoraWanProcessAES(payload, payload_len, key, res, addr, fCnt, 0, opts, true);
}
// EncryptDownlink encrypts a downlink payload
// - The payload contains the FRMPayload bytes
// - For FPort>0, the AppSKey is used
// - For FPort=0, the NwkSEncKey/NwkSKey is used
//func EncryptDownlink(key types.AES128Key, addr types.DevAddr, fCnt uint32, payload []byte, opts ...EncryptionOption) ([]byte, error) {
// return encryptMessage(key, 1, addr, fCnt, payload, opts...)
void LoraWanEncryptDownlink(uint8_t* key, uint32_t addr, uint32_t fCnt, uint8_t* payload, size_t payload_len, uint32_t opts, uint8_t* res) {
LoraWanProcessAES(payload, payload_len, key, res, addr, fCnt, 1, opts, true);
}
#ifdef USE_LORAWAN_TEST
/*-------------------------------------------------------------------------------------------*/
void LorWanTestUplinkDownlinkEncryption(void) {
uint8_t key[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
uint32_t addr = 0x01020304; // addr := types.DevAddr{1, 2, 3, 4}
uint32_t frameIdentifier = 0x0000001; // frameIdentifier := [4]byte{0x00, 0x00, 0x00, 0x01}
uint8_t res[TAS_LORAWAN_AES128_KEY_SIZE] = { 0 };
// FRM Payload
uint8_t payloadd[] = {0xCF, 0xF3, 0x0B, 0x4E};
LoraWanDecryptUplink(key, addr, 1, payloadd, 4, 0, res);
AddLog(LOG_LEVEL_DEBUG, PSTR("TST: LoraWanDecryptUplink %4_H"), res);
// a.So(res, should.Resemble, []byte{1, 2, 3, 4})
uint8_t payloade[] = {1, 2, 3, 4};
LoraWanEncryptDownlink(key, addr, 1, payloade, 4, 0, res);
AddLog(LOG_LEVEL_DEBUG, PSTR("TST: LoraWanEncryptDownlink %4_H"), res);
// a.So(res, should.Resemble, []byte{0x4E, 0x75, 0xF4, 0x40})
}
#endif // USE_LORAWAN_TEST
/*********************************************************************************************/
uint32_t LoraWanComputeMIC(uint8_t* key, uint8_t dir, uint16_t confFCnt, uint32_t addr, uint32_t fCnt, uint8_t* payload, size_t payload_len) {
uint8_t b0[TAS_LORAWAN_AES128_KEY_SIZE + payload_len]; // error: variable-sized object 'b0' may not be initialized
memset(b0, 0, sizeof(b0));
b0[0] = 0x49;
b0[1] = confFCnt;
b0[2] = confFCnt >> 8;
b0[5] = dir;
b0[6] = addr;
b0[7] = addr >> 8;
b0[8] = addr >> 16;
b0[9] = addr >> 24;
b0[10] = fCnt;
b0[11] = fCnt >> 8;
b0[15] = payload_len;
memcpy(&b0[16], payload, payload_len);
return LoraWanGenerateMIC(b0, sizeof(b0), key);
}
// ComputeLegacyUplinkMIC computes the Uplink Message Integrity Code.
// - The payload contains MHDR | FHDR | FPort | FRMPayload
// - The NwkSKey is used
uint32_t LoraWanComputeLegacyUplinkMIC(uint8_t* key, uint32_t addr, uint32_t fCnt, uint8_t* payload, uint8_t payload_len) {
return LoraWanComputeMIC(key, 0, 0, addr, fCnt, payload, payload_len);
}
// ComputeLegacyDownlinkMIC computes the Downlink Message Integrity Code.
// - The payload contains MHDR | FHDR | FPort | FRMPayload
// - The NwkSKey is used
uint32_t LoraWanComputeLegacyDownlinkMIC(uint8_t* key, uint32_t addr, uint32_t fCnt, uint8_t* payload, uint8_t payload_len) {
return LoraWanComputeMIC(key, 1, 0, addr, fCnt, payload, payload_len);
}
#ifdef USE_LORAWAN_TEST
/*-------------------------------------------------------------------------------------------*/
void LorWanTestUplinkDownlinkMIC(void) {
uint8_t key[] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
uint32_t addr = 0x01020304; // addr := types.DevAddr{1, 2, 3, 4}
uint8_t payloadWithoutMIC[] ={
0x40, // Unconfirmed Uplink
0x04, 0x03, 0x02, 0x01, // DevAddr 01020304
0x00, // Empty FCtrl
0x01, 0x00, // FCnt 1
0x01, // FPort 1
0x01, 0x02, 0x03, 0x04 // Data
};
uint32_t upMIC = LoraWanComputeLegacyUplinkMIC(key, addr, 1, payloadWithoutMIC, sizeof(payloadWithoutMIC));
AddLog(LOG_LEVEL_DEBUG, PSTR("TST: LoraWanComputeLegacyUplinkMIC %08X"), upMIC);
// a.So(mic, should.Equal, [4]byte{0x3B, 0x07, 0x31, 0x82}) - as uint32_t 0x8231073B
uint32_t dnMIC = LoraWanComputeLegacyDownlinkMIC(key, addr, 1, payloadWithoutMIC, sizeof(payloadWithoutMIC));
AddLog(LOG_LEVEL_DEBUG, PSTR("TST: LoraWanComputeLegacyDownlinkMIC %08X"), dnMIC);
// a.So(mic, should.Equal, [4]byte{0xA5, 0x60, 0x9F, 0xA9}) - as uint32_t 0xA99F60A5
}
#endif // USE_LORAWAN_TEST
#endif // USE_LORAWAN
#endif // USE_SPI_LORA

View File

@ -0,0 +1,700 @@
/*
xdrv_73_8_lorawan.ino - LoRaWan EU868 support for Tasmota
SPDX-FileCopyrightText: 2024 Theo Arends
SPDX-License-Identifier: GPL-3.0-only
*/
#ifdef USE_SPI_LORA
#ifdef USE_LORAWAN_BRIDGE
/*********************************************************************************************\
* EU868 LoRaWan bridge support
*
* The goal of the LoRaWan Bridge is to receive from joined LoRaWan End-Devices or Nodes and
* provide decrypted MQTT data.
*
* EU868 LoRaWan uses at minimum alternating 3 frequencies and 6 spreadingfactors (datarate or DR)
* which makes the use of single fixed frequency and spreadingfactor hardware like
* SX127x (LiliGo T3, M5 LoRa868 or RFM95W) or SX126x (LiLiGo T3S3) a challenge.
* This driver uses one fixed frequency and spreadingfactor trying to tell the End-Device to do
* the same. In some cases the End-Device needs to be (serial) configured to use a single
* channel and fixed datarate.
*
* To be able to do this:
* - Tasmota needs a filesystem to store persistent data (#define USE_UFILESYS)
* - Tasmota Lora has to be configured for one of the three EU868 supported frequencies and
* spreadingfactor using command
* LoRaConfig 2
* or individual commands
* LoRaFrequency 868.1 (or 868.3 and 868.5)
* LoRaSpreadingFactor 9 (or 7..12 equals LoRaWan DataRate 5..0)
* LoRaBandwidth 125
* LoRaCodingRate4 5
* LoRaSyncWord 52
* - LoRaWan has to be enabled (#define USE_LORAWAN_BRIDGE) and configured for the End-Device
* 32 character AppKey using command LoRaWanAppKey <vendor provided appkey>
* - The End-Device needs to start it's LoRaWan join process as documented by vendor.
* This will configure the LoRanWan bridge with the needed information. The bridge will also
* try to configure the device for single frequency and datarate. The process can take
* over 5 minutes as the device transmits at different frequencies and DR.
* - If all's well MQTT data will appear when the End-Device sends it's data.
* - If a device does not support Adaptive Datarate Control (ADR) the device needs to be set to
* a fixed frequency and datarate externally. As an example the Dragino LDS02 needs to be
* configured before joining using it's serial interface with the following commands
* Password 123456
* AT+CHS=868100000
* AT+CADR=0
* AT+CDATARATE=3
*
* The current driver supports decoding for Dragino LDS02 Door/Window sensor and MerryIoT
* DW10 Door/Window sensor. Both battery powered.
*
* The MQTT output can be user defined with SetOption commands like the Zigbee driver
* SetOption89 - Distinct MQTT topics per device (1) (#7835)
* SetOption100 - Remove LwReceived form JSON message (1)
* SetOption118 - Move LwReceived from JSON message and into the subtopic replacing "SENSOR"
* SetOption119 - Remove the device addr from json payload, can be used with LoRaWanName
* where the addr is already known from the topic
* SetOption144 - Include time in `LwReceived` messages like other sensors (SO100 0 and SO118 0)
* LoRaOption3 - Disable driver decoding (1) and provide decrypted payload for decoding
*
* The LoRaWanBridge is default off. Enable it with command
* LoRaWanBridge 1
\*********************************************************************************************/
/*********************************************************************************************\
* Driver Settings load and save
\*********************************************************************************************/
#ifdef USE_UFILESYS
#define D_JSON_APPKEY "AppKey"
#define D_JSON_DEVEUI "DevEUI"
#define D_JSON_DEVNONCE "DevNonce"
#define D_JSON_FCNTUP "FCntUp"
#define D_JSON_FCNTDOWN "FCntDown"
#define D_JSON_FLAGS "Flags"
bool LoraWanLoadData(void) {
char key[12]; // Max 99 nodes (drvset73_1 to drvset73_99)
for (uint32_t n = 0; n < TAS_LORAWAN_ENDNODES; n++) {
snprintf_P(key, sizeof(key), PSTR(XDRV_73_KEY "_%d"), n +1);
String json = UfsJsonSettingsRead(key);
if (json.length() == 0) { continue; } // Only load used slots
// {"AppKey":"00000000000000000000000000000000","DevEUI","0000000000000000","DevNonce":0,"FCntUp":0,"FCntDown":0,"Flags":0,"NAME":""}
JsonParser parser((char*)json.c_str());
JsonParserObject root = parser.getRootObject();
if (!root) { continue; } // Only load used slots
const char* app_key = nullptr;
app_key = root.getStr(PSTR(D_JSON_APPKEY), nullptr);
if (strlen(app_key)) {
size_t out_len = TAS_LORAWAN_AES128_KEY_SIZE;
HexToBytes(app_key, LoraSettings.end_node[n].AppKey, &out_len);
}
LoraSettings.end_node[n].DevEUIh = root.getUInt(PSTR(D_JSON_DEVEUI "h"), LoraSettings.end_node[n].DevEUIh);
LoraSettings.end_node[n].DevEUIl = root.getUInt(PSTR(D_JSON_DEVEUI "l"), LoraSettings.end_node[n].DevEUIl);
LoraSettings.end_node[n].DevNonce = root.getUInt(PSTR(D_JSON_DEVNONCE), LoraSettings.end_node[n].DevNonce);
LoraSettings.end_node[n].FCntUp = root.getUInt(PSTR(D_JSON_FCNTUP), LoraSettings.end_node[n].FCntUp);
LoraSettings.end_node[n].FCntDown = root.getUInt(PSTR(D_JSON_FCNTDOWN), LoraSettings.end_node[n].FCntDown);
LoraSettings.end_node[n].flags = root.getUInt(PSTR(D_JSON_FLAGS), LoraSettings.end_node[n].flags);
const char* name = nullptr;
name = root.getStr(PSTR(D_JSON_NAME), nullptr);
if (strlen(app_key)) {
LoraSettings.end_node[n].name = name;
}
}
return true;
}
bool LoraWanSaveData(void) {
bool result = false;
for (uint32_t n = 0; n < TAS_LORAWAN_ENDNODES; n++) {
if (LoraSettings.end_node[n].AppKey[0] > 0) { // Only save used slots
Response_P(PSTR("{\"" XDRV_73_KEY "_%d\":{\"" D_JSON_APPKEY "\":\"%16_H\""
",\"" D_JSON_DEVEUI "h\":%lu,\"" D_JSON_DEVEUI "l\":%lu"
",\"" D_JSON_DEVNONCE "\":%u"
",\"" D_JSON_FCNTUP "\":%u,\"" D_JSON_FCNTDOWN "\":%u"
",\"" D_JSON_FLAGS "\":%u"
",\"" D_JSON_NAME "\":\"%s\"}}"),
n +1,
LoraSettings.end_node[n].AppKey,
LoraSettings.end_node[n].DevEUIh,LoraSettings.end_node[n].DevEUIl,
LoraSettings.end_node[n].DevNonce,
LoraSettings.end_node[n].FCntUp, LoraSettings.end_node[n].FCntDown,
LoraSettings.end_node[n].flags,
LoraSettings.end_node[n].name.c_str());
result = UfsJsonSettingsWrite(ResponseData());
}
}
return result;
}
void LoraWanDeleteData(void) {
char key[12]; // Max 99 nodes (drvset73_1 to drvset73_99)
for (uint32_t n = 0; n < TAS_LORAWAN_ENDNODES; n++) {
snprintf_P(key, sizeof(key), PSTR(XDRV_73_KEY "_%d"), n +1);
UfsJsonSettingsDelete(key); // Use defaults
}
}
#endif // USE_UFILESYS
/*********************************************************************************************/
#include <Ticker.h>
Ticker LoraWan_Send;
void LoraWanTickerSend(void) {
Lorawan.send_buffer_step--;
if (1 == Lorawan.send_buffer_step) {
Lorawan.rx = true; // Always send during RX1
Lora.receive_time = 0; // Reset receive timer
LoraWan_Send.attach_ms(TAS_LORAWAN_RECEIVE_DELAY2, LoraWanTickerSend); // Retry after 1000 ms
}
if (Lorawan.rx) { // If received in RX1 do not resend in RX2
LoraSend(Lorawan.send_buffer, Lorawan.send_buffer_len, true);
}
if (Lorawan.send_buffer_step <= 0) {
LoraWan_Send.detach();
}
}
void LoraWanSendResponse(uint8_t* buffer, size_t len, uint32_t lorawan_delay) {
memcpy(Lorawan.send_buffer, buffer, sizeof(Lorawan.send_buffer));
Lorawan.send_buffer_len = len;
Lorawan.send_buffer_step = 2;
LoraWan_Send.attach_ms(lorawan_delay - TimePassedSince(Lora.receive_time), LoraWanTickerSend);
}
/*-------------------------------------------------------------------------------------------*/
uint32_t LoraWanSpreadingFactorToDataRate(void) {
// Allow only JoinReq message datarates (125kHz bandwidth)
if (LoraSettings.spreading_factor > 12) {
LoraSettings.spreading_factor = 12;
}
if (LoraSettings.spreading_factor < 7) {
LoraSettings.spreading_factor = 7;
}
LoraSettings.bandwidth = 125;
return 12 - LoraSettings.spreading_factor;
}
uint32_t LoraWanFrequencyToChannel(void) {
// EU863-870 (EU868) JoinReq message frequencies are 868.1, 868.3 and 868.5
uint32_t frequency = (LoraSettings.frequency * 10);
uint32_t channel = 250;
if (8681 == frequency) {
channel = 0;
}
else if (8683 == frequency) {
channel = 1;
}
else if (8685 == frequency) {
channel = 2;
}
if (250 == channel) {
LoraSettings.frequency = 868.1;
Lora.Config();
channel = 0;
}
return channel;
}
/*********************************************************************************************/
void LoraWanPublishHeader(uint32_t node) {
ResponseClear(); // clear string
// Do we prefix with `LwReceived`?
if (!Settings->flag4.remove_zbreceived && // SetOption100 - (Zigbee) Remove LwReceived form JSON message (1)
!Settings->flag5.zb_received_as_subtopic) { // SetOption118 - (Zigbee) Move LwReceived from JSON message and into the subtopic replacing "SENSOR" default
if (Settings->flag5.zigbee_include_time && // SetOption144 - (Zigbee) Include time in `LwReceived` messages like other sensors
(Rtc.utc_time >= START_VALID_TIME)) {
// Add time if needed (and if time is valid)
ResponseAppendTimeFormat(Settings->flag2.time_format); // CMND_TIME
ResponseAppend_P(PSTR(",\"LwReceived\":"));
} else {
ResponseAppend_P(PSTR("{\"LwReceived\":"));
}
}
if (!Settings->flag5.zb_omit_json_addr) { // SetOption119 - (Zigbee) Remove the device addr from json payload, can be used with zb_topic_fname where the addr is already known from the topic
ResponseAppend_P(PSTR("{\"%s\":"), EscapeJSONString(LoraSettings.end_node[node].name.c_str()).c_str());
}
ResponseAppend_P(PSTR("{\"Node\":%d,\"" D_JSON_ZIGBEE_DEVICE "\":\"0x%04X\""), node +1, LoraSettings.end_node[node].DevEUIl & 0x0000FFFF);
if (!LoraSettings.end_node[node].name.startsWith(F("0x"))) {
ResponseAppend_P(PSTR(",\"" D_JSON_ZIGBEE_NAME "\":\"%s\""), EscapeJSONString(LoraSettings.end_node[node].name.c_str()).c_str());
}
ResponseAppend_P(PSTR(",\"RSSI\":%1_f,\"SNR\":%1_f"), &Lora.rssi, &Lora.snr);
}
void LoraWanPublishFooter(uint32_t node) {
if (!Settings->flag5.zb_omit_json_addr) { // SetOption119 - (Zigbee) Remove the device addr from json payload, can be used with zb_topic_fname where the addr is already known from the topic
ResponseAppend_P(PSTR("}"));
}
if (!Settings->flag4.remove_zbreceived && // SetOption100 - (Zigbee) Remove LwReceived form JSON message (1)
!Settings->flag5.zb_received_as_subtopic) { // SetOption118 - (Zigbee) Move LwReceived from JSON message and into the subtopic replacing "SENSOR" default
ResponseAppend_P(PSTR("}"));
}
#ifdef USE_INFLUXDB
InfluxDbProcess(1); // Use a copy of ResponseData
#endif
if (Settings->flag4.zigbee_distinct_topics) { // SetOption89 - (MQTT, Zigbee) Distinct MQTT topics per device for Zigbee (1) (#7835)
char subtopic[TOPSZ];
// Clean special characters
char stemp[TOPSZ];
strlcpy(stemp, LoraSettings.end_node[node].name.c_str(), sizeof(stemp));
MakeValidMqtt(0, stemp);
if (Settings->flag5.zigbee_hide_bridge_topic) { // SetOption125 - (Zigbee) Hide bridge topic from zigbee topic (use with SetOption89) (1)
snprintf_P(subtopic, sizeof(subtopic), PSTR("%s"), stemp);
} else {
snprintf_P(subtopic, sizeof(subtopic), PSTR("%s/%s"), TasmotaGlobal.mqtt_topic, stemp);
}
char stopic[TOPSZ];
if (Settings->flag5.zb_received_as_subtopic) // SetOption118 - (Zigbee) Move LwReceived from JSON message and into the subtopic replacing "SENSOR" default
GetTopic_P(stopic, TELE, subtopic, PSTR("LwReceived"));
else
GetTopic_P(stopic, TELE, subtopic, PSTR(D_RSLT_SENSOR));
MqttPublish(stopic, Settings->flag.mqtt_sensor_retain);
} else {
MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR), Settings->flag.mqtt_sensor_retain);
}
XdrvRulesProcess(0); // apply rules
}
/*********************************************************************************************/
void LoraWanSendLinkADRReq(uint32_t node) {
uint32_t DevAddr = Lorawan.device_address +node;
uint16_t FCnt = LoraSettings.end_node[node].FCntDown++;
uint8_t NwkSKey[TAS_LORAWAN_AES128_KEY_SIZE];
LoraWanDeriveLegacyNwkSKey(node, NwkSKey);
uint8_t data[32];
data[0] = 0xA0; // Confirmed data downlink
data[1] = DevAddr;
data[2] = DevAddr >> 8;
data[3] = DevAddr >> 16;
data[4] = DevAddr >> 24;
data[5] = 0x05; // FCtrl with 5 FOpts
data[6] = FCnt;
data[7] = FCnt >> 8;
data[8] = 0x03; // CId LinkADRReq to single channel LoraFrequency and DR LoraSpreadingFactor
data[9] = LoraWanSpreadingFactorToDataRate() << 4 | 0x0F; // DataRate 3 and unchanged TXPower
data[10] = 0x01 << LoraWanFrequencyToChannel();
data[11] = 0x00;
data[12] = 0x00; // ChMaskCntl applies to Channels0..15, NbTrans is default (1)
uint32_t MIC = LoraWanComputeLegacyDownlinkMIC(NwkSKey, DevAddr, FCnt, data, 13);
data[13] = MIC;
data[14] = MIC >> 8;
data[15] = MIC >> 16;
data[16] = MIC >> 24;
// A0 F3F51700 05 0000 033F010000 0B2C1B8B
LoraWanSendResponse(data, 17, TAS_LORAWAN_RECEIVE_DELAY1);
}
bool LoraWanInput(uint8_t* data, uint32_t packet_size) {
bool result = false;
uint32_t MType = data[0] >> 5; // Upper three bits (used to be called FType)
if (0 == MType) { // Join request
// 0007010000004140A8D64A89710E4140A82893A8AD137F - Dragino
// 000600000000161600B51F000000161600FDA5D8127912 - MerryIoT
uint64_t JoinEUI = (uint64_t)data[ 1] | ((uint64_t)data[ 2] << 8) |
((uint64_t)data[ 3] << 16) | ((uint64_t)data[ 4] << 24) |
((uint64_t)data[ 5] << 32) | ((uint64_t)data[ 6] << 40) |
((uint64_t)data[ 7] << 48) | ((uint64_t)data[ 8] << 56);
uint32_t DevEUIl = (uint32_t)data[ 9] | ((uint32_t)data[10] << 8) | // Use uint32_t to fix easy JSON migration
((uint32_t)data[11] << 16) | ((uint32_t)data[12] << 24);
uint32_t DevEUIh = (uint32_t)data[13] | ((uint32_t)data[14] << 8) |
((uint32_t)data[15] << 16) | ((uint32_t)data[16] << 24);
uint16_t DevNonce = (uint16_t)data[17] | ((uint16_t)data[18] << 8);
uint32_t MIC = (uint32_t)data[19] | ((uint32_t)data[20] << 8) |
((uint32_t)data[21] << 16) | ((uint32_t)data[22] << 24);
for (uint32_t node = 0; node < TAS_LORAWAN_ENDNODES; node++) {
uint32_t CalcMIC = LoraWanGenerateMIC(data, 19, LoraSettings.end_node[node].AppKey);
if (MIC == CalcMIC) { // Valid MIC based on LoraWanAppKey
AddLog(LOG_LEVEL_DEBUG, PSTR("LOR: JoinEUI %8_H, DevEUIh %08X, DevEUIl %08X, DevNonce %04X, MIC %08X"),
(uint8_t*)&JoinEUI, DevEUIh, DevEUIl, DevNonce, MIC);
LoraSettings.end_node[node].DevEUIl = DevEUIl;
LoraSettings.end_node[node].DevEUIh = DevEUIh;
LoraSettings.end_node[node].DevNonce = DevNonce;
LoraSettings.end_node[node].FCntUp = 0;
LoraSettings.end_node[node].FCntDown = 0;
bitClear(LoraSettings.end_node[node].flags, TAS_LORAWAN_LINK_ADR_REQ);
if (LoraSettings.end_node[node].name.equals(F("0x0000"))) {
char name[10];
ext_snprintf_P(name, sizeof(name), PSTR("0x%04X"), LoraSettings.end_node[node].DevEUIl & 0x0000FFFF);
LoraSettings.end_node[node].name = name;
}
uint32_t JoinNonce = TAS_LORAWAN_JOINNONCE +node;
uint32_t DevAddr = Lorawan.device_address +node;
uint32_t NetID = TAS_LORAWAN_NETID;
uint8_t join_data[33] = { 0 };
join_data[0] = 0x20; // Join Accept
join_data[1] = JoinNonce;
join_data[2] = JoinNonce >> 8;
join_data[3] = JoinNonce >> 16;
join_data[4] = NetID;
join_data[5] = NetID >> 8;
join_data[6] = NetID >> 16;
join_data[7] = DevAddr;
join_data[8] = DevAddr >> 8;
join_data[9] = DevAddr >> 16;
join_data[10] = DevAddr >> 24;
join_data[11] = 0x03; // DLSettings
join_data[12] = 1; // RXDelay;
uint32_t NewMIC = LoraWanGenerateMIC(join_data, 13, LoraSettings.end_node[node].AppKey);
join_data[13] = NewMIC;
join_data[14] = NewMIC >> 8;
join_data[15] = NewMIC >> 16;
join_data[16] = NewMIC >> 24;
uint8_t EncData[33];
EncData[0] = join_data[0];
RadioLibAES128Instance.init(LoraSettings.end_node[node].AppKey);
RadioLibAES128Instance.decryptECB(&join_data[1], 16, &EncData[1]);
// AddLog(LOG_LEVEL_DEBUG, PSTR("DBG: Join %17_H"), join_data);
// 203106E5000000412E010003017CB31DD4 - Dragino
// 203206E5000000422E010003016A210EEA - MerryIoT
LoraWanSendResponse(EncData, 17, TAS_LORAWAN_JOIN_ACCEPT_DELAY1);
result = true;
break;
}
}
}
else if ((2 == MType) || // Unconfirmed data uplink
(4 == MType)) { // Confirmed data uplink
// 0 1 2 3 4 5 6 7 8 9 8 9101112131415... packet_size -4
// PHYPayload --------------------------------------------------------------
// MHDR MACPayload ---------------------------------------------- MIC ----
// MHDR FHDR ----------------------- FPort FRMPayload --------- MIC ----
// MHDR DevAddr FCtrl FCnt FOpts FPort FRMPayload --------- MIC ----
// 1 4 1 2 0..15 0..1 0..N 4 - Number of octets
// Not encrypted --------------------------- Encrypted ---------- Not encr
// - Dragino
// 40 412E0100 80 2500 0A 6A6FEFD6A16B0C7AC37B 5F95FABC - decrypt using AppSKey
// 80 412E0100 80 2A00 0A A58EF5E0D1DDE03424F0 6F2D56FA - decrypt using AppSKey
// 80 412E0100 80 2B00 0A 8F2F0D33E5C5027D57A6 F67C9DFE - decrypt using AppSKey
// 40 412E0100 A0 1800 00 0395 2C94B1D8 - FCtrl ADR support, Ack, FPort = 0 -> MAC commands, decrypt using NwkSKey
// 40 412E0100 A0 7800 00 78C9 A60D8977 - FCtrl ADR support, Ack, FPort = 0 -> MAC commands, decrypt using NwkSKey
// 40 F3F51700 20 0100 00 2A7C 407036A2 - FCtrl No ADR support, Ack, FPort = 0 -> MAC commands, decrypt using NwkSKey, response after LinkADRReq
// - MerryIoT
// 40 422E0100 80 0400 78 B9C75DF9E8934C6651 A57DA6B1 - decrypt using AppSKey
// 40 422E0100 80 0100 CC 7C462537AC00C07F99 5500BF2B - decrypt using AppSKey
// 40 422E0100 A2 1800 0307 78 29FBF8FD9227729984 8C71E95B - FCtrl ADR support, Ack, FOptsLen = 2 -> FOpts = MAC, response after LinkADRReq
// 40 F4F51700 A2 0200 0307 CC 6517D4AB06D32C9A9F 14CBA305 - FCtrl ADR support, Ack, FOptsLen = 2 -> FOpts = MAC, response after LinkADRReq
uint32_t DevAddr = (uint32_t)data[1] | ((uint32_t)data[2] << 8) | ((uint32_t)data[3] << 16) | ((uint32_t)data[4] << 24);
for (uint32_t node = 0; node < TAS_LORAWAN_ENDNODES; node++) {
if (0 == LoraSettings.end_node[node].DevEUIh) { continue; } // No DevEUI so never joined
if ((Lorawan.device_address +node) != DevAddr) { continue; } // Not my device
uint32_t FCtrl = data[5];
uint32_t FOptsLen = FCtrl & 0x0F;
uint32_t FCnt = (LoraSettings.end_node[node].FCntUp & 0xFFFF0000) | data[6] | (data[7] << 8);
uint8_t* FOpts = &data[8];
uint32_t FPort = data[8 +FOptsLen];
uint8_t* FRMPayload = &data[9 +FOptsLen];
uint32_t payload_len = packet_size -9 -FOptsLen -4;
uint32_t MIC = (uint32_t)data[packet_size -4] | ((uint32_t)data[packet_size -3] << 8) | ((uint32_t)data[packet_size -2] << 16) | ((uint32_t)data[packet_size -1] << 24);
uint8_t NwkSKey[TAS_LORAWAN_AES128_KEY_SIZE];
LoraWanDeriveLegacyNwkSKey(node, NwkSKey);
uint32_t CalcMIC = LoraWanComputeLegacyUplinkMIC(NwkSKey, DevAddr, FCnt, data, packet_size -4);
if (MIC != CalcMIC) { continue; } // Same device address but never joined
bool FCtrl_ADR = bitRead(FCtrl, 7);
bool FCtrl_ACK = bitRead(FCtrl, 5);
/*
if ((0 == FOptsLen) && (0 == FOpts[0])) { // MAC response
FOptsLen = payload_len;
FOpts = &data[9];
payload_len = 0;
}
*/
uint8_t payload_decrypted[TAS_LORAWAN_AES128_KEY_SIZE +9 +payload_len];
if (payload_len) {
uint8_t Key[TAS_LORAWAN_AES128_KEY_SIZE];
if (0 == FPort) {
LoraWanDeriveLegacyNwkSKey(node, Key);
} else {
LoraWanDeriveLegacyAppSKey(node, Key);
}
LoraWanDecryptUplink(Key, DevAddr, FCnt, FRMPayload, payload_len, 0, payload_decrypted);
}
uint32_t org_payload_len = payload_len; // Save for logging
if ((0 == FOptsLen) && (0 == FOpts[0])) { // MAC response in payload only
FOptsLen = payload_len; // Payload length is MAC length
FOpts = payload_decrypted; // Payload is encrypted MAC
payload_len = 0; // Payload is MAC only
}
#ifdef USE_LORA_DEBUG
AddLog(LOG_LEVEL_DEBUG, PSTR("LOR: DevAddr %08X, FCtrl %0X, FOptsLen %d, FCnt %d, FOpts %*_H, FPort %d, Payload %*_H, Decrypted %*_H, MIC %08X"),
DevAddr, FCtrl, FOptsLen, FCnt, FOptsLen, FOpts, FPort, org_payload_len, FRMPayload, org_payload_len, payload_decrypted, MIC);
#endif // USE_LORA_DEBUG
if (LoraSettings.end_node[node].FCntUp <= FCnt) { // Skip re-transmissions
Lorawan.rx = false; // Skip RX2 as this is a response from RX1
LoraSettings.end_node[node].FCntUp++;
if (LoraSettings.end_node[node].FCntUp < FCnt) { // Report missed frames
uint32_t FCnt_missed = FCnt - LoraSettings.end_node[node].FCntUp;
AddLog(LOG_LEVEL_DEBUG, PSTR("LOR: Missed frames %d"), FCnt_missed);
if (FCnt_missed > 1) { // Missed two or more frames
bitClear(LoraSettings.end_node[node].flags, TAS_LORAWAN_LINK_ADR_REQ); // Resend LinkADRReq
}
}
LoraSettings.end_node[node].FCntUp = FCnt;
if (FOptsLen) {
uint32_t i = 0;
while (i < FOptsLen) {
if (0x02 == FOpts[i]) { // Response from LinkCheckReq (LinkCheckAns)
// Used by end-device to validate it's connectivity to a network
// Need to send Margin/GWCnt
}
else if (0x03 == FOpts[i]) { // Response from LinkADRReq (LinkADRAns)
i++;
uint8_t status = FOpts[i];
AddLog(LOG_LEVEL_DEBUG, PSTR("LOR: MAC LinkADRAns PowerACK %d, DataRateACK %d, ChannelMaskACK %d"),
bitRead(status, 2), bitRead(status, 1), bitRead(status, 0));
bitSet(LoraSettings.end_node[node].flags, TAS_LORAWAN_LINK_ADR_REQ);
}
else if (0x04 == FOpts[i]) { // Response from DutyCycleReq (DutyCycleAns)
i++;
}
else if (0x05 == FOpts[i]) { // Response from RXParamSetupReq (RXParamSetupAns)
i++;
}
else if (0x06 == FOpts[i]) { // Response from DevStatusReq (DevStatusAns)
i++;
i++;
}
else if (0x07 == FOpts[i]) { // Response from NewChannelReq (NewChannelAns)
i++;
}
else if (0x08 == FOpts[i]) { // Response from RXTimingSetupReq (RXTimingSetupAns)
}
else if (0x09 == FOpts[i]) { // Response from TXParamSetupReq (TXParamSetupAns)
}
else if (0x0A == FOpts[i]) { // Response from DIChannelReq (DIChannelAns)
}
else if (0x0D == FOpts[i]) { // Response from DeviceTimeReq (DeviceTimeAns)
// Used by the end-device to request the current GPS time
// Need to send epoch/fractional second
}
else {
// RFU
}
i++;
}
}
if (payload_len) {
if (bitRead(LoraSettings.flags, TAS_LORAWAN_DECODE_ENABLED) &&
(0x00161600 == LoraSettings.end_node[node].DevEUIh)) { // MerryIoT
if (120 == FPort) { // MerryIoT door/window Sensor (DW10)
if (9 == payload_len) { // MerryIoT Sensor state
// 1 2 3 4 5 6 7 8 9
// 03 0F 19 2C 8A00 040000 - button
// 00 0F 19 2C 0000 050000 - door
uint8_t status = payload_decrypted[0];
float battery_volt = (float)(21 + payload_decrypted[1]) / 10.0;
int temperature = payload_decrypted[2];
int humidity = payload_decrypted[3];
uint32_t elapsed_time = payload_decrypted[4] | (payload_decrypted[5] << 8);
uint32_t events = payload_decrypted[6] | (payload_decrypted[7] << 8) | (payload_decrypted[8] << 16);
#ifdef USE_LORA_DEBUG
AddLog(LOG_LEVEL_DEBUG, PSTR("LOR: Node %d, DevEUI %08X%08X, Events %d, LastEvent %d min, DoorOpen %d, Button %d, Tamper %d, Tilt %d, Battery %1_fV, Temp %d, Hum %d"),
node +1, LoraSettings.end_node[node].DevEUIh, LoraSettings.end_node[node].DevEUIl,
events, elapsed_time,
bitRead(status, 0), bitRead(status, 1), bitRead(status, 2), bitRead(status, 3),
&battery_volt,
temperature, humidity);
#endif // USE_LORA_DEBUG
LoraWanPublishHeader(node);
ResponseAppend_P(PSTR(",\"Events\":%d,\"LastEvent\":%d,\"DoorOpen\":%d,\"Button\":%d,\"Tamper\":%d,\"Tilt\":%d"
",\"Battery\":%1_f,"),
events, elapsed_time,
bitRead(status, 0), bitRead(status, 1), bitRead(status, 2), bitRead(status, 3),
&battery_volt);
ResponseAppendTHD(temperature, humidity);
ResponseAppend_P(PSTR("}"));
LoraWanPublishFooter(node);
}
}
}
else if (bitRead(LoraSettings.flags, TAS_LORAWAN_DECODE_ENABLED) &&
(0xA840410E == LoraSettings.end_node[node].DevEUIh)) { // Dragino
// Dragino v1.7 fails to set DR with ADR so set it using serial interface:
// Password 123456
// AT+CHS=868100000
// Start join using reset button
// AT+CADR=0
// AT+CDATARATE=3
bitSet(LoraSettings.end_node[node].flags, TAS_LORAWAN_LINK_ADR_REQ);
if (10 == FPort) { // Dragino LDS02
// 8CD2 01 000010 000000 00 - Door Open, 3.282V
// 0CD2 01 000011 000000 00 - Door Closed
uint8_t status = payload_decrypted[0];
float battery_volt = (float)((payload_decrypted[1] | (payload_decrypted[0] << 8)) &0x3FFF) / 1000;
uint8_t MOD = payload_decrypted[2]; // Always 0x01
uint32_t events = payload_decrypted[5] | (payload_decrypted[4] << 8) | (payload_decrypted[3] << 16);
uint32_t open_duration = payload_decrypted[8] | (payload_decrypted[7] << 8) | (payload_decrypted[6] << 16);
uint8_t alarm = payload_decrypted[9];
#ifdef USE_LORA_DEBUG
AddLog(LOG_LEVEL_DEBUG, PSTR("LOR: Node %d, DevEUI %08X%08X, Events %d, LastEvent %d min, DoorOpen %d, Battery %3_fV, Alarm %d"),
node +1, LoraSettings.end_node[node].DevEUIh, LoraSettings.end_node[node].DevEUIl,
events, open_duration,
bitRead(status, 7),
&battery_volt,
bitRead(alarm, 0));
#endif // USE_LORA_DEBUG
LoraWanPublishHeader(node);
ResponseAppend_P(PSTR(",\"Events\":%d,\"LastEvent\":%d,\"DoorOpen\":%d,\"Alarm\":%d,\"Battery\":%3_f}"),
events, open_duration, bitRead(status, 7), bitRead(alarm, 0), &battery_volt);
LoraWanPublishFooter(node);
}
}
else {
// Joined device without decoding
LoraWanPublishHeader(node);
ResponseAppend_P(PSTR(",\"DevEUIh\":\"%08X\",\"DevEUIl\":\"%08X\",\"FPort\":%d,\"Payload\":["),
LoraSettings.end_node[node].DevEUIh, LoraSettings.end_node[node].DevEUIl, FPort);
for (uint32_t i = 0; i < payload_len; i++) {
ResponseAppend_P(PSTR("%s%d"), (0==i)?"":",", payload_decrypted[i]);
}
ResponseAppend_P(PSTR("]}"));
LoraWanPublishFooter(node);
}
if (4 == MType) { // Confirmed data uplink
data[0] = 0x60; // Unconfirmed data downlink
data[5] |= 0x20; // FCtrl Set ACK bit
uint16_t FCnt = LoraSettings.end_node[node].FCntDown++;
data[6] = FCnt;
data[7] = FCnt >> 8;
uint32_t MIC = LoraWanComputeLegacyDownlinkMIC(NwkSKey, DevAddr, FCnt, data, packet_size -4);
data[packet_size -4] = MIC;
data[packet_size -3] = MIC >> 8;
data[packet_size -2] = MIC >> 16;
data[packet_size -1] = MIC >> 24;
LoraWanSendResponse(data, packet_size, TAS_LORAWAN_RECEIVE_DELAY1);
}
}
if (2 == MType) { // Unconfirmed data uplink
if (!bitRead(LoraSettings.end_node[node].flags, TAS_LORAWAN_LINK_ADR_REQ) &&
FCtrl_ADR && !FCtrl_ACK) {
// Try to fix single channel and datarate
LoraWanSendLinkADRReq(node); // Resend LinkADRReq
}
}
}
result = true;
break;
}
}
Lora.receive_time = 0;
return result;
}
/*********************************************************************************************\
* Commands
\*********************************************************************************************/
#define D_CMND_LORAWANBRIDGE "Bridge"
#define D_CMND_LORAWANAPPKEY "AppKey"
#define D_CMND_LORAWANNAME "Name"
const char kLoraWanCommands[] PROGMEM = "LoRaWan|" // Prefix
D_CMND_LORAWANBRIDGE "|" D_CMND_LORAWANAPPKEY "|" D_CMND_LORAWANNAME;
void (* const LoraWanCommand[])(void) PROGMEM = {
&CmndLoraWanBridge, &CmndLoraWanAppKey, &CmndLoraWanName };
void CmndLoraWanBridge(void) {
// LoraWanBridge - Show LoraOption1
// LoraWanBridge 1 - Set LoraOption1 1 = Enable LoraWanBridge
uint32_t pindex = 0;
if (XdrvMailbox.payload >= 0) {
bitWrite(LoraSettings.flags, pindex, XdrvMailbox.payload);
}
#ifdef USE_LORAWAN_TEST
LoraWanTestKeyDerivation();
LorWanTestUplinkDownlinkEncryption();
LorWanTestUplinkDownlinkMIC();
#endif // USE_LORAWAN_TEST
ResponseCmndChar(GetStateText(bitRead(LoraSettings.flags, pindex)));
}
void CmndLoraWanAppKey(void) {
// LoraWanAppKey
// LoraWanAppKey2 0123456789abcdef0123456789abcdef
if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= TAS_LORAWAN_ENDNODES)) {
uint32_t node = XdrvMailbox.index -1;
if (32 == XdrvMailbox.data_len) {
size_t out_len = 16;
HexToBytes(XdrvMailbox.data, LoraSettings.end_node[node].AppKey, &out_len);
if (0 == LoraSettings.end_node[node].name.length()) {
LoraSettings.end_node[node].name = F("0x0000");
}
}
else if (0 == XdrvMailbox.payload) {
memset(&LoraSettings.end_node[node], 0, sizeof(tEndNode));
}
char appkey[33];
ext_snprintf_P(appkey, sizeof(appkey), PSTR("%16_H"), LoraSettings.end_node[node].AppKey);
ResponseCmndIdxChar(appkey);
}
}
void CmndLoraWanName(void) {
// LoraWanName
// LoraWanName 1 - Set to short DevEUI (or 0x0000 if not yet joined)
// LoraWanName2 LDS02a
if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= TAS_LORAWAN_ENDNODES)) {
uint32_t node = XdrvMailbox.index -1;
if (XdrvMailbox.data_len) {
if (1 == XdrvMailbox.payload) {
char name[10];
ext_snprintf_P(name, sizeof(name), PSTR("0x%04X"), LoraSettings.end_node[node].DevEUIl & 0x0000FFFF);
LoraSettings.end_node[node].name = name;
} else {
LoraSettings.end_node[node].name = XdrvMailbox.data;
}
}
ResponseCmndIdxChar(LoraSettings.end_node[node].name.c_str());
}
}
/*********************************************************************************************\
* Interface
\*********************************************************************************************/
void LoraWanInit(void) {
// The Things Network has been assigned a 7-bits "device address prefix" a.k.a. NwkID
// %0010011. Using that, TTN currently sends NetID 0x000013, and a TTN DevAddr always
// starts with 0x26 or 0x27
// Private networks are supposed to used NetID 0x000000.
Lorawan.device_address = (TAS_LORAWAN_NETID << 25) | (ESP_getChipId() & 0x01FFFFFF);
}
#endif // USE_LORAWAN_BRIDGE
#endif // USE_SPI_LORA

View File

@ -16,15 +16,196 @@
/*********************************************************************************************/ /*********************************************************************************************/
void LoraDefaults(void) {
LoraSettings.frequency = TAS_LORA_FREQUENCY;
LoraSettings.bandwidth = TAS_LORA_BANDWIDTH;
LoraSettings.spreading_factor = TAS_LORA_SPREADING_FACTOR;
LoraSettings.coding_rate = TAS_LORA_CODING_RATE;
LoraSettings.sync_word = TAS_LORA_SYNC_WORD;
LoraSettings.output_power = TAS_LORA_OUTPUT_POWER;
LoraSettings.preamble_length = TAS_LORA_PREAMBLE_LENGTH;
LoraSettings.current_limit = TAS_LORA_CURRENT_LIMIT;
LoraSettings.implicit_header = TAS_LORA_HEADER;
LoraSettings.crc_bytes = TAS_LORA_CRC_BYTES;
}
void LoraWanDefaults(void) {
LoraSettings.frequency = TAS_LORAWAN_FREQUENCY;
LoraSettings.bandwidth = TAS_LORAWAN_BANDWIDTH;
LoraSettings.spreading_factor = TAS_LORAWAN_SPREADING_FACTOR;
LoraSettings.coding_rate = TAS_LORAWAN_CODING_RATE;
LoraSettings.sync_word = TAS_LORAWAN_SYNC_WORD;
LoraSettings.output_power = TAS_LORAWAN_OUTPUT_POWER;
LoraSettings.preamble_length = TAS_LORAWAN_PREAMBLE_LENGTH;
LoraSettings.current_limit = TAS_LORAWAN_CURRENT_LIMIT;
LoraSettings.implicit_header = TAS_LORAWAN_HEADER;
LoraSettings.crc_bytes = TAS_LORAWAN_CRC_BYTES;
}
void LoraSettings2Json(void) {
ResponseAppend_P(PSTR("\"" D_JSON_FREQUENCY "\":%1_f"), &LoraSettings.frequency); // xxx.x MHz
ResponseAppend_P(PSTR(",\"" D_JSON_BANDWIDTH "\":%1_f"), &LoraSettings.bandwidth); // xxx.x kHz
ResponseAppend_P(PSTR(",\"" D_JSON_SPREADING_FACTOR "\":%d"), LoraSettings.spreading_factor);
ResponseAppend_P(PSTR(",\"" D_JSON_CODINGRATE4 "\":%d"), LoraSettings.coding_rate);
ResponseAppend_P(PSTR(",\"" D_JSON_SYNCWORD "\":%d"), LoraSettings.sync_word);
ResponseAppend_P(PSTR(",\"" D_JSON_OUTPUT_POWER "\":%d"), LoraSettings.output_power); // dBm
ResponseAppend_P(PSTR(",\"" D_JSON_PREAMBLE_LENGTH "\":%d"), LoraSettings.preamble_length); // symbols
ResponseAppend_P(PSTR(",\"" D_JSON_CURRENT_LIMIT "\":%1_f"), &LoraSettings.current_limit); // xx.x mA (Overcurrent Protection - OCP)
ResponseAppend_P(PSTR(",\"" D_JSON_IMPLICIT_HEADER "\":%d"), LoraSettings.implicit_header); // 0 = explicit
ResponseAppend_P(PSTR(",\"" D_JSON_CRC_BYTES "\":%d"), LoraSettings.crc_bytes); // bytes
}
void LoraJson2Settings(JsonParserObject root) {
LoraSettings.frequency = root.getFloat(PSTR(D_JSON_FREQUENCY), LoraSettings.frequency);
LoraSettings.bandwidth = root.getFloat(PSTR(D_JSON_BANDWIDTH), LoraSettings.bandwidth);
LoraSettings.spreading_factor = root.getUInt(PSTR(D_JSON_SPREADING_FACTOR), LoraSettings.spreading_factor);
LoraSettings.coding_rate = root.getUInt(PSTR(D_JSON_CODINGRATE4), LoraSettings.coding_rate);
LoraSettings.sync_word = root.getUInt(PSTR(D_JSON_SYNCWORD), LoraSettings.sync_word);
LoraSettings.output_power = root.getUInt(PSTR(D_JSON_OUTPUT_POWER), LoraSettings.output_power);
LoraSettings.preamble_length = root.getUInt(PSTR(D_JSON_PREAMBLE_LENGTH), LoraSettings.preamble_length);
LoraSettings.current_limit = root.getFloat(PSTR(D_JSON_CURRENT_LIMIT), LoraSettings.current_limit);
LoraSettings.implicit_header = root.getUInt(PSTR(D_JSON_IMPLICIT_HEADER), LoraSettings.implicit_header);
LoraSettings.crc_bytes = root.getUInt(PSTR(D_JSON_CRC_BYTES), LoraSettings.crc_bytes);
}
/*********************************************************************************************\
* Driver Settings load and save
\*********************************************************************************************/
#ifdef USE_UFILESYS
#define XDRV_73_KEY "drvset73"
bool LoraLoadData(void) {
char key[] = XDRV_73_KEY;
String json = UfsJsonSettingsRead(key);
if (json.length() == 0) { return false; }
// {"Crc":1882268982,"Flags":0,"Frequency":868.1,"Bandwidth":125.0,"SpreadingFactor":7,"CodingRate4":5,"SyncWord":52,"OutputPower":10,"PreambleLength":8,"CurrentLimit":60.0,"ImplicitHeader":0,"CrcBytes":2}
JsonParser parser((char*)json.c_str());
JsonParserObject root = parser.getRootObject();
if (!root) { return false; }
LoraSettings.crc32 = root.getUInt(PSTR("Crc"), LoraSettings.crc32);
LoraSettings.flags = root.getUInt(PSTR("Flags"), LoraSettings.flags);
LoraJson2Settings(root);
#ifdef USE_LORAWAN_BRIDGE
if (!LoraWanLoadData()) {
return false;
}
#endif // USE_LORAWAN_BRIDGE
return true;
}
bool LoraSaveData(void) {
Response_P(PSTR("{\"" XDRV_73_KEY "\":{"
"\"Crc\":%u,"
"\"Flags\":%u,"),
LoraSettings.crc32,
LoraSettings.flags);
LoraSettings2Json();
ResponseAppend_P(PSTR("}}"));
if (!UfsJsonSettingsWrite(ResponseData())) {
return false;
}
#ifdef USE_LORAWAN_BRIDGE
if (!LoraWanSaveData()) {
return false;
}
#endif // USE_LORAWAN_BRIDGE
return true;
}
void LoraDeleteData(void) {
char key[] = XDRV_73_KEY;
UfsJsonSettingsDelete(key); // Use defaults
#ifdef USE_LORAWAN_BRIDGE
LoraWanDeleteData();
#endif // USE_LORAWAN_BRIDGE
}
#endif // USE_UFILESYS
/*********************************************************************************************/
void LoraSettingsLoad(bool erase) {
// Called from FUNC_PRE_INIT (erase = 0) once at restart
// Called from FUNC_RESET_SETTINGS (erase = 1) after command reset 4, 5, or 6
// *** Start init default values in case key is not found ***
AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("DRV: " D_USE_DEFAULTS));
memset(&LoraSettings, 0x00, sizeof(LoraSettings));
// Init any other parameter in struct LoraSettings
LoraDefaults();
// *** End Init default values ***
#ifndef USE_UFILESYS
AddLog(LOG_LEVEL_INFO, PSTR("CFG: Lora use defaults as file system not enabled"));
#else
// Try to load key
if (erase) {
LoraDeleteData();
}
else if (LoraLoadData()) {
AddLog(LOG_LEVEL_INFO, PSTR("CFG: Lora loaded from file"));
}
else {
// File system not ready: No flash space reserved for file system
AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("CFG: Lora use defaults as file system not ready or key not found"));
}
#endif // USE_UFILESYS
}
void LoraSettingsSave(void) {
// Called from FUNC_SAVE_SETTINGS every SaveData second and at restart
#ifdef USE_UFILESYS
uint32_t crc32 = GetCfgCrc32((uint8_t*)&LoraSettings +4, sizeof(LoraSettings) -4); // Skip crc32
if (crc32 != LoraSettings.crc32) {
// Try to save file /.drvset122
LoraSettings.crc32 = crc32;
if (LoraSaveData()) {
AddLog(LOG_LEVEL_DEBUG, PSTR("CFG: Lora saved to file"));
} else {
// File system not ready: No flash space reserved for file system
AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("CFG: Lora ERROR File system not ready or unable to save file"));
}
}
#endif // USE_UFILESYS
}
/*********************************************************************************************/
bool LoraSend(uint8_t* data, uint32_t len, bool invert) {
uint32_t lora_time = millis(); // Time is important for LoRaWan RX windows
bool result = Lora.Send(data, len, invert);
AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("LOR: Send (%u) '%*_H', Invert %d, Time %d"),
lora_time, len, data, invert, TimePassedSince(lora_time));
return result;
}
void LoraInput(void) { void LoraInput(void) {
if (!Lora.Available()) { return; } if (!Lora.Available()) { return; }
char data[LORA_MAX_PACKET_LENGTH] = { 0 }; char data[TAS_LORA_MAX_PACKET_LENGTH] = { 0 };
int packet_size = Lora.Receive(data); int packet_size = Lora.Receive(data);
if (!packet_size) { return; } if (!packet_size) { return; }
AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("LOR: Rcvd (%u) '%*_H', RSSI %1_f, SNR %1_f"),
Lora.receive_time, packet_size, data, &Lora.rssi, &Lora.snr);
#ifdef USE_LORAWAN_BRIDGE
if (bitRead(LoraSettings.flags, TAS_LORAWAN_BRIDGE_ENABLED)) {
if (LoraWanInput((uint8_t*)data, packet_size)) {
return;
}
}
#endif // USE_LORAWAN_BRIDGE
Lora.receive_time = 0;
if (TAS_LORA_REMOTE_COMMAND == data[0]) { if (TAS_LORA_REMOTE_COMMAND == data[0]) {
char *payload = data +1; // Skip TAS_LORA_REMOTE_COMMAND char *payload = data +1; // Skip TAS_LORA_REMOTE_COMMAND
char *command_part; char *command_part;
char *topic_part = strtok_r(payload, " ", &command_part); char *topic_part = strtok_r(payload, " ", &command_part);
if (topic_part && command_part) { if (topic_part && command_part) {
@ -32,7 +213,7 @@ void LoraInput(void) {
ExecuteCommand(command_part, SRC_REMOTE); ExecuteCommand(command_part, SRC_REMOTE);
return; return;
} else { } else {
*--command_part = ' '; // Restore strtok_r '/0' *--command_part = ' '; // Restore strtok_r '/0'
} }
} }
} }
@ -63,19 +244,6 @@ void LoraInput(void) {
MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR("LoRaReceived")); MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR("LoRaReceived"));
} }
void LoraDefaults(void) {
Lora.frequency = TAS_LORA_FREQUENCY;
Lora.bandwidth = TAS_LORA_BANDWIDTH;
Lora.spreading_factor = TAS_LORA_SPREADING_FACTOR;
Lora.coding_rate = TAS_LORA_CODING_RATE;
Lora.sync_word = TAS_LORA_SYNC_WORD;
Lora.output_power = TAS_LORA_OUTPUT_POWER;
Lora.preamble_length = TAS_LORA_PREAMBLE_LENGTH;
Lora.current_limit = TAS_LORA_CURRENT_LIMIT;
Lora.implicit_header = TAS_LORA_HEADER;
Lora.crc_bytes = TAS_LORA_CRC_BYTES;
}
void LoraInit(void) { void LoraInit(void) {
if ((SPI_MOSI_MISO == TasmotaGlobal.spi_enabled) && if ((SPI_MOSI_MISO == TasmotaGlobal.spi_enabled) &&
(PinUsed(GPIO_LORA_CS)) && (PinUsed(GPIO_LORA_RST))) { (PinUsed(GPIO_LORA_CS)) && (PinUsed(GPIO_LORA_RST))) {
@ -86,8 +254,10 @@ void LoraInit(void) {
SPI.begin(Pin(GPIO_SPI_CLK), Pin(GPIO_SPI_MISO), Pin(GPIO_SPI_MOSI), -1); SPI.begin(Pin(GPIO_SPI_CLK), Pin(GPIO_SPI_MISO), Pin(GPIO_SPI_MOSI), -1);
#endif // ESP32 #endif // ESP32
Lora.enableInterrupt = true; #ifdef USE_LORAWAN_BRIDGE
LoraDefaults(); LoraWanInit();
#endif // USE_LORAWAN_BRIDGE
LoraSettingsLoad(0);
char hardware[20]; char hardware[20];
strcpy_P(hardware, PSTR("Not")); strcpy_P(hardware, PSTR("Not"));
@ -127,27 +297,42 @@ void LoraInit(void) {
* Commands * Commands
\*********************************************************************************************/ \*********************************************************************************************/
#define D_CMND_LORASEND "Send" #define D_CMND_LORASEND "Send"
#define D_CMND_LORACONFIG "Config" #define D_CMND_LORACONFIG "Config"
#define D_CMND_LORACOMMAND "Command" #define D_CMND_LORACOMMAND "Command"
#define D_CMND_LORAOPTION "Option"
const char kLoraCommands[] PROGMEM = "LoRa|" // Prefix const char kLoraCommands[] PROGMEM = "LoRa|" // Prefix
D_CMND_LORASEND "|" D_CMND_LORACONFIG "|" D_CMND_LORACOMMAND ; D_CMND_LORASEND "|" D_CMND_LORACONFIG "|" D_CMND_LORACOMMAND "|" D_CMND_LORAOPTION;
void (* const LoraCommand[])(void) PROGMEM = { void (* const LoraCommand[])(void) PROGMEM = {
&CmndLoraSend, &CmndLoraConfig, &CmndLoraCommand }; &CmndLoraSend, &CmndLoraConfig, &CmndLoraCommand, &CmndLoraOption };
void CmndLoraOption(void) {
// LoraOption1 1 - Enable LoRaWanBridge
// LoraOption2 1 - Enable LoRaWanBridge Join
// LoraOption3 1 - Enable LoRaWanBridge decoding
if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= 8)) {
uint32_t pindex = XdrvMailbox.index -1;
if (XdrvMailbox.payload >= 0) {
bitWrite(LoraSettings.flags, pindex, XdrvMailbox.payload);
}
ResponseCmndIdxChar(GetStateText(bitRead(LoraSettings.flags, pindex)));
}
}
void CmndLoraCommand(void) { void CmndLoraCommand(void) {
// LoRaCommand <topic_of_lora_receiver> <command> // LoRaCommand <topic_of_lora_receiver> <command>
// LoRaCommand lorareceiver power 2 // LoRaCommand lorareceiver power 2
// LoRaCommand lorareceiver publish cmnd/anytopic/power 2 // LoRaCommand lorareceiver publish cmnd/anytopic/power 2
// LoRaCommand lorareceiver LoRaCommand thisreceiver status
if (XdrvMailbox.data_len > 0) { if (XdrvMailbox.data_len > 0) {
char data[LORA_MAX_PACKET_LENGTH] = { 0 }; char data[TAS_LORA_MAX_PACKET_LENGTH] = { 0 };
XdrvMailbox.data_len++; // Add Signal CmndLoraCommand to lora receiver XdrvMailbox.data_len++; // Add Signal CmndLoraCommand to lora receiver
uint32_t len = (XdrvMailbox.data_len < LORA_MAX_PACKET_LENGTH -1) ? XdrvMailbox.data_len : LORA_MAX_PACKET_LENGTH -2; uint32_t len = (XdrvMailbox.data_len < TAS_LORA_MAX_PACKET_LENGTH -1) ? XdrvMailbox.data_len : TAS_LORA_MAX_PACKET_LENGTH -2;
data[0] = TAS_LORA_REMOTE_COMMAND; data[0] = TAS_LORA_REMOTE_COMMAND;
strlcpy(data +1, XdrvMailbox.data, len); strlcpy(data +1, XdrvMailbox.data, len);
Lora.Send((uint8_t*)data, len); LoraSend((uint8_t*)data, len, false);
ResponseCmndDone(); ResponseCmndDone();
} }
} }
@ -162,14 +347,19 @@ void CmndLoraSend(void) {
// LoRaSend4 "Hello Tiger" - Send "Hello Tiger" and set to binary decoding // LoRaSend4 "Hello Tiger" - Send "Hello Tiger" and set to binary decoding
// LoRaSend5 "AA004566" - Send "AA004566" as hex values // LoRaSend5 "AA004566" - Send "AA004566" as hex values
// LoRaSend6 "72,101,108,108" - Send decimals as hex values // LoRaSend6 "72,101,108,108" - Send decimals as hex values
// if (XdrvMailbox.index > 9) { XdrvMailbox.index -= 10; } // Allows leading spaces (not supported - See support_command/CommandHandler) // LoRaSend15 "AA004566" - Send "AA004566" as hex values with invert IQ
bool invert = false;
if (XdrvMailbox.index > 9) {
XdrvMailbox.index -= 10;
invert = true;
}
if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= 6)) { if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= 6)) {
Lora.raw = (XdrvMailbox.index > 3); // Global flag set even without data Lora.raw = (XdrvMailbox.index > 3); // Global flag set even without data
if (XdrvMailbox.data_len > 0) { if (XdrvMailbox.data_len > 0) {
char data[LORA_MAX_PACKET_LENGTH] = { 0 }; char data[TAS_LORA_MAX_PACKET_LENGTH] = { 0 };
uint32_t len = (XdrvMailbox.data_len < LORA_MAX_PACKET_LENGTH -1) ? XdrvMailbox.data_len : LORA_MAX_PACKET_LENGTH -2; uint32_t len = (XdrvMailbox.data_len < TAS_LORA_MAX_PACKET_LENGTH -1) ? XdrvMailbox.data_len : TAS_LORA_MAX_PACKET_LENGTH -2;
#ifdef USE_LORA_DEBUG #ifdef USE_LORA_DEBUG
// AddLog(LOG_LEVEL_DEBUG, PSTR("DBG: Len %d, Send %*_H"), len, len + 2, XdrvMailbox.data); // AddLog(LOG_LEVEL_DEBUG, PSTR("DBG: Len %d, Send %*_H"), len, len, XdrvMailbox.data);
#endif #endif
if (1 == XdrvMailbox.index) { // "Hello Tiger\n" if (1 == XdrvMailbox.index) { // "Hello Tiger\n"
memcpy(data, XdrvMailbox.data, len); memcpy(data, XdrvMailbox.data, len);
@ -193,7 +383,7 @@ void CmndLoraSend(void) {
while (size > 1) { while (size > 1) {
strlcpy(stemp, codes, sizeof(stemp)); strlcpy(stemp, codes, sizeof(stemp));
data[len++] = strtol(stemp, &p, 16); data[len++] = strtol(stemp, &p, 16);
if (len > LORA_MAX_PACKET_LENGTH -2) { break; } if (len > TAS_LORA_MAX_PACKET_LENGTH -2) { break; }
size -= 2; size -= 2;
codes += 2; codes += 2;
} }
@ -205,14 +395,14 @@ void CmndLoraSend(void) {
len = 0; len = 0;
for (char* str = strtok_r(values, ",", &p); str; str = strtok_r(nullptr, ",", &p)) { for (char* str = strtok_r(values, ",", &p); str; str = strtok_r(nullptr, ",", &p)) {
data[len++] = (uint8_t)atoi(str); data[len++] = (uint8_t)atoi(str);
if (len > LORA_MAX_PACKET_LENGTH -2) { break; } if (len > TAS_LORA_MAX_PACKET_LENGTH -2) { break; }
} }
} }
else { else {
len = 0; len = 0;
} }
if (len) { if (len) {
Lora.Send((uint8_t*)data, len); LoraSend((uint8_t*)data, len, invert);
} }
ResponseCmndDone(); ResponseCmndDone();
} }
@ -222,41 +412,31 @@ void CmndLoraSend(void) {
void CmndLoraConfig(void) { void CmndLoraConfig(void) {
// LoRaConfig - Show all parameters // LoRaConfig - Show all parameters
// LoRaConfig 1 - Set default parameters // LoRaConfig 1 - Set default parameters
// LoRaConfig 2 - Set default LoRaWan bridge parameters
// LoRaConfig {"Frequency":868.0,"Bandwidth":125.0} - Enter float parameters // LoRaConfig {"Frequency":868.0,"Bandwidth":125.0} - Enter float parameters
// LoRaConfig {"SyncWord":18} - Enter decimal parameter (=0x12) // LoRaConfig {"SyncWord":18} - Enter decimal parameter (=0x12)
if (XdrvMailbox.data_len > 0) { if (XdrvMailbox.data_len > 0) {
if (XdrvMailbox.payload == 1) { if (XdrvMailbox.payload == 1) {
LoraDefaults(); LoraDefaults();
Lora.Config(); Lora.Config();
} else { }
else if (XdrvMailbox.payload == 2) {
LoraWanDefaults();
Lora.Config();
}
else {
JsonParser parser(XdrvMailbox.data); JsonParser parser(XdrvMailbox.data);
JsonParserObject root = parser.getRootObject(); JsonParserObject root = parser.getRootObject();
if (root) { if (root) {
Lora.frequency = root.getFloat(PSTR(D_JSON_FREQUENCY), Lora.frequency); LoraJson2Settings(root);
Lora.bandwidth = root.getFloat(PSTR(D_JSON_BANDWIDTH), Lora.bandwidth);
Lora.spreading_factor = root.getUInt(PSTR(D_JSON_SPREADING_FACTOR), Lora.spreading_factor);
Lora.coding_rate = root.getUInt(PSTR(D_JSON_CODINGRATE4), Lora.coding_rate);
Lora.sync_word = root.getUInt(PSTR(D_JSON_SYNCWORD), Lora.sync_word);
Lora.output_power = root.getUInt(PSTR(D_JSON_OUTPUT_POWER), Lora.output_power);
Lora.preamble_length = root.getUInt(PSTR(D_JSON_PREAMBLE_LENGTH), Lora.preamble_length);
Lora.current_limit = root.getFloat(PSTR(D_JSON_CURRENT_LIMIT), Lora.current_limit);
Lora.implicit_header = root.getUInt(PSTR(D_JSON_IMPLICIT_HEADER), Lora.implicit_header);
Lora.crc_bytes = root.getUInt(PSTR(D_JSON_CRC_BYTES), Lora.crc_bytes);
Lora.Config(); Lora.Config();
} }
} }
} }
ResponseCmnd(); // {"LoRaConfig": ResponseCmnd(); // {"LoRaConfig":
ResponseAppend_P(PSTR("{\"" D_JSON_FREQUENCY "\":%1_f"), &Lora.frequency); // xxx.x MHz ResponseAppend_P(PSTR("{"));
ResponseAppend_P(PSTR(",\"" D_JSON_BANDWIDTH "\":%1_f"), &Lora.bandwidth); // xxx.x kHz LoraSettings2Json();
ResponseAppend_P(PSTR(",\"" D_JSON_SPREADING_FACTOR "\":%d"), Lora.spreading_factor); ResponseAppend_P(PSTR("}}"));
ResponseAppend_P(PSTR(",\"" D_JSON_CODINGRATE4 "\":%d"), Lora.coding_rate);
ResponseAppend_P(PSTR(",\"" D_JSON_SYNCWORD "\":%d"), Lora.sync_word);
ResponseAppend_P(PSTR(",\"" D_JSON_OUTPUT_POWER "\":%d"), Lora.output_power); // dBm
ResponseAppend_P(PSTR(",\"" D_JSON_PREAMBLE_LENGTH "\":%d"), Lora.preamble_length); // symbols
ResponseAppend_P(PSTR(",\"" D_JSON_CURRENT_LIMIT "\":%1_f"), &Lora.current_limit); // xx.x mA (Overcurrent Protection - OCP)
ResponseAppend_P(PSTR(",\"" D_JSON_IMPLICIT_HEADER "\":%d"), Lora.implicit_header); // 0 = explicit
ResponseAppend_P(PSTR(",\"" D_JSON_CRC_BYTES "\":%d}}"), Lora.crc_bytes); // bytes
} }
/*********************************************************************************************\ /*********************************************************************************************\
@ -275,8 +455,19 @@ bool Xdrv73(uint32_t function) {
case FUNC_SLEEP_LOOP: case FUNC_SLEEP_LOOP:
LoraInput(); LoraInput();
break; break;
case FUNC_RESET_SETTINGS:
LoraSettingsLoad(1);
break;
case FUNC_SAVE_SETTINGS:
LoraSettingsSave();
break;
case FUNC_COMMAND: case FUNC_COMMAND:
result = DecodeCommand(kLoraCommands, LoraCommand); result = DecodeCommand(kLoraCommands, LoraCommand);
#ifdef USE_LORAWAN_BRIDGE
if (!result) {
result = DecodeCommand(kLoraWanCommands, LoraWanCommand);
}
#endif // USE_LORAWAN_BRIDGE
break; break;
case FUNC_ACTIVE: case FUNC_ACTIVE:
result = true; result = true;