From 263d84315f3574adb8aaf21fb48cedb2f8a6af51 Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Tue, 3 Jun 2025 17:58:35 +0200 Subject: [PATCH] Add support for LoraWanSend and LoraWanNode --- .../xdrv_73_0_lora_struct.ino | 12 +- .../xdrv_73_8_lorawan_bridge.ino | 150 +++++++++++++++--- .../tasmota_xdrv_driver/xdrv_73_9_lora.ino | 4 + 3 files changed, 144 insertions(+), 22 deletions(-) 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 b2d6edae4..aa816f585 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_73_0_lora_struct.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_73_0_lora_struct.ino @@ -179,7 +179,8 @@ enum TasLoraFlags { }; enum TasLoraWanFlags { - TAS_LORAWAN_FLAG_LINK_ADR_REQ + TAS_LORAWAN_FLAG_LINK_ADR_REQ, + TAS_LORAWAN_FLAG_DISABLED }; enum TasLoraWanMTypes { @@ -220,7 +221,13 @@ enum TasLoraWanCIDNode { TAS_LORAWAN_CID_DL_CHANNEL_ANS, TAS_LORAWAN_CID_RFU1_ANS, TAS_LORAWAN_CID_RFU2_ANS, - TAS_LORAWAN_CID_DEVICE_TIME_REQ + TAS_LORAWAN_CID_DEVICE_TIME_REQ, + TAS_LORAWAN_CID_RFU1, + TAS_LORAWAN_CID_RFU2, + TAS_LORAWAN_CID_PING_SLOT_INFO_REQ, // Class B + TAS_LORAWAN_CID_PING_SLOT_CHANNEL_ANS, // Class B + TAS_LORAWAN_CID_BEACON_TIMING_REQ, // Class B - Deprecated + TAS_LORAWAN_CID_BEACON_FREQ_ANS // Class B }; enum LoRaWanRadioMode_t { @@ -315,6 +322,7 @@ typedef struct Lora_t { uint8_t send_buffer_step; uint8_t send_buffer_len; uint8_t nodes; + uint8_t delay_settings_save; bool rx; bool send_request; bool profile_changed; diff --git a/tasmota/tasmota_xdrv_driver/xdrv_73_8_lorawan_bridge.ino b/tasmota/tasmota_xdrv_driver/xdrv_73_8_lorawan_bridge.ino index 8d7080a29..72f1a54b6 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_73_8_lorawan_bridge.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_73_8_lorawan_bridge.ino @@ -446,6 +446,7 @@ void LoraWanSendResponse(uint8_t* buffer, size_t len, uint32_t lorawan_delay) { Lora->send_request = false; Lora->backup_settings = Lora->settings; // Make a copy; Lora->send_buffer_step = 2; // Send at RX1 and RX2 + Lora->delay_settings_save = ((lorawan_delay + TAS_LORAWAN_RECEIVE_DELAY2) / 1000) +2; // Delay settings save uint32_t delay_rx1 = lorawan_delay - TimePassedSince(Lora->receive_time); LoraWan_Send_RX1.once_ms(delay_rx1, LoraWanTickerSend); @@ -535,6 +536,43 @@ uint32_t LoraWanSpreadingFactorToDataRate(bool downlink) { /*********************************************************************************************/ +void LoraWanSendAppData(uint32_t node, uint8_t* FRMPayload, uint32_t len) { + if (len > 15) { return; } // M is region specific and depends on DataRate + + uint32_t DevAddr = Lora->device_address +node; + uint16_t FCnt = Lora->settings.end_node[node]->FCntDown++; + + uint8_t Key[TAS_LORAWAN_AES128_KEY_SIZE]; + uint8_t FRMPayload_encrypted[TAS_LORAWAN_AES128_KEY_SIZE +9 +len]; + LoraWanDeriveLegacyAppSKey(node, Key); + LoraWanEncryptDownlink(Key, DevAddr, FCnt, FRMPayload, len, 0, FRMPayload_encrypted); + + uint32_t data_size = 13 + len; + uint8_t data[data_size]; + data[0] = TAS_LORAWAN_MTYPE_UNCONFIRMED_DATA_DOWNLINK << 5; + data[1] = DevAddr; + data[2] = DevAddr >> 8; + data[3] = DevAddr >> 16; + data[4] = DevAddr >> 24; + data[5] = 0x00; // FCtrl - No FOpts + data[6] = FCnt; + data[7] = FCnt >> 8; + data[8] = 0x01; // FPort no MAC (as used by Dragino) + for (uint32_t i = 0; i < len; i++) { + data[9 +i] = FRMPayload_encrypted[i]; + } + LoraWanDeriveLegacyNwkSKey(node, Key); + uint32_t MIC = LoraWanComputeLegacyDownlinkMIC(Key, DevAddr, FCnt, data, data_size -4); + data[data_size -4] = MIC; + data[data_size -3] = MIC >> 8; + data[data_size -2] = MIC >> 16; + data[data_size -1] = MIC >> 24; + + LoraWanSendResponse(data, data_size, 10); +} + +/*-------------------------------------------------------------------------------------------*/ + void LoraWanSendLinkADRReq(uint32_t node) { uint32_t DevAddr = Lora->device_address +node; uint16_t FCnt = Lora->settings.end_node[node]->FCntDown++; @@ -623,6 +661,8 @@ void LoraWanSendMacResponse(uint32_t node, uint8_t* FOpts, uint32_t FCtrl) { LoraWanSendResponse(data, data_size, 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) @@ -644,6 +684,8 @@ bool LoraWanInput(uint8_t* data, uint32_t packet_size) { ((uint32_t)data[21] << 16) | ((uint32_t)data[22] << 24); for (uint32_t node = 0; node < Lora->nodes; node++) { + if (bitRead(Lora->settings.end_node[node]->flags, TAS_LORAWAN_FLAG_DISABLED)) { continue; } // Skip + uint32_t CalcMIC = LoraWanGenerateMIC(data, 19, Lora->settings.end_node[node]->AppKey); if (MIC == CalcMIC) { // Valid MIC based on LoraWanAppKey @@ -733,12 +775,13 @@ bool LoraWanInput(uint8_t* data, uint32_t packet_size) { // 40 422E0100 A2 1800 0307 78 29FBF8FD9227729984 8C71E95B - FCtrl ADR support, ADRACKReq=0, FOptsLen = 2 -> FOpts = MAC, response after LinkADRReq // 40 F4F51700 A2 0200 0307 CC 6517D4AB06D32C9A9F 14CBA305 - FCtrl ADR support, ADRACKReq=0, FOptsLen = 2 -> FOpts = MAC, response after LinkADRReq - bool bResponseSent = false; // Make sure do not send multiple responses + bool bResponseSent = false; // Make sure do not send multiple responses 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 < Lora->nodes; node++) { - if (0 == Lora->settings.end_node[node]->DevEUIh) { continue; } // No DevEUI so never joined + if (0 == Lora->settings.end_node[node]->DevEUIh) { continue; } // No DevEUI so never joined if ((Lora->device_address +node) != DevAddr) { continue; } // Not my device + if (bitRead(Lora->settings.end_node[node]->flags, TAS_LORAWAN_FLAG_DISABLED)) { continue; } // Skip uint32_t FCtrl = data[5]; uint32_t FOptsLen = FCtrl & 0x0F; @@ -752,16 +795,16 @@ bool LoraWanInput(uint8_t* data, uint32_t packet_size) { 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 + if (MIC != CalcMIC) { continue; } // Same device address but never joined bool FCtrl_ADR = bitRead(FCtrl, 7); - bool FCtrl_ADRACKReq = bitRead(FCtrl, 6); //Device is requesting a response, so that it knows comms is still up. + bool FCtrl_ADRACKReq = bitRead(FCtrl, 6); // Device is requesting a response, so that it knows comms is still up. // else device will eventually enter backoff mode and we loose comms // ref: https://lora-alliance.org/wp-content/uploads/2021/11/LoRaWAN-Link-Layer-Specification-v1.0.4.pdf // page 19 // In testing with a Dragino LHT52 device, FCtrl_ADRACKReq was set after 64 (0x40) uplinks (= 21.3 hrs) bool FCtrl_ACK = bitRead(FCtrl, 5); - bool Fctrl_ClassB = bitRead(FCtrl, 4); + bool Fctrl_ClassB = bitRead(FCtrl, 4); // Ready to receive scheduled downlink pings (Class B only) /* if ((0 == FOptsLen) && (0 == FOpts[0])) { // MAC response @@ -794,7 +837,7 @@ bool LoraWanInput(uint8_t* data, uint32_t packet_size) { #endif // USE_LORA_DEBUG if (Lora->settings.end_node[node]->FCntUp <= FCnt) { // Skip re-transmissions - Lora->rx = false; // Skip RX2 as this is a response from RX1 + Lora->rx = false; // Skip RX2 as this is a response from RX1 Lora->settings.end_node[node]->FCntUp++; if (Lora->settings.end_node[node]->FCntUp < FCnt) { // Report missed frames uint32_t FCnt_missed = FCnt - Lora->settings.end_node[node]->FCntUp; @@ -862,6 +905,15 @@ bool LoraWanInput(uint8_t* data, uint32_t packet_size) { mac_data[mac_data_idx++] = 0x00; // Fractional-second 1/256 sec } } + else if (TAS_LORAWAN_CID_PING_SLOT_INFO_REQ == FOpts[i]) { + i++; // PingSlotParam + } + else if (TAS_LORAWAN_CID_PING_SLOT_CHANNEL_ANS == FOpts[i]) { + i++; // Status + } + else if (TAS_LORAWAN_CID_BEACON_FREQ_ANS == FOpts[i]) { + i++; // Status + } else { // RFU } @@ -936,19 +988,25 @@ bool LoraWanInput(uint8_t* data, uint32_t packet_size) { \*********************************************************************************************/ #define D_CMND_LORAWANBRIDGE "Bridge" +#define D_CMND_LORAWANNODE "Node" #define D_CMND_LORAWANAPPKEY "AppKey" #define D_CMND_LORAWANNAME "Name" #define D_CMND_LORAWANDECODER "Decoder" +#define D_CMND_LORAWANSEND "Send" -const char kLoraWanCommands[] PROGMEM = "LoRaWan|" // Prefix - D_CMND_LORAWANBRIDGE "|" D_CMND_LORAWANAPPKEY "|" D_CMND_LORAWANNAME "|" D_CMND_LORAWANDECODER; +const char kLoraWanCommands[] PROGMEM = "LoRaWan" // Prefix + "|" D_CMND_LORAWANBRIDGE "|" D_CMND_LORAWANNODE "|" D_CMND_LORAWANAPPKEY + "|" D_CMND_LORAWANNAME "|" D_CMND_LORAWANDECODER "|" D_CMND_LORAWANSEND; void (* const LoraWanCommand[])(void) PROGMEM = { - &CmndLoraWanBridge, &CmndLoraWanAppKey, &CmndLoraWanName, &CmndLoraWanDecoder }; + &CmndLoraWanBridge, &CmndLoraWanNode, &CmndLoraWanAppKey, + &CmndLoraWanName, &CmndLoraWanDecoder, &CmndLoraWanSend }; void CmndLoraWanBridge(void) { - // LoraWanBridge - Show LoraOption1 - // LoraWanBridge 1 - Set LoraOption1 1 = Enable LoraWanBridge + // Enable LoraWan bridge + // LoraWanBridge <0|1> + // LoraWanBridge - Show LoraOption1 + // LoraWanBridge 1 - Set LoraOption1 1 = Enable LoraWanBridge uint32_t pindex = 0; if (XdrvMailbox.payload >= 0) { bitWrite(Lora->settings.flags, pindex, XdrvMailbox.payload); @@ -961,13 +1019,33 @@ void CmndLoraWanBridge(void) { ResponseCmndChar(GetStateText(bitRead(Lora->settings.flags, pindex))); } +void CmndLoraWanNode(void) { + // Disable joined node (for testing purposes on other gateway) + // LoraWanNode<1..16> <0|1> + // LoraWanNode - Show current state + // LoraWanNode2 1 - Enable node 2 + if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= Lora->nodes)) { + uint32_t node = XdrvMailbox.index -1; + if (XdrvMailbox.data_len) { + if (1 == XdrvMailbox.payload) { + bitClear(Lora->settings.end_node[node]->flags, TAS_LORAWAN_FLAG_DISABLED); // Enable (clears bit) + } else { + bitSet(Lora->settings.end_node[node]->flags, TAS_LORAWAN_FLAG_DISABLED); // Disable (sets bit) + } + } + ResponseCmndIdxChar(bitRead(Lora->settings.end_node[node]->flags, TAS_LORAWAN_FLAG_DISABLED)?"Disabled":"Enabled"); + } +} + void CmndLoraWanAppKey(void) { - // LoraWanAppKey - // LoraWanAppKey2 0123456789abcdef0123456789abcdef + // Configure node 32 digit AppKey + // LoraWanAppKey<1..16> + // LoraWanAppKey - Show current appkey + // LoraWanAppKey2 0123456789abcdef0123456789abcdef if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= Lora->nodes +1)) { if (Lora->nodes < XdrvMailbox.index) { if (!LoraWanAddNode()) { - return; // Memory allocation failed or TAS_LORAWAN_ENDNODES reached + return; // Memory allocation failed or TAS_LORAWAN_ENDNODES reached } } uint32_t node = XdrvMailbox.index -1; @@ -987,9 +1065,11 @@ void CmndLoraWanAppKey(void) { } void CmndLoraWanName(void) { - // LoraWanName - // LoraWanName 1 - Set to short DevEUI (or 0x0000 if not yet joined) - // LoraWanName2 LDS02a + // Configure node name + // LoraName<1..16> <1|name> + // LoraWanName - Show current name + // LoraWanName 1 - Set to short DevEUI (or 0x0000 if not yet joined) + // LoraWanName2 LDS02a if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= Lora->nodes)) { uint32_t node = XdrvMailbox.index -1; if (XdrvMailbox.data_len) { @@ -1006,9 +1086,10 @@ void CmndLoraWanName(void) { } void CmndLoraWanDecoder(void) { - // LoraWanDecoder - // LoraWanDecoder DraginoLDS02 - Set Dragino LDS02 message decoder for node 1 - // LoraWanDecoder2 MerryIoTDW10 - Set MerryIoT DW10 message decoder for node 2 + // Configure node berry decoder name + // LoraWanDecoder<1..16> + // LoraWanDecoder LDS02 - Set Dragino LDS02 message decoder for node 1 + // LoraWanDecoder2 DW10 - Set MerryIoT DW10 message decoder for node 2 if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= Lora->nodes)) { uint32_t node = XdrvMailbox.index -1; if (XdrvMailbox.data_len) { @@ -1018,6 +1099,35 @@ void CmndLoraWanDecoder(void) { } } +void CmndLoraWanSend(void) { + // Send downlink command to node. Works in Class C (which uses more power) + // LoraWanSend<1..16> + // LoraWanSend3 260014 - Set dragino RJTDC time interval to 20 minutes + if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= Lora->nodes)) { + uint32_t node = XdrvMailbox.index -1; + if (XdrvMailbox.data_len) { + char *codes = RemoveSpace(XdrvMailbox.data); + int size = strlen(XdrvMailbox.data); + if (size > 30) { return; } // M is region specific and depends on DataRate + char *p; + char stemp[3]; + uint8_t code; + uint8_t data[16]; + uint32_t data_idx = 0; + while (size > 1) { + strlcpy(stemp, codes, sizeof(stemp)); + code = strtol(stemp, &p, 16); + data[data_idx++] = code; // "260014" as hex values + size -= 2; + codes += 2; + } + Lora->receive_time = millis(); + LoraWanSendAppData(node, data, data_idx); + ResponseCmndDone(); + } + } +} + /*********************************************************************************************\ * Interface \*********************************************************************************************/ diff --git a/tasmota/tasmota_xdrv_driver/xdrv_73_9_lora.ino b/tasmota/tasmota_xdrv_driver/xdrv_73_9_lora.ino index 559c6cff6..eff2748fb 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_73_9_lora.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_73_9_lora.ino @@ -175,6 +175,10 @@ void LoraSettingsLoad(bool erase) { void LoraSettingsSave(void) { // Called from FUNC_SAVE_SETTINGS every SaveData second and at restart #ifdef USE_UFILESYS + if (Lora->delay_settings_save) { // Delay settings update when expecting cascading changes + Lora->delay_settings_save--; + return; + } uint32_t crc32 = GetCfgCrc32((uint8_t*)&Lora->settings +4, sizeof(LoraSettings_t) -4); // Skip crc32 #ifdef USE_LORAWAN_BRIDGE crc32 += LoraWanGetCfgCrc();