From f10218a2571b2ad3db822c3c5a2ad26416fbde29 Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Sat, 23 Mar 2024 15:20:22 +0100 Subject: [PATCH] Add support for LoRaWanBridge --- CHANGELOG.md | 16 +- RELEASENOTES.md | 14 +- .../xdrv_73_0_lora_struct.ino | 136 +++- .../xdrv_73_3_lora_sx126x.ino | 141 ++-- .../xdrv_73_3_lora_sx127x.ino | 78 +- .../xdrv_73_4_lorawan_cryptography.ino | 243 ++++++ .../xdrv_73_8_lorawan_bridge.ino | 700 ++++++++++++++++++ .../tasmota_xdrv_driver/xdrv_73_9_lora.ino | 299 ++++++-- 8 files changed, 1396 insertions(+), 231 deletions(-) create mode 100644 tasmota/tasmota_xdrv_driver/xdrv_73_4_lorawan_cryptography.ino create mode 100644 tasmota/tasmota_xdrv_driver/xdrv_73_8_lorawan_bridge.ino diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ce36f348..03f05718e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,16 +5,20 @@ All notable changes to this project will be documented in this file. ## [13.4.0.3] ### 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 ### Changed -- LVGL library from v9.0.0 to v9.1.0 +- ESP32 LVGL library from v9.0.0 to v9.1.0 (#21008) ### 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 @@ -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) - LVGL `lv.draw_label_dsc` and `lv_bar.get_indic_area` (#20936) - HASPmota support for scale, percentages (#20974) +- Support for ESP32-S3 120Mhz (#20973) +- Support for MCP23S08 (#20971) ### Breaking Changed - 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) - HASPmota moved to a distinct library `lv_haspmota` (#20929) - HASPmota solidify server-side (#20938) +- Refactor Platformio script `post_esp32.py` (#20966) ### Fixed - Berry bug when parsing ternary operator (#20839) - HASPmota widgets line, btnmatrix, qrcode, bar, checkbox (#20881) - Filesystem save of JSON settings data - Berry fix walrus with member or index (#20939) +- TuyaV2 suppressed dimmer updates from MQTT (#20950) ## [13.4.0.1] 20240229 ### Added -- Experimental support for LoRa +- Support for LoRa - HASPmota `pb.delete` to delete an object (#20735) - LVGL and HASPmota typicons font (#20742) - HASPmota more attributes (#20744) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a8c056b45..d3a0a5a91 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -118,12 +118,15 @@ The latter links can be used for OTA upgrades too like ``OtaUrl https://ota.tasm ## Changelog v13.4.0.3 ### 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) - 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) - 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) +- 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 `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) @@ -140,6 +143,8 @@ The latter links can be used for OTA upgrades too like ``OtaUrl https://ota.tasm ### Changed - 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) - 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) @@ -157,11 +162,16 @@ The latter links can be used for OTA upgrades too like ``OtaUrl https://ota.tasm ### Fixed - Filesystem save of JSON settings data - 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) +- 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 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 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 diff --git a/tasmota/tasmota_xdrv_driver/xdrv_73_0_lora_struct.ino b/tasmota/tasmota_xdrv_driver/xdrv_73_0_lora_struct.ino index 597df82ba..f552a035d 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_73_0_lora_struct.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_73_0_lora_struct.ino @@ -8,70 +8,154 @@ #ifdef USE_SPI #ifdef USE_SPI_LORA + +#ifdef USE_LORAWAN_BRIDGE +#define USE_LORAWAN +#endif /*********************************************************************************************\ * LoRa defines and global struct \*********************************************************************************************/ //#define USE_LORA_DEBUG -#define 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 XDRV_73_KEY "drvset73" #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 #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 #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 #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 #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 #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 #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 #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 #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 #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 +#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 { bool (* Config)(void); bool (* Available)(void); int (* Receive)(char*); - bool (* Send)(uint8_t*, uint32_t); + bool (* Send)(uint8_t*, uint32_t, bool); + uint32_t receive_time; float rssi; float snr; - 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 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 + uint8_t packet_size; // Max is 255 (LORA_MAX_PACKET_LENGTH) + volatile bool receivedFlag; // flag to indicate that a packet was received bool sendFlag; bool raw; bool present; } 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 diff --git a/tasmota/tasmota_xdrv_driver/xdrv_73_3_lora_sx126x.ino b/tasmota/tasmota_xdrv_driver/xdrv_73_3_lora_sx126x.ino index fa6a81df5..a7419884e 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_73_3_lora_sx126x.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_73_3_lora_sx126x.ino @@ -30,125 +30,76 @@ SX1262 LoRaRadio = nullptr; // Select LoRa support 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 } 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) { - Lora.enableInterrupt = false; // disable the interrupt service routine while processing the data - Lora.receivedFlag = false; // reset flag - - int packet_size = 0; - int state = LoRaRadio.readData((uint8_t*)data, LORA_MAX_PACKET_LENGTH -1); + Lora.receivedFlag = false; // Reset flag + int packet_size = LoRaRadio.getPacketLength(); + int state = LoRaRadio.readData((uint8_t*)data, TAS_LORA_MAX_PACKET_LENGTH -1); + // LoRaWan downlink frames are sent without CRC, which will raise error on SX126x. We can ignore that error + 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 (!Lora.sendFlag) { - packet_size = LoRaRadio.getPacketLength(); -#ifdef USE_LORA_DEBUG - AddLog(LOG_LEVEL_DEBUG, PSTR("LOR: Packet %d, Rcvd %32_H"), packet_size, data); -#endif - } + Lora.rssi = LoRaRadio.getRSSI(); + Lora.snr = LoRaRadio.getSNR(); + } else { + packet_size = 0; // Some other error occurred + 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; } -bool LoraSendSx126x(uint8_t* data, uint32_t len) { - Lora.sendFlag = true; -#ifdef USE_LORA_DEBUG - AddLog(LOG_LEVEL_DEBUG, PSTR("LOR: Len %d, Send %*_H"), len, len + 2, data); -#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; - } +bool LoraSendSx126x(uint8_t* data, uint32_t len, bool invert) { + Lora.sendFlag = true; // Use this flag as LoRaRadio.transmit enable send interrupt + if (invert) { + LoRaRadio.invertIQ(true); } - return send_success; + int state = LoRaRadio.transmit(data, len); + if (invert) { + LoRaRadio.invertIQ(false); + } + return (RADIOLIB_ERR_NONE == state); } bool LoraConfigSx126x(void) { - LoRaRadio.setCodingRate(Lora.coding_rate); - LoRaRadio.setSyncWord(Lora.sync_word); - LoRaRadio.setPreambleLength(Lora.preamble_length); - LoRaRadio.setCurrentLimit(Lora.current_limit); - LoRaRadio.setCRC(Lora.crc_bytes); - LoRaRadio.setSpreadingFactor(Lora.spreading_factor); - LoRaRadio.setBandwidth(Lora.bandwidth); - LoRaRadio.setFrequency(Lora.frequency); - LoRaRadio.setOutputPower(Lora.output_power); - if (Lora.implicit_header) { - LoRaRadio.implicitHeader(Lora.implicit_header); + LoRaRadio.setCodingRate(LoraSettings.coding_rate); + LoRaRadio.setSyncWord(LoraSettings.sync_word); + LoRaRadio.setPreambleLength(LoraSettings.preamble_length); + LoRaRadio.setCurrentLimit(LoraSettings.current_limit); + LoRaRadio.setCRC(LoraSettings.crc_bytes); + LoRaRadio.setSpreadingFactor(LoraSettings.spreading_factor); + LoRaRadio.setBandwidth(LoraSettings.bandwidth); + LoRaRadio.setFrequency(LoraSettings.frequency); + LoRaRadio.setOutputPower(LoraSettings.output_power); + if (LoraSettings.implicit_header) { + LoRaRadio.implicitHeader(LoraSettings.implicit_header); } else { LoRaRadio.explicitHeader(); } + LoRaRadio.invertIQ(false); return true; } bool LoraInitSx126x(void) { 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(); LoRaRadio.setDio1Action(LoraOnReceiveSx126x); if (RADIOLIB_ERR_NONE == LoRaRadio.startReceive()) { diff --git a/tasmota/tasmota_xdrv_driver/xdrv_73_3_lora_sx127x.ino b/tasmota/tasmota_xdrv_driver/xdrv_73_3_lora_sx127x.ino index b220dcb9b..ce86c7e5f 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_73_3_lora_sx127x.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_73_3_lora_sx127x.ino @@ -27,30 +27,17 @@ * - LoRa_DIO0 \*********************************************************************************************/ -//#define USE_LORA_SX127X_CAD - #include // 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) { + // This function is called when a complete packet is received by the module #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 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 } @@ -59,73 +46,64 @@ bool LoraAvailableSx127x(void) { } int LoraReceiveSx127x(char* data) { - Lora.enableInterrupt = false; // disable the interrupt service routine while processing the data - int packet_size = 0; while (LoRa.available()) { // read packet up to LORA_MAX_PACKET_LENGTH 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; } } -// 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.snr = LoRa.packetSnr(); -#ifdef USE_LORA_SX127X_CAD - LoRa.channelActivityDetection(); // put the radio into CAD mode -#endif // USE_LORA_SX127X_CAD + Lora.packet_size = 0; // reset flag return packet_size; } -bool LoraSendSx127x(uint8_t* data, uint32_t len) { -// Lora.sendFlag = true; - LoRa.beginPacket(Lora.implicit_header); // start packet - LoRa.write(data, len); // send message +bool LoraSendSx127x(uint8_t* data, uint32_t len, bool invert) { + if (invert) { + LoRa.enableInvertIQ(); // active invert I and Q signals + } + LoRa.beginPacket(LoraSettings.implicit_header); // start packet + LoRa.write(data, len); // send message LoRa.endPacket(); // finish packet and send it + if (invert) { + LoRa.disableInvertIQ(); // normal mode + } LoRa.receive(); // go back into receive mode return true; } bool LoraConfigSx127x(void) { - LoRa.setFrequency(Lora.frequency * 1000 * 1000); - LoRa.setSignalBandwidth(Lora.bandwidth * 1000); - LoRa.setSpreadingFactor(Lora.spreading_factor); - LoRa.setCodingRate4(Lora.coding_rate); - LoRa.setSyncWord(Lora.sync_word); - LoRa.setTxPower(Lora.output_power); - LoRa.setPreambleLength(Lora.preamble_length); - LoRa.setOCP(Lora.current_limit); - if (Lora.crc_bytes) { + LoRa.setFrequency(LoraSettings.frequency * 1000 * 1000); + LoRa.setSignalBandwidth(LoraSettings.bandwidth * 1000); + LoRa.setSpreadingFactor(LoraSettings.spreading_factor); + LoRa.setCodingRate4(LoraSettings.coding_rate); + LoRa.setSyncWord(LoraSettings.sync_word); + LoRa.setTxPower(LoraSettings.output_power); + LoRa.setPreambleLength(LoraSettings.preamble_length); + LoRa.setOCP(LoraSettings.current_limit); + if (LoraSettings.crc_bytes) { LoRa.enableCrc(); } else { LoRa.disableCrc(); } /* - if (Lora.implicit_header) { + if (LoraSettings.implicit_header) { LoRa.implicitHeaderMode(); } else { LoRa.explicitHeaderMode(); } */ + LoRa.disableInvertIQ(); // normal mode return true; } bool LoraInitSx127x(void) { 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(); LoRa.onReceive(LoraOnReceiveSx127x); -#ifdef USE_LORA_SX127X_CAD - LoRa.onCadDone(LoraOnCadDoneSx127x); // register the channel activity dectection callback - LoRa.channelActivityDetection(); -#else LoRa.receive(); -#endif // USE_LORA_SX127X_CAD return true; } return false; diff --git a/tasmota/tasmota_xdrv_driver/xdrv_73_4_lorawan_cryptography.ino b/tasmota/tasmota_xdrv_driver/xdrv_73_4_lorawan_cryptography.ino new file mode 100644 index 000000000..94e6acde9 --- /dev/null +++ b/tasmota/tasmota_xdrv_driver/xdrv_73_4_lorawan_cryptography.ino @@ -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 + +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 diff --git a/tasmota/tasmota_xdrv_driver/xdrv_73_8_lorawan_bridge.ino b/tasmota/tasmota_xdrv_driver/xdrv_73_8_lorawan_bridge.ino new file mode 100644 index 000000000..e00fac085 --- /dev/null +++ b/tasmota/tasmota_xdrv_driver/xdrv_73_8_lorawan_bridge.ino @@ -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 + * - 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 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 diff --git a/tasmota/tasmota_xdrv_driver/xdrv_73_9_lora.ino b/tasmota/tasmota_xdrv_driver/xdrv_73_9_lora.ino index 25fb1cc49..d0b483b70 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_73_9_lora.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_73_9_lora.ino @@ -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) { 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); 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]) { - char *payload = data +1; // Skip TAS_LORA_REMOTE_COMMAND + char *payload = data +1; // Skip TAS_LORA_REMOTE_COMMAND char *command_part; char *topic_part = strtok_r(payload, " ", &command_part); if (topic_part && command_part) { @@ -32,7 +213,7 @@ void LoraInput(void) { ExecuteCommand(command_part, SRC_REMOTE); return; } 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")); } -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) { if ((SPI_MOSI_MISO == TasmotaGlobal.spi_enabled) && (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); #endif // ESP32 - Lora.enableInterrupt = true; - LoraDefaults(); +#ifdef USE_LORAWAN_BRIDGE + LoraWanInit(); +#endif // USE_LORAWAN_BRIDGE + LoraSettingsLoad(0); char hardware[20]; strcpy_P(hardware, PSTR("Not")); @@ -127,27 +297,42 @@ void LoraInit(void) { * Commands \*********************************************************************************************/ -#define D_CMND_LORASEND "Send" -#define D_CMND_LORACONFIG "Config" -#define D_CMND_LORACOMMAND "Command" +#define D_CMND_LORASEND "Send" +#define D_CMND_LORACONFIG "Config" +#define D_CMND_LORACOMMAND "Command" +#define D_CMND_LORAOPTION "Option" 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 = { - &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) { // LoRaCommand // LoRaCommand lorareceiver power 2 // LoRaCommand lorareceiver publish cmnd/anytopic/power 2 + // LoRaCommand lorareceiver LoRaCommand thisreceiver status 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 - 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; strlcpy(data +1, XdrvMailbox.data, len); - Lora.Send((uint8_t*)data, len); + LoraSend((uint8_t*)data, len, false); ResponseCmndDone(); } } @@ -162,14 +347,19 @@ void CmndLoraSend(void) { // LoRaSend4 "Hello Tiger" - Send "Hello Tiger" and set to binary decoding // LoRaSend5 "AA004566" - Send "AA004566" 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)) { Lora.raw = (XdrvMailbox.index > 3); // Global flag set even without data if (XdrvMailbox.data_len > 0) { - char data[LORA_MAX_PACKET_LENGTH] = { 0 }; - uint32_t len = (XdrvMailbox.data_len < LORA_MAX_PACKET_LENGTH -1) ? XdrvMailbox.data_len : LORA_MAX_PACKET_LENGTH -2; + char data[TAS_LORA_MAX_PACKET_LENGTH] = { 0 }; + 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 -// 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 if (1 == XdrvMailbox.index) { // "Hello Tiger\n" memcpy(data, XdrvMailbox.data, len); @@ -193,7 +383,7 @@ void CmndLoraSend(void) { while (size > 1) { strlcpy(stemp, codes, sizeof(stemp)); 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; codes += 2; } @@ -205,14 +395,14 @@ void CmndLoraSend(void) { len = 0; for (char* str = strtok_r(values, ",", &p); str; str = strtok_r(nullptr, ",", &p)) { data[len++] = (uint8_t)atoi(str); - if (len > LORA_MAX_PACKET_LENGTH -2) { break; } + if (len > TAS_LORA_MAX_PACKET_LENGTH -2) { break; } } } else { len = 0; } if (len) { - Lora.Send((uint8_t*)data, len); + LoraSend((uint8_t*)data, len, invert); } ResponseCmndDone(); } @@ -222,41 +412,31 @@ void CmndLoraSend(void) { void CmndLoraConfig(void) { // LoRaConfig - Show all parameters // LoRaConfig 1 - Set default parameters + // LoRaConfig 2 - Set default LoRaWan bridge parameters // LoRaConfig {"Frequency":868.0,"Bandwidth":125.0} - Enter float parameters // LoRaConfig {"SyncWord":18} - Enter decimal parameter (=0x12) if (XdrvMailbox.data_len > 0) { if (XdrvMailbox.payload == 1) { LoraDefaults(); Lora.Config(); - } else { + } + else if (XdrvMailbox.payload == 2) { + LoraWanDefaults(); + Lora.Config(); + } + else { JsonParser parser(XdrvMailbox.data); JsonParserObject root = parser.getRootObject(); if (root) { - Lora.frequency = root.getFloat(PSTR(D_JSON_FREQUENCY), Lora.frequency); - 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); + LoraJson2Settings(root); Lora.Config(); } } } ResponseCmnd(); // {"LoRaConfig": - ResponseAppend_P(PSTR("{\"" D_JSON_FREQUENCY "\":%1_f"), &Lora.frequency); // xxx.x MHz - ResponseAppend_P(PSTR(",\"" D_JSON_BANDWIDTH "\":%1_f"), &Lora.bandwidth); // xxx.x kHz - ResponseAppend_P(PSTR(",\"" D_JSON_SPREADING_FACTOR "\":%d"), Lora.spreading_factor); - 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 + ResponseAppend_P(PSTR("{")); + LoraSettings2Json(); + ResponseAppend_P(PSTR("}}")); } /*********************************************************************************************\ @@ -275,8 +455,19 @@ bool Xdrv73(uint32_t function) { case FUNC_SLEEP_LOOP: LoraInput(); break; + case FUNC_RESET_SETTINGS: + LoraSettingsLoad(1); + break; + case FUNC_SAVE_SETTINGS: + LoraSettingsSave(); + break; case FUNC_COMMAND: result = DecodeCommand(kLoraCommands, LoraCommand); +#ifdef USE_LORAWAN_BRIDGE + if (!result) { + result = DecodeCommand(kLoraWanCommands, LoraWanCommand); + } +#endif // USE_LORAWAN_BRIDGE break; case FUNC_ACTIVE: result = true;