From f2fbd73044ac93f9a9ba8284e6954b59960d0d79 Mon Sep 17 00:00:00 2001 From: StevenCellist <47155822+StevenCellist@users.noreply.github.com> Date: Sat, 21 Jun 2025 15:48:30 +0200 Subject: [PATCH] [LoRaWAN] Provide support for TSxxx packages (including TS009 reference implementation) (#1528) * [LoRaWAN] Fix for incorrect use of dutycycle function * [LoRaWAN] Implement TSxxx package support And fix an MIC problem for >16-bit downlink FCnt * Revert unused error code * [LoRaWAN] Add TS009 example * [LoRaWAN] Comment a platform dependency in TS009 header * [LoRaWAN] Make TSxxx example platform independent * [LoRaWAN] Remove unused variable * [LoRaWAN] Remove printf from example * [LoRaWAN] Fix scope of variables * [LoRaWAN] Remove printf from example * [LoRaWAN] Fix cppcheck issue * [LoRaWAN] Feedback improvements --- .../LoRaWAN_TS_Packages/LoRaWAN_TS009.h | 239 +++++++ .../LoRaWAN_TS_Packages.ino | 171 +++++ examples/LoRaWAN/LoRaWAN_TS_Packages/config.h | 115 ++++ .../LoRaWAN/LoRaWAN_TS_Packages/lorawan.h | 76 +++ src/TypeDef.h | 9 +- src/protocols/LoRaWAN/LoRaWAN.cpp | 593 +++++++++--------- src/protocols/LoRaWAN/LoRaWAN.h | 110 +++- 7 files changed, 1011 insertions(+), 302 deletions(-) create mode 100644 examples/LoRaWAN/LoRaWAN_TS_Packages/LoRaWAN_TS009.h create mode 100644 examples/LoRaWAN/LoRaWAN_TS_Packages/LoRaWAN_TS_Packages.ino create mode 100644 examples/LoRaWAN/LoRaWAN_TS_Packages/config.h create mode 100644 examples/LoRaWAN/LoRaWAN_TS_Packages/lorawan.h diff --git a/examples/LoRaWAN/LoRaWAN_TS_Packages/LoRaWAN_TS009.h b/examples/LoRaWAN/LoRaWAN_TS_Packages/LoRaWAN_TS009.h new file mode 100644 index 00000000..6e360c5d --- /dev/null +++ b/examples/LoRaWAN/LoRaWAN_TS_Packages/LoRaWAN_TS009.h @@ -0,0 +1,239 @@ +#include +#include +// #include + +#warning "The variables below must match your main code. Please check your radio type!" +extern SX1278 radio; // this can be any LoRaWAN-compatible type (e.g. SX1262) +extern LoRaWANNode node; +extern uint32_t periodicity; +extern bool isConfirmed; +extern bool reply; +extern uint8_t dataUp[255]; +extern size_t lenUp; +extern uint8_t fPort; + +#define RADIOLIB_LORAWAN_TS009_PACKAGE_VERSION (0x00) +#define RADIOLIB_LORAWAN_TS009_DUT_RESET (0x01) +#define RADIOLIB_LORAWAN_TS009_DUT_JOIN (0x02) +#define RADIOLIB_LORAWAN_TS009_SWITCH_CLASS (0x03) +#define RADIOLIB_LORAWAN_TS009_ADR_BIT_CHANGE (0x04) +#define RADIOLIB_LORAWAN_TS009_REGIONAL_DUTY_CYCLE (0x05) +#define RADIOLIB_LORAWAN_TS009_TX_PERIODICITY_CHANGE (0x06) +#define RADIOLIB_LORAWAN_TS009_TX_FRAMES_CTRL (0x07) +#define RADIOLIB_LORAWAN_TS009_ECHO_PAYLOAD (0x08) +#define RADIOLIB_LORAWAN_TS009_RX_APP_CNT (0x09) +#define RADIOLIB_LORAWAN_TS009_RX_APP_CNT_RESET (0x0A) +#define RADIOLIB_LORAWAN_TS009_LINK_CHECK (0x20) +#define RADIOLIB_LORAWAN_TS009_DEVICE_TIME (0x21) +#define RADIOLIB_LORAWAN_TS009_PING_SLOT_INFO (0x22) +#define RADIOLIB_LORAWAN_TS009_TX_CW (0x7D) +#define RADIOLIB_LORAWAN_TS009_DUT_FPORT224_DISABLE (0x7E) +#define RADIOLIB_LORAWAN_TS009_DUT_VERSIONS (0x7F) + +/*! + \brief This function implements the TS009 specification. + To enable this package, add this to your setup: + `node.addAppPackage(RADIOLIB_LORAWAN_PACKAGE_TS009, handleTS009)` + Make sure that all `extern` variables are handled in your user code! +*/ +void handleTS009(uint8_t* dataDown, size_t lenDown) { + if(lenDown == 0 || dataDown == NULL) { + return; + } + RADIOLIB_DEBUG_PRINTLN("CID = %02x, len = %d", dataDown[0], lenDown - 1); + + switch(dataDown[0]) { + case(RADIOLIB_LORAWAN_TS009_PACKAGE_VERSION): { + lenUp = 3; + dataUp[1] = 5; // PackageIdentifier + dataUp[2] = 1; // PackageVersion + fPort = RADIOLIB_LORAWAN_FPORT_TS009; + RADIOLIB_DEBUG_PRINTLN("PackageIdentifier: %d, PackageVersion: %d", dataUp[1], dataUp[2]); + + reply = true; + } break; + + case(RADIOLIB_LORAWAN_TS009_DUT_RESET): { + RADIOLIB_DEBUG_PRINTLN("Restarting..."); + + #warning "Please implement this reset function yourself!" + + // the function to reset the MCU is platform-dependent + // for ESP32 for example, this would be: + // ESP.restart(); + + reply = false; + } break; + + case(RADIOLIB_LORAWAN_TS009_DUT_JOIN): { + RADIOLIB_DEBUG_PRINTLN("Reverting to Join state"); + node.clearSession(); + + reply = false; + } break; + + case(RADIOLIB_LORAWAN_TS009_SWITCH_CLASS): { + uint8_t classType = dataDown[1]; + node.setClass(classType); + RADIOLIB_DEBUG_PRINTLN("Switching to class: %s", classType == 0 ? "A" : (classType == 1 ? "B" : "C")); + + reply = false; + } break; + + case(RADIOLIB_LORAWAN_TS009_ADR_BIT_CHANGE): { + bool adr = (bool)dataDown[1]; + node.setADR(adr); + RADIOLIB_DEBUG_PRINTLN("ADR: %d", adr); + + reply = false; + } break; + + case(RADIOLIB_LORAWAN_TS009_REGIONAL_DUTY_CYCLE): { + bool dutycycle = (bool)dataDown[1]; + node.setDutyCycle(dutycycle, 36000); + RADIOLIB_DEBUG_PRINTLN("Dutycycle: %d", dutycycle); + + reply = false; + } break; + + case(RADIOLIB_LORAWAN_TS009_TX_PERIODICITY_CHANGE): { + uint32_t defaultIntervalSecs = 30; + uint32_t intervals[11] = {defaultIntervalSecs, 5, 10, 20, 30, 40, 50, 60, 120, 240, 480}; + periodicity = intervals[dataDown[1]]; + + RADIOLIB_DEBUG_PRINTLN("Tx Periodicity: %d", periodicity); + + reply = false; + } break; + + case(RADIOLIB_LORAWAN_TS009_TX_FRAMES_CTRL): { + switch(dataDown[1]) { + case(0): + // no change + // isConfirmed = isConfirmed; + break; + case(1): + isConfirmed = false; + break; + case(2): + isConfirmed = true; + break; + } + RADIOLIB_DEBUG_PRINTLN("Confirmed: %d", isConfirmed); + + reply = false; + } break; + + case(RADIOLIB_LORAWAN_TS009_ECHO_PAYLOAD): { + lenUp = lenDown; + for (int i = 1; i < lenDown; i++) { + dataUp[i] = dataDown[i] + 1; + } + fPort = RADIOLIB_LORAWAN_FPORT_TS009; + RADIOLIB_DEBUG_PRINTLN("Echoing payload"); + + reply = true; + } break; + + case(RADIOLIB_LORAWAN_TS009_RX_APP_CNT): { + lenUp = 3; + uint16_t aFcntDown16 = (uint16_t)node.getAFCntDown(); + dataUp[1] = aFcntDown16 & 0xFF; + dataUp[2] = aFcntDown16 >> 8; + fPort = RADIOLIB_LORAWAN_FPORT_TS009; + RADIOLIB_DEBUG_PRINTLN("aFCntDown16: %d", aFcntDown16); + + reply = true; + } break; + + case(RADIOLIB_LORAWAN_TS009_RX_APP_CNT_RESET): { + RADIOLIB_DEBUG_PRINTLN("Resetting Application Frame count"); + node.resetFCntDown(); + + reply = false; + } break; + + case(RADIOLIB_LORAWAN_TS009_LINK_CHECK): { + lenUp = 0; + fPort = RADIOLIB_LORAWAN_FPORT_MAC_COMMAND; + node.sendMacCommandReq(RADIOLIB_LORAWAN_MAC_LINK_CHECK); + RADIOLIB_DEBUG_PRINTLN("Requesting LinkCheck"); + + reply = true; + } break; + + case(RADIOLIB_LORAWAN_TS009_DEVICE_TIME): { + lenUp = 0; + fPort = RADIOLIB_LORAWAN_FPORT_MAC_COMMAND; + node.sendMacCommandReq(RADIOLIB_LORAWAN_MAC_DEVICE_TIME); + RADIOLIB_DEBUG_PRINTLN("Requesting DeviceTime"); + + reply = true; + } break; + + case(RADIOLIB_LORAWAN_TS009_PING_SLOT_INFO): { + lenUp = 0; + RADIOLIB_DEBUG_PRINTLN("Requesting PingSlotInfo not implemented"); + // send PingSlotInfo MAC command which is not implemented + reply = false; + } break; + + case(RADIOLIB_LORAWAN_TS009_TX_CW): { + // not implemented + uint16_t timeout = (dataDown[2] << 8) | dataDown[1]; + uint32_t freqRaw = (dataDown[5] << 16) | (dataDown[4] << 8) | (dataDown[3]); + float freq = (float)freqRaw/10000.0; + uint8_t txPower = dataDown[6]; + RADIOLIB_DEBUG_PRINTLN("Continuous wave: %7.3f MHz, %d dBm, %d s", freq, txPower, timeout); + radio.setFrequency(freq); + radio.setOutputPower(txPower); + radio.transmitDirect(); + delay(timeout * 1000); + radio.standby(); + + reply = false; + } break; + + case(RADIOLIB_LORAWAN_TS009_DUT_FPORT224_DISABLE): { + RADIOLIB_DEBUG_PRINTLN("Disabling FPort 224"); + node.removePackage(RADIOLIB_LORAWAN_PACKAGE_TS009); + + reply = false; + } break; + + case(RADIOLIB_LORAWAN_TS009_DUT_VERSIONS): { + lenUp = 13; + // firmware version - this is RadioLib's version as an example + dataUp[1] = RADIOLIB_VERSION_MAJOR; + dataUp[2] = RADIOLIB_VERSION_MINOR; + dataUp[3] = RADIOLIB_VERSION_PATCH; + dataUp[4] = RADIOLIB_VERSION_EXTRA; + + // lorawan version + dataUp[5] = 1; +#if (LORAWAN_VERSION == 1) + dataUp[6] = 1; + dataUp[7] = 0; +#else + dataUp[6] = 0; + dataUp[7] = 4; +#endif + dataUp[8] = 0; + + // regional parameters version + dataUp[9] = 1; + dataUp[10] = 0; + dataUp[11] = 4; + dataUp[12] = 0; + fPort = RADIOLIB_LORAWAN_FPORT_TS009; + RADIOLIB_DEBUG_PRINTLN("Requested DUT versions"); + + reply = true; + } break; + } + + // if we must reply, copy the command ID into the uplink buffer + if(reply) { + dataUp[0] = dataDown[0]; + } +} \ No newline at end of file diff --git a/examples/LoRaWAN/LoRaWAN_TS_Packages/LoRaWAN_TS_Packages.ino b/examples/LoRaWAN/LoRaWAN_TS_Packages/LoRaWAN_TS_Packages.ino new file mode 100644 index 00000000..f9c1f437 --- /dev/null +++ b/examples/LoRaWAN/LoRaWAN_TS_Packages/LoRaWAN_TS_Packages.ino @@ -0,0 +1,171 @@ +/* + RadioLib LoRaWAN Packages Example + + This example shows how the TS009 package can be used + and is the sketch used for passing pre-certification testing. + This scheme can also be used for other future packages + such as TS003/004/005/006/007 which, combined, build FUOTA. + + PLEASE NOTE that this is a highly customized sketch with + settings that likely violate laws & regulations, and it is + intended to be used with RF blocking materials and attenuators. + + For default module settings, see the wiki page + https://github.com/jgromes/RadioLib/wiki/Default-configuration + + For full API reference, see the GitHub Pages + https://jgromes.github.io/RadioLib/ + + For LoRaWAN details, see the wiki page + https://github.com/jgromes/RadioLib/wiki/LoRaWAN + +*/ + +#include +#include + +#include "LoRaWAN_TS009.h" +#include "config.h" +#include "lorawan.h" + +uint32_t periodicity = uplinkIntervalSeconds; +bool isConfirmed = false; +bool reply = false; + +uint8_t fPort = 1; +uint8_t dataUp[255]; +uint8_t dataDown[255]; +size_t lenUp = 0; +size_t lenDown = 0; + +void setup() { + Serial.begin(115200); + delay(3000); + + Serial.print(F("Initialise the radio ... ")); + int state = radio.begin(); + if(state == RADIOLIB_ERR_NONE) { + Serial.println(F("success!")); + } else { + Serial.print(F("failed, code ")); + Serial.println(state); + while(true); + } + + // setup, restore and join the network + lwBegin(); + lwRestore(); + lwActivate(); + + // add TS009 package + node.addAppPackage(RADIOLIB_LORAWAN_PACKAGE_TS009, handleTS009); + + // LCTT (TS009 testing) has a huge timing problem on the JoinAccept Rx2 window... + node.scanGuard = 100; + + // these settings are totally not recommended + // but unfortunately they are the default settings for TS009 testing + node.setDutyCycle(false); + node.setADR(false); +} + +void loop() { + while(!node.isActivated()) { + lwActivate(); + // this 5s interval is way too short for normal use! + // but you'd be waiting around for long minutes during TS009 testing otherwise + if(!node.isActivated()) { + delay(5000); + } + node.setDutyCycle(false); + } + + int state = RADIOLIB_ERR_NONE; + LoRaWANEvent_t eventUp; + LoRaWANEvent_t eventDown; + + uint32_t start = millis(); + + Serial.println("--------------------"); + Serial.println("[LoRaWAN] Sending uplink packet ... "); + if (!reply) { + memset(dataUp, 0, 255); + lenUp = 4; + fPort = 1; + sprintf((char*)dataUp, "%04d", node.getFCntUp()); + state = node.sendReceive(dataUp, lenUp, fPort, dataDown, &lenDown, isConfirmed, &eventUp, &eventDown); + } else { + reply = false; + state = node.sendReceive(dataUp, lenUp, fPort, dataDown, &lenDown, isConfirmed, &eventUp, &eventDown); + } + + if(state >= RADIOLIB_ERR_NONE) { + Serial.println(F("[LoRaWAN] success!")); + } + + if(state > 0) { + // print data of the packet (if there are any) + Serial.print(F("[LoRaWAN] Data:\t\t")); + if(lenDown > 0) { + arrayDump(dataDown, lenDown); + } else { + Serial.println(F("")); + } + + Serial.print(F("[LoRaWAN] FPort:\t")); + Serial.print(eventDown.fPort); + + // print RSSI (Received Signal Strength Indicator) + Serial.print(F("[LoRaWAN] RSSI:\t\t")); + Serial.print(radio.getRSSI()); + Serial.println(F(" dBm")); + + // print SNR (Signal-to-Noise Ratio) + Serial.print(F("[LoRaWAN] SNR:\t\t")); + Serial.print(radio.getSNR()); + Serial.println(F(" dB")); + + uint8_t margin = 0; + uint8_t gwCnt = 0; + if(node.getMacLinkCheckAns(&margin, &gwCnt) == RADIOLIB_ERR_NONE) { + Serial.print(F("[LoRaWAN] LinkCheck margin:\t")); + Serial.println(margin); + Serial.print(F("[LoRaWAN] LinkCheck count:\t")); + Serial.println(gwCnt); + } + + uint32_t networkTime = 0; + uint8_t fracSecond = 0; + if(node.getMacDeviceTimeAns(&networkTime, &fracSecond, true) == RADIOLIB_ERR_NONE) { + Serial.print(F("[LoRaWAN] DeviceTime Unix:\t")); + Serial.println(networkTime); + Serial.print(F("[LoRaWAN] DeviceTime second:\t")); + Serial.print(fracSecond); + Serial.println(F("/256")); + } + + } else if(state == 0) { + Serial.println(F("No downlink!")); + + } else { + Serial.print(F("failed, code ")); + Serial.println(state); + } + + uint32_t end = millis(); + + uint32_t delayDc = node.timeUntilUplink(); + uint32_t delayMs = periodicity*1000; + if(delayMs > end - start) { + delayMs -= (end - start); + } else { + delayMs = 1; + } + delayMs += 50; + Serial.print(F("Delay: ")); + Serial.print(max(delayDc, delayMs)); + Serial.println(F(" ms")); + + // wait before sending another packet + delay(max(delayDc, delayMs)); +} \ No newline at end of file diff --git a/examples/LoRaWAN/LoRaWAN_TS_Packages/config.h b/examples/LoRaWAN/LoRaWAN_TS_Packages/config.h new file mode 100644 index 00000000..9c64a4da --- /dev/null +++ b/examples/LoRaWAN/LoRaWAN_TS_Packages/config.h @@ -0,0 +1,115 @@ +#ifndef _CONFIG_H +#define _CONFIG_H + +#include + +// first you have to set your radio model and pin configuration +// this is provided just as a default example +SX1278 radio = new Module(10, 2, 9, 3); + +// if you have RadioBoards (https://github.com/radiolib-org/RadioBoards) +// and are using one of the supported boards, you can do the following: +/* +#define RADIO_BOARD_AUTO +#include + +Radio radio = new RadioModule(); +*/ + +// how often to send an uplink - consider legal & FUP constraints +const uint32_t uplinkIntervalSeconds = 1UL * 60UL; // minutes x seconds + +#define LORAWAN_VERSION (0) // use version 1.LORAWAN_VERSION when joining +#define LORAWAN_OTAA (1) // use OTAA (1) or ABP (0) + +#if (LORAWAN_OTAA == 1) +// joinEUI - previous versions of LoRaWAN called this AppEUI +// for development purposes you can use all zeros - see wiki for details +#define RADIOLIB_LORAWAN_JOIN_EUI 0x0000000000000000 + +// the Device EUI & two keys can be generated on the TTN console +#ifndef RADIOLIB_LORAWAN_DEV_EUI // Replace with your Device EUI +#define RADIOLIB_LORAWAN_DEV_EUI 0x---------------- +#endif +#ifndef RADIOLIB_LORAWAN_APP_KEY // Replace with your App Key +#define RADIOLIB_LORAWAN_APP_KEY 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x-- +#endif + +// copy over the EUI's & keys in to the something that will not compile if incorrectly formatted +uint64_t joinEUI = RADIOLIB_LORAWAN_JOIN_EUI; +uint64_t devEUI = RADIOLIB_LORAWAN_DEV_EUI; +uint8_t appKey[] = { RADIOLIB_LORAWAN_APP_KEY }; + +#if (LORAWAN_VERSION == 1) +#ifndef RADIOLIB_LORAWAN_NWK_KEY // Put your Nwk Key here +#define RADIOLIB_LORAWAN_NWK_KEY 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x-- +#endif +uint8_t nwkKey[] = { RADIOLIB_LORAWAN_NWK_KEY }; // LW v1.1 only +#endif + +#else // ABP + +#ifndef RADIOLIB_LORAWAN_DEV_ADDR // Replace with your DevAddr +#define RADIOLIB_LORAWAN_DEV_ADDR 0x------ +#endif + +#ifndef RADIOLIB_LORAWAN_NWKSENC_KEY // Replace with your NwkSEnc Key +#define RADIOLIB_LORAWAN_NWKSENC_KEY 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x-- +#endif +#ifndef RADIOLIB_LORAWAN_APPS_KEY // Replace with your AppS Key +#define RADIOLIB_LORAWAN_APPS_KEY 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x-- +#endif + +// copy over the keys in to the something that will not compile if incorrectly formatted +uint32_t devAddr = RADIOLIB_LORAWAN_DEV_ADDR; +uint8_t sNwkSEncKey[] = { RADIOLIB_LORAWAN_NWKSENC_KEY }; +uint8_t appSKey[] = { RADIOLIB_LORAWAN_APPS_KEY }; + +#if (LORAWAN_VERSION == 1) +#ifndef RADIOLIB_LORAWAN_FNWKSINT_KEY // Replace with your FNwkSInt Key +#define RADIOLIB_LORAWAN_FNWKSINT_KEY 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x-- +#endif +#ifndef RADIOLIB_LORAWAN_SNWKSINT_KEY // Replace with your SNwkSInt Key +#define RADIOLIB_LORAWAN_SNWKSINT_KEY 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x-- +#endif +uint8_t fNwkSIntKey[] = { RADIOLIB_LORAWAN_FNWKSINT_KEY }; // LW v1.1 only +uint8_t sNwkSIntKey[] = { RADIOLIB_LORAWAN_SNWKSINT_KEY }; // LW v1.1 only +#endif + +#endif // OTAA/ABP + +// for the curious, the #ifndef blocks allow for automated testing &/or you can +// put your EUI & keys in to your platformio.ini - see wiki for more tips + +// regional choices: EU868, US915, AU915, AS923, IN865, KR920, CN780, CN500 +const LoRaWANBand_t Region = EU868; +const uint8_t subBand = 0; // For US915, change this to 2, otherwise leave on 0 + +// ============================================================================ +// Below is to support the sketch - only make changes if the notes say so ... + +// create the LoRaWAN node +LoRaWANNode node(&radio, &Region, subBand); + +// helper function to display any issues +void debug(bool isFail, const __FlashStringHelper* message, int state, bool Freeze) { + if (isFail) { + Serial.print(message); + Serial.print("("); + Serial.print(state); + Serial.println(")"); + while (Freeze); + } +} + +// helper function to display a byte array +void arrayDump(uint8_t *buffer, uint16_t len) { + for(uint16_t c = 0; c < len; c++) { + char b = buffer[c]; + if(b < 0x10) { Serial.print('0'); } + Serial.print(b, HEX); + } + Serial.println(); +} + +#endif \ No newline at end of file diff --git a/examples/LoRaWAN/LoRaWAN_TS_Packages/lorawan.h b/examples/LoRaWAN/LoRaWAN_TS_Packages/lorawan.h new file mode 100644 index 00000000..95220fcb --- /dev/null +++ b/examples/LoRaWAN/LoRaWAN_TS_Packages/lorawan.h @@ -0,0 +1,76 @@ +#ifndef _LORAWAN_H +#define _LORAWAN_H + +#include +#include "config.h" + +#warning "You are required to implement persistence here! (ESP32 example provided in comments)" + +// #include +// Preferences store; +// uint8_t LWnonces[RADIOLIB_LORAWAN_NONCES_BUF_SIZE]; + +bool lwBegin() { +#if (LORAWAN_OTAA == 1) + #if (LORAWAN_VERSION == 1) + node.beginOTAA(joinEUI, devEUI, nwkKey, appKey); + #else + node.beginOTAA(joinEUI, devEUI, NULL, appKey); + #endif +#else + #if (LORAWAN_VERSION == 1) + node.beginABP(devAddr, fNwkSIntKey, sNwkSIntKey, sNwkSEncKey, appSKey); + #else + node.beginABP(devAddr, NULL, NULL, sNwkSEncKey, appSKey); + #endif +#endif + return(true); +} + +int16_t lwRestore() { + int16_t state = RADIOLIB_ERR_UNKNOWN; + + // store.begin("radiolib"); + // if (store.isKey("nonces")) { + // radio.standby(); + + // store.getBytes("nonces", LWnonces, RADIOLIB_LORAWAN_NONCES_BUF_SIZE); + // state = node.setBufferNonces(LWnonces); + // } + // store.end(); + + return(state); +} + +void lwActivate() { + int16_t state = RADIOLIB_ERR_NETWORK_NOT_JOINED; + Serial.println(F("[LoRaWAN] Attempting network join ... ")); + + radio.standby(); + +#if (LORAWAN_OTAA == 1) + state = node.activateOTAA(); +#else + state = node.activateABP(); +#endif + + if(state == RADIOLIB_LORAWAN_SESSION_RESTORED) { + Serial.println(F("[LoRaWAN] Session restored!")); + return; + } + + // store.begin("radiolib"); + // uint8_t *persist = node.getBufferNonces(); + // store.putBytes("nonces", persist, RADIOLIB_LORAWAN_NONCES_BUF_SIZE); + // store.end(); + + if(state == RADIOLIB_LORAWAN_NEW_SESSION) { + Serial.println(F("[LoRaWAN] Successfully started new session!")); + return; + } + + Serial.print(F("[LoRaWAN] Failed, code ")); + Serial.println(state); +} + +#endif \ No newline at end of file diff --git a/src/TypeDef.h b/src/TypeDef.h index ae1841d9..c9d550f9 100644 --- a/src/TypeDef.h +++ b/src/TypeDef.h @@ -549,9 +549,9 @@ #define RADIOLIB_ERR_JOIN_NONCE_INVALID (-1111) /*! - \brief The downlink frame counter is slightly lower than expected, looks like a replay attack. + \brief The downlink MIC could not be verified (incorrect key or invalid FCnt) */ -#define RADIOLIB_ERR_DOWNLINK_FCNT_INVALID (-1112) +#define RADIOLIB_ERR_MIC_MISMATCH (-1112) /*! \brief Multicast frame counter is invalid (outside bounds). @@ -598,11 +598,6 @@ */ #define RADIOLIB_ERR_INVALID_MODE (-1121) -/*! - \brief The downlink MIC could not be verified (incorrect key or invalid FCnt) -*/ -#define RADIOLIB_ERR_MIC_MISMATCH (-1122) - // LR11x0-specific status codes /*! diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index e2cf3ee0..29f569ae 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -14,6 +14,9 @@ LoRaWANNode::LoRaWANNode(PhysicalLayer* phy, const LoRaWANBand_t* band, uint8_t this->txPowerMax = this->band->powerMax; this->subBand = subBand; memset(this->channelPlan, 0, sizeof(this->channelPlan)); + for(int i = 0; i < RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES; i++) { + this->packages[i] = RADIOLIB_LORAWAN_PACKAGE_NONE; + } } #if defined(RADIOLIB_BUILD_ARDUINO) @@ -228,10 +231,6 @@ int16_t LoRaWANNode::sendReceive(const uint8_t* dataUp, size_t lenUp, uint8_t fP LoRaWANNode::clearMacCommands(this->fOptsUp, &this->fOptsUpLen, RADIOLIB_LORAWAN_UPLINK); return(rxWindow); } - - // a downlink was received, so we can clear the whole MAC uplink buffer - memset(this->fOptsUp, 0, RADIOLIB_LORAWAN_FHDR_FOPTS_MAX_LEN); - this->fOptsUpLen = 0; state = this->parseDownlink(dataDown, lenDown, rxWindow, eventDown); RADIOLIB_ASSERT(state); @@ -1143,40 +1142,30 @@ void LoRaWANNode::stopMulticastSession() { int16_t LoRaWANNode::isValidUplink(size_t len, uint8_t fPort) { // check destination fPort - switch(fPort) { - case RADIOLIB_LORAWAN_FPORT_MAC_COMMAND: { - // MAC FPort only good if internally overruled (no application payload) - if (!this->isMACPayload) { - RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Requested uplink at FPort %d - rejected! This FPort is reserved.", fPort); - return(RADIOLIB_ERR_INVALID_PORT); - } - } break; - case RADIOLIB_LORAWAN_FPORT_TS009: { - // TS009 FPort only good if overruled during verification testing - if(!this->TS009) { - RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Requested uplink at FPort %d - rejected! This FPort is not enabled.", fPort); - return(RADIOLIB_ERR_INVALID_PORT); - } - } break; - case RADIOLIB_LORAWAN_FPORT_TS011: { - // TS011 FPort only good if overruled during relay exchange - if(!this->TS011) { - RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Requested uplink at FPort %d - rejected! This FPort is not enabled.", fPort); - return(RADIOLIB_ERR_INVALID_PORT); - } - } break; - default: { - if((fPort >= RADIOLIB_LORAWAN_FPORT_PAYLOAD_MIN) && (fPort <= RADIOLIB_LORAWAN_FPORT_PAYLOAD_MAX)) { - // user payload ports, all good + bool ok = false; + if(fPort == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND && this->isMACPayload) { + ok = true; + } + if(fPort >= RADIOLIB_LORAWAN_FPORT_PAYLOAD_MIN && fPort <= RADIOLIB_LORAWAN_FPORT_PAYLOAD_MAX) { + ok = true; + } + if(fPort >= RADIOLIB_LORAWAN_FPORT_RESERVED) { + for(int id = 0; id < RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES; id++) { + if(this->packages[id].enabled && fPort == this->packages[id].packFPort) { + ok = true; break; } - RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Requested uplink at FPort %d - rejected! This FPort is reserved.", fPort); - } break; + } + } + + if(!ok) { + RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Requested uplink at FPort %d - rejected! This FPort is reserved.", fPort); + return(RADIOLIB_ERR_INVALID_PORT); } // check maximum payload len as defined in band uint8_t maxPayLen = this->band->payloadLenMax[this->channels[RADIOLIB_LORAWAN_UPLINK].dr]; - if(this->TS011) { + if(this->packages[RADIOLIB_LORAWAN_PACKAGE_TS011].enabled) { maxPayLen = RADIOLIB_MIN(maxPayLen, 222); // payload length is limited to 222 if under repeater } @@ -1340,9 +1329,16 @@ void LoRaWANNode::composeUplink(const uint8_t* in, uint8_t lenIn, uint8_t* out, // select encryption key based on the target fPort uint8_t* encKey = this->appSKey; - if((fPort == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND) || (fPort == RADIOLIB_LORAWAN_FPORT_TS011)) { + if(fPort == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND) { encKey = this->nwkSEncKey; } + // check if any of the packages uses this FPort + for(int id = 0; id < RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES; id++) { + if(this->packages[id].enabled && fPort == this->packages[id].packFPort) { + encKey = this->packages[id].isAppPack ? this->appSKey : this->nwkSEncKey; + break; + } + } // encrypt the frame payload processAES(in, lenIn, encKey, &out[RADIOLIB_LORAWAN_FRAME_PAYLOAD_POS(this->fOptsUpLen)], this->devAddr, this->fCntUp, RADIOLIB_LORAWAN_UPLINK, 0x00, true); @@ -1496,7 +1492,7 @@ int16_t LoRaWANNode::receiveClassA(uint8_t dir, const LoRaWANChannel_t* dlChanne // get the maximum allowed Time-on-Air of a packet given the current datarate uint8_t maxPayLen = this->band->payloadLenMax[dlChannel->dr]; - if(this->TS011) { + if(this->packages[RADIOLIB_LORAWAN_PACKAGE_TS011].enabled) { maxPayLen = RADIOLIB_MIN(maxPayLen, 222); // payload length is limited to 222 if under repeater } RadioLibTime_t tMax = this->phyLayer->getTimeOnAir(maxPayLen + 13) / 1000; // mandatory FHDR is 12/13 bytes @@ -1669,7 +1665,7 @@ int16_t LoRaWANNode::receiveClassC(RadioLibTime_t timeout) { // the specified maximum length M over the data rate used to receive the frame // SHALL be silently discarded. uint8_t maxPayLen = this->band->payloadLenMax[this->channels[RADIOLIB_LORAWAN_RX_BC].dr]; - if(this->TS011) { + if(this->packages[RADIOLIB_LORAWAN_PACKAGE_TS011].enabled) { maxPayLen = RADIOLIB_MIN(maxPayLen, 222); // payload length is limited to 222 if under repeater } if(this->phyLayer->getPacketLength() > (size_t)(maxPayLen + 13)) { // mandatory FHDR is 12/13 bytes @@ -1779,12 +1775,12 @@ int16_t LoRaWANNode::parseDownlink(uint8_t* data, size_t* len, uint8_t window, L return(RADIOLIB_ERR_DOWNLINK_MALFORMED); } - // calculate length of piggy-backed FOpts - uint8_t fOptsPbLen = downlinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] & RADIOLIB_LORAWAN_FHDR_FOPTS_LEN_MASK; + // calculate length of (piggy-backed) FOpts + uint8_t fOptsLen = downlinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] & RADIOLIB_LORAWAN_FHDR_FOPTS_LEN_MASK; - // MHDR(1) - DevAddr(4) - FCtrl(1) - FCnt(2) - FOptsPb - Payload - MIC(4) + // MHDR(1) - DevAddr(4) - FCtrl(1) - FCnt(2) - FOpts - Payload - MIC(4) // potentially also an FPort, will find out next - uint8_t payLen = downlinkMsgLen - 1 - 4 - 1 - 2 - fOptsPbLen - 4; + uint8_t payLen = downlinkMsgLen - 1 - 4 - 1 - 2 - fOptsLen - 4; // in LoRaWAN v1.1, a frame is a Network frame if there is no Application payload // i.e.: either no payload at all (empty frame or FOpts only), or MAC only payload @@ -1795,74 +1791,62 @@ int16_t LoRaWANNode::parseDownlink(uint8_t* data, size_t* len, uint8_t window, L } if(payLen > 0) { payLen -= 1; // subtract one as fPort is set - fPort = downlinkMsg[RADIOLIB_LORAWAN_FHDR_FPORT_POS(fOptsPbLen)]; + fPort = downlinkMsg[RADIOLIB_LORAWAN_FHDR_FPORT_POS(fOptsLen)]; - // check if fPort value is actually allowed - switch(fPort) { - case RADIOLIB_LORAWAN_FPORT_MAC_COMMAND: { - // payload consists of all MAC commands (or is empty) + // check destination fPort + bool ok = false; - // LoRaWAN v1.0.4 only: A Class B/C downlink SHALL NOT transport any MAC command. - // (...) it SHALL silently discard the entire frame. - // However, we also enforce this for LoRaWAN v1.1 (TTS does not allow this anyway). - if(window == RADIOLIB_LORAWAN_RX_BC) { - #if !RADIOLIB_STATIC_ONLY - delete[] downlinkMsg; - #endif - return(RADIOLIB_ERR_DOWNLINK_MALFORMED); - } - } break; - case RADIOLIB_LORAWAN_FPORT_TS009: { - // TS009 FPort only good if overruled during verification testing - if(!this->TS009) { - RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Downlink at FPort %d - rejected! This FPort is not enabled.", fPort); - #if !RADIOLIB_STATIC_ONLY - delete[] downlinkMsg; - #endif - return(RADIOLIB_ERR_INVALID_PORT); - } - isAppDownlink = true; - } break; - case RADIOLIB_LORAWAN_FPORT_TS011: { - // TS011 FPort only good if overruled during relay exchange - if(!this->TS011) { - RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Downlink at FPort %d - rejected! This FPort is not enabled.", fPort); - #if !RADIOLIB_STATIC_ONLY - delete[] downlinkMsg; - #endif - return(RADIOLIB_ERR_INVALID_PORT); - } - isAppDownlink = true; - } break; - default: { - if((fPort >= RADIOLIB_LORAWAN_FPORT_PAYLOAD_MIN) && (fPort <= RADIOLIB_LORAWAN_FPORT_PAYLOAD_MAX)) { - // payload is user-defined (or empty) - may carry piggybacked MAC commands - isAppDownlink = true; + // LoRaWAN v1.0.4 only: A Class B/C downlink SHALL NOT transport any MAC command. + // (...) it SHALL silently discard the entire frame. + // However, we also enforce this for LoRaWAN v1.1 (TTS does not allow this anyway). + if(fPort == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND && window < RADIOLIB_LORAWAN_RX_BC) { + // payload consists of all MAC commands (or is empty) + ok = true; + } + if(fPort >= RADIOLIB_LORAWAN_FPORT_PAYLOAD_MIN && fPort <= RADIOLIB_LORAWAN_FPORT_PAYLOAD_MAX) { + ok = true; + isAppDownlink = true; + } + // check if any of the packages uses this FPort + if(fPort >= RADIOLIB_LORAWAN_FPORT_RESERVED) { + for(int id = 0; id < RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES; id++) { + if(this->packages[id].enabled && fPort == this->packages[id].packFPort) { + ok = true; + isAppDownlink = this->packages[id].isAppPack; break; } - RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Downlink at FPort %d - rejected! This FPort is reserved.", fPort); - #if !RADIOLIB_STATIC_ONLY - delete[] downlinkMsg; - #endif - return(RADIOLIB_ERR_INVALID_PORT); - } break; + } } + if(!ok) { + RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Received downlink at FPort %d - rejected! This FPort is reserved.", fPort); + #if !RADIOLIB_STATIC_ONLY + delete[] downlinkMsg; + #endif + return(RADIOLIB_ERR_INVALID_PORT); + } } - // MAC commands SHALL NOT be present in the payload field and the frame options field simultaneously. - // Should this occur, the end-device SHALL silently discard the frame. - if(fOptsPbLen > 0 && payLen > 0 && fPort == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND) { - #if !RADIOLIB_STATIC_ONLY - delete[] downlinkMsg; - #endif - return(RADIOLIB_ERR_DOWNLINK_MALFORMED); + // handle FOpts in uplink with FPort = 0 (or absent which means 0) + if(fPort == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND) { + if(fOptsLen > 0 && payLen > 0) { + // MAC commands SHALL NOT be present in the payload field and the frame options field simultaneously. + // Should this occur, the end-device SHALL silently discard the frame. + #if !RADIOLIB_STATIC_ONLY + delete[] downlinkMsg; + #endif + return(RADIOLIB_ERR_DOWNLINK_MALFORMED); + } + // if FOpts are in the payload, use this instead + if(payLen > 0) { + fOptsLen = payLen; + } } // LoRaWAN v1.0.4 only: A Class B/C downlink SHALL NOT transport any MAC command. // (...) it SHALL silently discard the entire frame. // However, we also enforce this for LoRaWAN v1.1 (TTS does not allow this anyway). - if(fOptsPbLen > 0 && window == RADIOLIB_LORAWAN_RX_BC) { + if(fOptsLen > 0 && window == RADIOLIB_LORAWAN_RX_BC) { #if !RADIOLIB_STATIC_ONLY delete[] downlinkMsg; #endif @@ -1886,17 +1870,11 @@ int16_t LoRaWANNode::parseDownlink(uint8_t* data, size_t* len, uint8_t window, L } } - // if the downlink FCnt16 value is 'slightly' lower than expected by the device, assume a replay attack - uint16_t devFCnt16 = (uint16_t)devFCnt32; - if(devFCnt16 > 0 && (uint16_t)(devFCnt16 - payFCnt16) < RADIOLIB_LORAWAN_MIN_ROLLOVER_FCNT_GAP) { - RADIOLIB_DEBUG_PROTOCOL_PRINTLN("FCnt rejected: %d -> %d", devFCnt16, payFCnt16); - return(RADIOLIB_ERR_DOWNLINK_FCNT_INVALID); - } - - // assume a rollover if the FCnt16 in the payload is smaller than the previous FCnt16 known by device + // assume a rollover if the FCnt16 in the payload is equal to / smaller + // than the previous FCnt16 known by device // (MAX_FCNT_GAP is deprecated for 1.0.4 / 1.1, TTS and CS both apply a 16-bit rollover) - if(payFCnt16 < devFCnt16) { - RADIOLIB_DEBUG_PROTOCOL_PRINTLN("FCnt rollover: %d -> %d", devFCnt16, payFCnt16); + if(devFCnt32 > 0 && payFCnt16 <= (uint16_t)devFCnt32) { + RADIOLIB_DEBUG_PROTOCOL_PRINTLN("FCnt rollover: %d -> %d", (uint16_t)devFCnt32, payFCnt16); devFCnt32 += 0x10000; // add 16-bit value } devFCnt32 &= ~0xFFFF; // clear lower 16 bits known by device @@ -1928,7 +1906,7 @@ int16_t LoRaWANNode::parseDownlink(uint8_t* data, size_t* len, uint8_t window, L } downlinkMsg[RADIOLIB_LORAWAN_BLOCK_DIR_POS] = RADIOLIB_LORAWAN_DOWNLINK; LoRaWANNode::hton(&downlinkMsg[RADIOLIB_LORAWAN_BLOCK_DEV_ADDR_POS], addr); - LoRaWANNode::hton(&downlinkMsg[RADIOLIB_LORAWAN_BLOCK_FCNT_POS], devFCnt32); + LoRaWANNode::hton(&downlinkMsg[RADIOLIB_LORAWAN_BLOCK_FCNT_POS], devFCnt32); downlinkMsg[RADIOLIB_LORAWAN_MIC_BLOCK_LEN_POS] = downlinkMsgLen - sizeof(uint32_t); // check the MIC @@ -1943,8 +1921,15 @@ int16_t LoRaWANNode::parseDownlink(uint8_t* data, size_t* len, uint8_t window, L #endif return(RADIOLIB_ERR_MIC_MISMATCH); } - - // save current FCnt to respective frame counter + + // all checks passed, so dump its contents and start processing + RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Downlink (%sFCntDown = %lu) encoded:", + (this->multicast && window == RADIOLIB_LORAWAN_RX_BC) ? "M" : + (isAppDownlink ? "A" : "N"), + (unsigned long)devFCnt32); + RADIOLIB_DEBUG_PROTOCOL_HEXDUMP(downlinkMsg, RADIOLIB_AES128_BLOCK_SIZE + downlinkMsgLen); + + // save new FCnt to respective frame counter if(this->multicast && window == RADIOLIB_LORAWAN_RX_BC) { // multicast: McApp downlink this->mcAFCnt = devFCnt32; @@ -1957,26 +1942,24 @@ int16_t LoRaWANNode::parseDownlink(uint8_t* data, size_t* len, uint8_t window, L } } - RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Downlink (%sFCntDown = %lu) encoded:", - (this->multicast && window == RADIOLIB_LORAWAN_RX_BC) ? "M" : - (isAppDownlink ? "A" : "N"), - (unsigned long)devFCnt32); - RADIOLIB_DEBUG_PROTOCOL_HEXDUMP(downlinkMsg, RADIOLIB_AES128_BLOCK_SIZE + downlinkMsgLen); - - // if this is a confirmed frame, save the downlink number (only app frames can be confirmed) bool isConfirmedDown = false; - if((downlinkMsg[RADIOLIB_LORAWAN_FHDR_LEN_START_OFFS] & 0xFE) == RADIOLIB_LORAWAN_MHDR_MTYPE_CONF_DATA_DOWN) { - this->confFCntDown = this->aFCntDown; - isConfirmedDown = true; - } - - // if this downlink is on FPort 0, the FOptsLen is the length of the payload - // in any other case, the payload (length) is user accessible - uint8_t fOptsLen = fOptsPbLen; - if(fPort == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND && payLen > 0) { - fOptsLen = payLen; - } else { - *len = payLen; + + // do some housekeeping for normal Class A downlinks (not allowed for Class B/C) + if(window < RADIOLIB_LORAWAN_RX_BC) { + // if this is a confirmed frame, save the downlink number (only app frames can be confirmed) + if((downlinkMsg[RADIOLIB_LORAWAN_FHDR_LEN_START_OFFS] & 0xFE) == RADIOLIB_LORAWAN_MHDR_MTYPE_CONF_DATA_DOWN) { + this->confFCntDown = this->aFCntDown; + isConfirmedDown = true; + } + + // a Class A downlink was received, so restart the ADR counter with the next uplink + this->adrFCnt = this->getFCntUp() + 1; + + // a Class A downlink was received, so we can clear the MAC uplink and downlink buffers + memset(this->fOptsUp, 0, RADIOLIB_LORAWAN_FHDR_FOPTS_MAX_LEN); + this->fOptsUpLen = 0; + memset(this->fOptsDown, 0, RADIOLIB_LORAWAN_FHDR_FOPTS_MAX_LEN); + this->fOptsDownLen = 0; } #if !RADIOLIB_STATIC_ONLY @@ -1985,26 +1968,162 @@ int16_t LoRaWANNode::parseDownlink(uint8_t* data, size_t* len, uint8_t window, L uint8_t fOpts[RADIOLIB_STATIC_ARRAY_SIZE]; #endif - // figure out if the payload should end up in user data or internal FOpts buffer - uint8_t* dest; - if(fPort == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND) { - dest = fOpts; - } else { - dest = data; + // decrypt any FOpts on FPort = 0, in which case FOptsLen is the length of the payload + if(fPort == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND && payLen > 0) { + payLen = 0; + processAES(&downlinkMsg[RADIOLIB_LORAWAN_FRAME_PAYLOAD_POS(0)], fOptsLen, this->nwkSEncKey, fOpts, addr, devFCnt32, RADIOLIB_LORAWAN_DOWNLINK, 0x00, true); + + // decrypt any piggy-backed FOpts + } else if(fOptsLen > 0) { + // the decryption depends on the LoRaWAN version + if(this->rev == 1) { + // in LoRaWAN v1.1, the piggy-backed FOpts are encrypted using the NwkSEncKey + uint8_t ctrId = 0x01 + isAppDownlink; // see LoRaWAN v1.1 errata + processAES(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], (size_t)fOptsLen, this->nwkSEncKey, fOpts, this->devAddr, devFCnt32, RADIOLIB_LORAWAN_DOWNLINK, ctrId, true); + } else { + // in LoRaWAN v1.0.x, the piggy-backed FOpts are unencrypted + memcpy(fOpts, &downlinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], (size_t)fOptsLen); + } + } + + // process any FOpts + if(fOptsLen > 0) { + uint8_t* mPtr = fOpts; + uint8_t procLen = 0; + uint8_t fOptsRe[RADIOLIB_LORAWAN_MAX_DOWNLINK_SIZE] = { 0 }; + uint8_t fOptsReLen = 0; + + // indication whether LinkAdr MAC command has been processed + bool mAdr = false; + + while(procLen < fOptsLen) { + uint8_t cid = *mPtr; // MAC id is the first byte + + // fetch length of MAC downlink command and uplink response + uint8_t fLen = 1; + uint8_t fLenRe = 1; + state = this->getMacLen(cid, &fLen, RADIOLIB_LORAWAN_DOWNLINK, true, mPtr + 1); + if(state != RADIOLIB_ERR_NONE) { + RADIOLIB_DEBUG_PROTOCOL_PRINTLN("WARNING: Unknown MAC CID %02x", cid); + RADIOLIB_DEBUG_PROTOCOL_PRINTLN("WARNING: Skipping remaining MAC commands"); + fOptsLen = procLen; // truncate to last processed MAC command + break; + } + (void)this->getMacLen(cid, &fLenRe, RADIOLIB_LORAWAN_UPLINK, true); + + // check whether the complete payload is present + if(procLen + fLen > fOptsLen) { + RADIOLIB_DEBUG_PROTOCOL_PRINTLN("WARNING: Incomplete MAC command %02x (%d bytes, expected %d)", cid, fOptsLen - procLen, fLen); + RADIOLIB_DEBUG_PROTOCOL_PRINTLN("WARNING: Skipping remaining MAC commands"); + fOptsLen = procLen; // truncate to last processed MAC command + break; + } + + bool reply = false; + + // if this is a LinkAdr MAC command, pre-process contiguous commands into one atomic block + if(cid == RADIOLIB_LORAWAN_MAC_LINK_ADR) { + // if there was any LinkAdr command before, set NACK and continue without processing + if(mAdr) { + reply = true; + fOptsRe[fOptsReLen + 1] = 0x00; + + // if this is the first LinkAdr command, do some special treatment: + } else { + mAdr = true; + uint8_t fAdrLen = 5; + uint8_t mAdrOpt[14] = { 0 }; + + // retrieve all contiguous LinkAdr commands + while(procLen + fLen + fAdrLen < fOptsLen + 1 && *(mPtr + fLen) == RADIOLIB_LORAWAN_MAC_LINK_ADR) { + fLen += 5; // ADR command is 5 bytes + fLenRe += 2; // ADR response is 2 bytes + } + + // pre-process them into a single complete channel mask (stored in mAdrOpt) + LoRaWANNode::preprocessMacLinkAdr(mPtr, fLen, mAdrOpt); + + // execute like a normal MAC command (but pointing to mAdrOpt instead) + reply = this->execMacCommand(cid, mAdrOpt, 14, &fOptsRe[fOptsReLen + 1]); + + // in LoRaWAN v1.0.x, all ACK bytes should have equal status - fix in post-processing + if(this->rev == 0) { + LoRaWANNode::postprocessMacLinkAdr(&fOptsRe[fOptsReLen], fLen); + + // in LoRaWAN v1.1, just provide one ACK, so no post-processing but cut off reply length + } else { + fLenRe = 2; + } + } + + // MAC command other than LinkAdr, just process the payload + } else { + reply = this->execMacCommand(cid, mPtr + 1, fLen - 1, &fOptsRe[fOptsReLen + 1]); + } + + // if there is a reply, only add it to the reply if maximum payload size allows + if(reply && (fOptsReLen + fLenRe <= this->band->payloadLenMax[this->channels[RADIOLIB_LORAWAN_UPLINK].dr])) { + fOptsRe[fOptsReLen] = cid; + fOptsReLen += fLenRe; + } + + procLen += fLen; + mPtr += fLen; + } + + // remove all MAC commands except those whose payload can be requested by the user + // (which are LinkCheck and DeviceTime) + LoRaWANNode::clearMacCommands(fOpts, &fOptsLen, RADIOLIB_LORAWAN_DOWNLINK); + memcpy(this->fOptsDown, fOpts, fOptsLen); + this->fOptsDownLen = fOptsLen; + + // if fOptsLen for the next uplink is larger than can be piggybacked onto an uplink, send separate uplink + if(fOptsReLen > RADIOLIB_LORAWAN_FHDR_FOPTS_MAX_LEN) { + + RADIOLIB_DEBUG_PROTOCOL_PRINTLN("! Sending MAC-only uplink (%d bytes):", fOptsReLen); + RADIOLIB_DEBUG_PROTOCOL_HEXDUMP(fOptsRe, fOptsReLen); + + this->isMACPayload = true; + + // temporarily lift dutyCycle restrictions to allow immediate MAC response + bool prevDC = this->dutyCycleEnabled; + this->dutyCycleEnabled = false; + this->sendReceive(fOptsRe, fOptsReLen, RADIOLIB_LORAWAN_FPORT_MAC_COMMAND); + this->dutyCycleEnabled = prevDC; + + } else { // fOptsReLen <= 15 + memcpy(this->fOptsUp, fOptsRe, fOptsReLen); + this->fOptsUpLen = fOptsReLen; + } } // figure out which key to use to decrypt the payload uint8_t* encKey = this->appSKey; - if((fPort == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND) || (fPort == RADIOLIB_LORAWAN_FPORT_TS011)) { - encKey = this->nwkSEncKey; - } if(this->multicast && window == RADIOLIB_LORAWAN_RX_BC) { encKey = this->mcAppSKey; } + for(int id = 0; id < RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES; id++) { + if(this->packages[id].enabled && fPort == this->packages[id].packFPort) { + encKey = this->packages[id].isAppPack ? this->appSKey : this->nwkSEncKey; + break; + } + } // decrypt the frame payload - processAES(&downlinkMsg[RADIOLIB_LORAWAN_FRAME_PAYLOAD_POS(fOptsPbLen)], payLen, encKey, dest, addr, devFCnt32, RADIOLIB_LORAWAN_DOWNLINK, 0x00, true); - + // by default, the data and length are user-accessible + processAES(&downlinkMsg[RADIOLIB_LORAWAN_FRAME_PAYLOAD_POS(fOptsLen)], payLen, encKey, data, addr, devFCnt32, RADIOLIB_LORAWAN_DOWNLINK, 0x00, true); + *len = payLen; + + // however, if this frame belongs to a package, redirect instead and 'hide' contents from the user + // just to be sure that it doesn't get re-interpreted... + for(int id = 0; id < RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES; id++) { + if(this->packages[id].enabled && fPort == this->packages[id].packFPort) { + this->packages[id].callback(data, *len); + memset(data, 0, *len); + *len = 0; + } + } + // pass the event info if requested if(event) { event->dir = RADIOLIB_LORAWAN_DOWNLINK; @@ -2019,151 +2138,6 @@ int16_t LoRaWANNode::parseDownlink(uint8_t* data, size_t* len, uint8_t window, L event->multicast = (bool)this->multicast; } - // for RxBC downlinks, return already, we aren't allowed to do any FOpts stuff - if(window == RADIOLIB_LORAWAN_RX_BC) { - #if !RADIOLIB_STATIC_ONLY - delete[] fOpts; - delete[] downlinkMsg; - #endif - return(RADIOLIB_ERR_NONE); - } - - // a downlink was received, so restart the ADR counter with the next uplink - this->adrFCnt = this->getFCntUp() + 1; - - // decrypt any piggy-backed FOpts - if(fOptsPbLen > 0) { - // the decryption depends on the LoRaWAN version - if(this->rev == 1) { - // in LoRaWAN v1.1, the piggy-backed FOpts are encrypted using the NwkSEncKey - uint8_t ctrId = 0x01 + isAppDownlink; // see LoRaWAN v1.1 errata - processAES(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], (size_t)fOptsPbLen, this->nwkSEncKey, fOpts, this->devAddr, devFCnt32, RADIOLIB_LORAWAN_DOWNLINK, ctrId, true); - } else { - // in LoRaWAN v1.0.x, the piggy-backed FOpts are unencrypted - memcpy(fOpts, &downlinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], (size_t)fOptsPbLen); - } - } - - // clear the previous MAC commands, if any - memset(this->fOptsDown, 0, RADIOLIB_LORAWAN_FHDR_FOPTS_MAX_LEN); - - // process FOpts (if there are any) - uint8_t cid; - uint8_t fLen = 1; - uint8_t* mPtr = fOpts; - uint8_t procLen = 0; - uint8_t fOptsRe[RADIOLIB_LORAWAN_MAX_DOWNLINK_SIZE] = { 0 }; - uint8_t fOptsReLen = 0; - - // indication whether LinkAdr MAC command has been processed - bool mAdr = false; - - while(procLen < fOptsLen) { - cid = *mPtr; // MAC id is the first byte - - // fetch length of MAC downlink payload - state = this->getMacLen(cid, &fLen, RADIOLIB_LORAWAN_DOWNLINK, true, mPtr + 1); - if(state != RADIOLIB_ERR_NONE) { - RADIOLIB_DEBUG_PROTOCOL_PRINTLN("WARNING: Unknown MAC CID %02x", cid); - RADIOLIB_DEBUG_PROTOCOL_PRINTLN("WARNING: Skipping remaining MAC commands"); - fOptsLen = procLen; // truncate to last processed MAC command - break; - } - - // already fetch length of MAC answer payload (if any), include CID - uint8_t fLenRe = 0; - (void)this->getMacLen(cid, &fLenRe, RADIOLIB_LORAWAN_UPLINK, true); - // don't care about return value: the previous getMacLen() would have failed anyway - - if(procLen + fLen > fOptsLen) { - RADIOLIB_DEBUG_PROTOCOL_PRINTLN("WARNING: Incomplete MAC command %02x (%d bytes, expected %d)", cid, fOptsLen - procLen, fLen); - RADIOLIB_DEBUG_PROTOCOL_PRINTLN("WARNING: Skipping remaining MAC commands"); - fOptsLen = procLen; // truncate to last processed MAC command - break; - } - - bool reply = false; - - // if this is a LinkAdr MAC command, pre-process contiguous commands into one atomic block - if(cid == RADIOLIB_LORAWAN_MAC_LINK_ADR) { - // if there was any LinkAdr command before, set NACK and continue without processing - if(mAdr) { - reply = true; - fOptsRe[fOptsReLen + 1] = 0x00; - - // if this is the first LinkAdr command, do some special treatment: - } else { - mAdr = true; - uint8_t fAdrLen = 5; - uint8_t mAdrOpt[14] = { 0 }; - - // retrieve all contiguous LinkAdr commands - while(procLen + fLen + fAdrLen < fOptsLen + 1 && *(mPtr + fLen) == RADIOLIB_LORAWAN_MAC_LINK_ADR) { - fLen += 5; // ADR command is 5 bytes - fLenRe += 2; // ADR response is 2 bytes - } - - // pre-process them into a single complete channel mask (stored in mAdrOpt) - LoRaWANNode::preprocessMacLinkAdr(mPtr, fLen, mAdrOpt); - - // execute like a normal MAC command (but pointing to mAdrOpt instead) - reply = this->execMacCommand(cid, mAdrOpt, 14, &fOptsRe[fOptsReLen + 1]); - - // in LoRaWAN v1.0.x, all ACK bytes should have equal status - fix in post-processing - if(this->rev == 0) { - LoRaWANNode::postprocessMacLinkAdr(&fOptsRe[fOptsReLen], fLen); - - // in LoRaWAN v1.1, just provide one ACK, so no post-processing but cut off reply length - } else { - fLenRe = 2; - } - } - - // MAC command other than LinkAdr, just process the payload - } else { - reply = this->execMacCommand(cid, mPtr + 1, fLen - 1, &fOptsRe[fOptsReLen + 1]); - } - - // if there is a reply, only add it to the reply if maximum payload size allows - if(reply && (fOptsReLen + fLenRe <= this->band->payloadLenMax[this->channels[RADIOLIB_LORAWAN_UPLINK].dr])) { - fOptsRe[fOptsReLen] = cid; - fOptsReLen += fLenRe; - } - - procLen += fLen; - mPtr += fLen; - } - - // remove all MAC commands except those whose payload can be requested by the user - // (which are LinkCheck and DeviceTime) - if(fOptsLen > 0) { - LoRaWANNode::clearMacCommands(fOpts, &fOptsLen, RADIOLIB_LORAWAN_DOWNLINK); - memcpy(this->fOptsDown, fOpts, fOptsLen); - } - this->fOptsDownLen = fOptsLen; - - // if fOptsLen for the next uplink is larger than can be piggybacked onto an uplink, send separate uplink - if(fOptsReLen > RADIOLIB_LORAWAN_FHDR_FOPTS_MAX_LEN) { - - RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Uplink MAC-only payload (%d bytes):", fOptsReLen); - RADIOLIB_DEBUG_PROTOCOL_HEXDUMP(fOptsRe, fOptsReLen); - - this->isMACPayload = true; - // temporarily lift dutyCycle restrictions to allow immediate MAC response - bool prevDC = this->dutyCycleEnabled; - this->dutyCycleEnabled = false; - - RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Sending MAC-only uplink .. "); - - this->sendReceive(fOptsRe, fOptsReLen, RADIOLIB_LORAWAN_FPORT_MAC_COMMAND); - - this->dutyCycleEnabled = prevDC; - - } else { // fOptsReLen <= 15 - memcpy(this->fOptsUp, fOptsRe, fOptsReLen); - this->fOptsUpLen = fOptsReLen; - } - #if !RADIOLIB_STATIC_ONLY delete[] fOpts; delete[] downlinkMsg; @@ -3101,6 +3075,7 @@ void LoRaWANNode::setDutyCycle(bool enable, RadioLibTime_t msPerHour) { this->dutyCycleEnabled = enable; if(!enable) { this->dutyCycle = 0; + return; } if(msPerHour == 0) { this->dutyCycle = this->band->dutyCycle; @@ -3662,7 +3637,7 @@ uint8_t LoRaWANNode::getMaxPayloadLen() { uint8_t minLen = 0; uint8_t maxLen = this->band->payloadLenMax[this->channels[RADIOLIB_LORAWAN_UPLINK].dr]; - if(this->TS011) { + if(this->packages[RADIOLIB_LORAWAN_PACKAGE_TS011].enabled) { maxLen = RADIOLIB_MIN(maxLen, 222); // payload length is limited to N=222 if under repeater } maxLen += 13; // mandatory FHDR is 12/13 bytes @@ -3697,6 +3672,58 @@ void LoRaWANNode::setSleepFunction(SleepCb_t cb) { this->sleepCb = cb; } +int16_t LoRaWANNode::addAppPackage(uint8_t packageId, PackageCb_t callback) { + if(packageId >= RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES) { + return(RADIOLIB_ERR_INVALID_MODE); + } + return(this->addAppPackage(packageId, callback, PackageTable[packageId].packFPort)); +} + +int16_t LoRaWANNode::addAppPackage(uint8_t packageId, PackageCb_t callback, uint8_t fPort) { + if(packageId >= RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES) { + return(RADIOLIB_ERR_INVALID_MODE); + } + if(PackageTable[packageId].isAppPack == false) { + return(RADIOLIB_ERR_INVALID_MODE); + } + if(PackageTable[packageId].fixedFPort && fPort != PackageTable[packageId].packFPort) { + return(RADIOLIB_ERR_INVALID_PORT); + } + if(callback == NULL) { + return(RADIOLIB_ERR_NULL_POINTER); + } + this->packages[packageId] = PackageTable[packageId]; + this->packages[packageId].packFPort = fPort; + this->packages[packageId].callback = callback; + this->packages[packageId].enabled = true; + return(RADIOLIB_ERR_NONE); +} + +int16_t LoRaWANNode::addNwkPackage(uint8_t packageId, PackageCb_t callback) { + if(packageId >= RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES) { + return(RADIOLIB_ERR_INVALID_MODE); + } + if(PackageTable[packageId].isAppPack == true) { + return(RADIOLIB_ERR_INVALID_MODE); + } + if(callback == NULL) { + return(RADIOLIB_ERR_NULL_POINTER); + } + this->packages[packageId] = PackageTable[packageId]; + this->packages[packageId].callback = callback; + this->packages[packageId].enabled = true; + return(RADIOLIB_ERR_NONE); +} + +void LoRaWANNode::removePackage(uint8_t packageId) { + // silently ignore, assume that the user supplies decent index + if(packageId >= RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES) { + return; + } + this->packages[packageId].enabled = false; + return; +} + int16_t LoRaWANNode::findDataRate(uint8_t dr, DataRate_t* dataRate) { int16_t state = RADIOLIB_ERR_NONE; diff --git a/src/protocols/LoRaWAN/LoRaWAN.h b/src/protocols/LoRaWAN/LoRaWAN.h index 98ea9d7d..d9d9e30c 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.h +++ b/src/protocols/LoRaWAN/LoRaWAN.h @@ -44,8 +44,6 @@ #define RADIOLIB_LORAWAN_FPORT_MAC_COMMAND (0x00 << 0) // 7 0 payload contains MAC commands only #define RADIOLIB_LORAWAN_FPORT_PAYLOAD_MIN (0x01 << 0) // 7 0 start of user-allowed fPort range #define RADIOLIB_LORAWAN_FPORT_PAYLOAD_MAX (0xDF << 0) // 7 0 end of user-allowed fPort range -#define RADIOLIB_LORAWAN_FPORT_TS009 (0xE0 << 0) // 7 0 fPort used for TS009 testing -#define RADIOLIB_LORAWAN_FPORT_TS011 (0xE2 << 0) // 7 0 fPort used for TS011 Forwarding #define RADIOLIB_LORAWAN_FPORT_RESERVED (0xE0 << 0) // 7 0 fPort values equal to and larger than this are reserved // data rate encoding @@ -97,9 +95,6 @@ #define RADIOLIB_LORAWAN_REJOIN_MAX_COUNT_N (10) // send rejoin request 16384 uplinks #define RADIOLIB_LORAWAN_REJOIN_MAX_TIME_N (15) // once every year, not actually implemented -// developer recommended default setting (not specified) -#define RADIOLIB_LORAWAN_MIN_ROLLOVER_FCNT_GAP (16384) // equal to deprecated MaxFCntGap - // join request message layout #define RADIOLIB_LORAWAN_JOIN_REQUEST_LEN (23) #define RADIOLIB_LORAWAN_JOIN_REQUEST_JOIN_EUI_POS (1) @@ -202,6 +197,27 @@ #define RADIOLIB_LORAWAN_MAC_DEVICE_MODE (0x20) #define RADIOLIB_LORAWAN_MAC_PROPRIETARY (0x80) +// number of supported LoRaWAN TS packages +#define RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES (7) + +// Package ID numbering as per TS008 (TS011 ID is made up) +#define RADIOLIB_LORAWAN_PACKAGE_TS007 (0) +#define RADIOLIB_LORAWAN_PACKAGE_TS003 (1) +#define RADIOLIB_LORAWAN_PACKAGE_TS005 (2) +#define RADIOLIB_LORAWAN_PACKAGE_TS004 (3) +#define RADIOLIB_LORAWAN_PACKAGE_TS006 (4) +#define RADIOLIB_LORAWAN_PACKAGE_TS009 (5) +#define RADIOLIB_LORAWAN_PACKAGE_TS011 (6) + +// Package FPort (specified or recommended) +#define RADIOLIB_LORAWAN_FPORT_TS007 (225) +#define RADIOLIB_LORAWAN_FPORT_TS003 (202) +#define RADIOLIB_LORAWAN_FPORT_TS005 (200) +#define RADIOLIB_LORAWAN_FPORT_TS004 (201) +#define RADIOLIB_LORAWAN_FPORT_TS006 (203) +#define RADIOLIB_LORAWAN_FPORT_TS009 (224) +#define RADIOLIB_LORAWAN_FPORT_TS011 (226) + // the length of internal MAC command queue - hopefully this is enough for most use cases #define RADIOLIB_LORAWAN_MAC_COMMAND_QUEUE_SIZE (9) @@ -261,6 +277,45 @@ constexpr LoRaWANMacCommand_t MacTable[RADIOLIB_LORAWAN_NUM_MAC_COMMANDS] = { { RADIOLIB_LORAWAN_MAC_PROPRIETARY, 5, 0, false, true }, }; +/*! \brief A user-supplied callback for LoRaWAN Application Packages (TSxxx) */ +typedef void (*PackageCb_t)(uint8_t* dataDown, size_t lenDown); + +/*! + \struct LoRaWANPackage_t + \brief LoRaWAN Packages structure (for TSxxx documents). +*/ +struct LoRaWANPackage_t { + /*! \brief Package ID as per TS008 */ + uint8_t packId; + + /*! \brief Package default FPort (specified or recommended) */ + uint8_t packFPort; + + /*! \brief Whether the package runs through the Application layer */ + bool isAppPack; + + /*! \brief Whether the FPort value is fixed or may be modified */ + bool fixedFPort; + + /*! \brief Whether the package is currently in use */ + bool enabled; + + /*! \brief User-provided callback for handling package downlinks */ + PackageCb_t callback; +}; + +constexpr LoRaWANPackage_t PackageTable[RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES] = { + { RADIOLIB_LORAWAN_PACKAGE_TS007, RADIOLIB_LORAWAN_FPORT_TS007, true, false, false }, + { RADIOLIB_LORAWAN_PACKAGE_TS003, RADIOLIB_LORAWAN_FPORT_TS003, true, true, false }, + { RADIOLIB_LORAWAN_PACKAGE_TS005, RADIOLIB_LORAWAN_FPORT_TS005, true, true, false }, + { RADIOLIB_LORAWAN_PACKAGE_TS004, RADIOLIB_LORAWAN_FPORT_TS004, true, true, false }, + { RADIOLIB_LORAWAN_PACKAGE_TS006, RADIOLIB_LORAWAN_FPORT_TS006, true, true, false }, + { RADIOLIB_LORAWAN_PACKAGE_TS009, RADIOLIB_LORAWAN_FPORT_TS009, true, false, false }, + { RADIOLIB_LORAWAN_PACKAGE_TS011, RADIOLIB_LORAWAN_FPORT_TS011, false, false, false } +}; + +#define RADIOLIB_LORAWAN_PACKAGE_NONE { .packId = 0, .packFPort = 0, .isAppPack = false, .fixedFPort = false, .enabled = false, .callback = NULL } + #define RADIOLIB_LORAWAN_NONCES_VERSION_VAL (0x0001) enum LoRaWANSchemeBase_t { @@ -886,11 +941,37 @@ class LoRaWANNode { */ void setSleepFunction(SleepCb_t cb); - /*! - \brief TS009 Protocol Specification Verification switch - (allows FPort 224 and cuts off uplink payload instead of rejecting if maximum length exceeded). + /*! + \brief Add a LoRaWAN Application Package as defined in one of the TSxxx documents. + Any downlinks that occur on the corresponding FPort will be redirected to + a supplied callback that implements this package. These downlink contents will be + hidden from the user as the downlink buffer will be empty and the length zero. + The package may need to overrule the behaviour of your device - refer to the examples. + Advanced users only! + \param packageId The ID of the package (one of RADIOLIB_LORAWAN_PACKAGE_TSxxx). + \param callback The downlink handler for this package of type (uint8_t* dataDown, size_t lenDown). + \returns \ref status_codes */ - bool TS009 = false; + int16_t addAppPackage(uint8_t packageId, PackageCb_t callback); + + /*! + \brief Add a LoRaWAN Application Package as defined in one of the TSxxx documents. + Any downlinks that occur on the corresponding FPort will be redirected to + a supplied callback that implements this package. These downlink contents will be + hidden from the user as the downlink buffer will be empty and the length zero. + The package may need to overrule the behaviour of your device - refer to the examples. + Advanced users only! + \param packageId The ID of the package (one of RADIOLIB_LORAWAN_PACKAGE_TSxxx). + \param callback The downlink handler for this package of type (uint8_t* dataDown, size_t lenDown). + \param fPort A custom FPort for packages that have a default FPort < 224. + \returns \ref status_codes + */ + int16_t addAppPackage(uint8_t packageId, PackageCb_t callback, uint8_t fPort); + + /*! + \brief Disable a package that was previously added. + */ + void removePackage(uint8_t packageId); /*! \brief Rx window padding in milliseconds @@ -983,6 +1064,9 @@ class LoRaWANNode { uint32_t mcAFCnt = 0; uint32_t mcAFCntMax = 0; + // enabled TS packages + LoRaWANPackage_t packages[RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES]; + // enable/disable CSMA for LoRaWAN bool csmaEnabled = false; @@ -1032,9 +1116,7 @@ class LoRaWANNode { // save the selected sub-band in case this must be restored in ADR control uint8_t subBand = 0; - // allow port 226 for devices implementing TS011 - bool TS011 = false; - + // user-provided sleep callback SleepCb_t sleepCb = nullptr; // this will reset the device credentials, so the device starts completely new @@ -1079,6 +1161,10 @@ class LoRaWANNode { // extract downlink payload and process MAC commands int16_t parseDownlink(uint8_t* data, size_t* len, uint8_t window, LoRaWANEvent_t* event = NULL); + // add a LoRaWAN package that runs through the network layer + // (not available to users, they are only allowed to add application packages) + int16_t addNwkPackage(uint8_t packageId, PackageCb_t callback); + // execute mac command, return the number of processed bytes for sequential processing bool execMacCommand(uint8_t cid, uint8_t* optIn, uint8_t lenIn); bool execMacCommand(uint8_t cid, uint8_t* optIn, uint8_t lenIn, uint8_t* optOut);