From 574555ca098d5b134da69b615f04669b62f625e0 Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Fri, 5 Jan 2024 11:06:24 +0100 Subject: [PATCH 1/9] [LoRaWAN] Revamp internal processing, key checking, new MAC commands, implement DutyCycle & DwellTime --- .../LoRaWAN_End_Device/LoRaWAN_End_Device.ino | 24 +- .../LoRaWAN_End_Device_ABP.ino | 38 +- .../LoRaWAN_End_Device_Persistent.ino | 47 +- .../LoRaWAN_End_Device_Reference.ino | 57 +- keywords.txt | 9 + src/BuildOpt.h | 2 +- src/Hal.h | 128 +- src/TypeDef.h | 2 +- src/protocols/LoRaWAN/LoRaWAN.cpp | 1279 +++++++++++------ src/protocols/LoRaWAN/LoRaWAN.h | 191 ++- src/protocols/LoRaWAN/LoRaWANBands.cpp | 27 + 11 files changed, 1254 insertions(+), 550 deletions(-) diff --git a/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino b/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino index b5eaaf8c9..c2ea337fc 100644 --- a/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino +++ b/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino @@ -24,11 +24,12 @@ // include the library #include -// SX1278 has the following connections: -// NSS pin: 10 -// DIO0 pin: 2 -// RESET pin: 9 -// DIO1 pin: 3 +// SX1262 has the following pin order: +// Module(NSS/CS, DIO1, RESET, BUSY) +// SX1262 radio = new Module(8, 14, 12, 13); + +// SX1278 has the following pin order: +// Module(NSS/CS, DIO0, RESET, DIO1) SX1278 radio = new Module(10, 2, 9, 3); // create the node instance on the EU-868 band @@ -85,6 +86,11 @@ void setup() { node.selectSubband(8, 15); */ + // on EEPROM-enabled boards, after the device has been activated, + // the session can be restored without rejoining after device power cycle + // this is intrinsically done when calling `beginOTAA()` with the same keys + // in that case, the function will not need to transmit a JoinRequest + // now we can start the activation // this can take up to 10 seconds, and requires a LoRaWAN gateway in range // a specific starting-datarate can be selected in dynamic bands (e.g. EU868): @@ -147,7 +153,11 @@ void loop() { Serial.print(F("failed, code ")); Serial.println(state); } - + // wait before sending another packet - delay(30000); + uint32_t minimumDelay = 60000; // try to send once every minute + uint32_t interval = node.timeUntilUplink(); // calculate minimum duty cycle delay (per law!) + uint32_t delayMs = max(interval, minimumDelay); // cannot send faster than duty cycle allows + + delay(delayMs); } diff --git a/examples/LoRaWAN/LoRaWAN_End_Device_ABP/LoRaWAN_End_Device_ABP.ino b/examples/LoRaWAN/LoRaWAN_End_Device_ABP/LoRaWAN_End_Device_ABP.ino index 05198f5ff..c2bd5ff13 100644 --- a/examples/LoRaWAN/LoRaWAN_End_Device_ABP/LoRaWAN_End_Device_ABP.ino +++ b/examples/LoRaWAN/LoRaWAN_End_Device_ABP/LoRaWAN_End_Device_ABP.ino @@ -25,11 +25,12 @@ // include the library #include -// SX1278 has the following connections: -// NSS pin: 10 -// DIO0 pin: 2 -// RESET pin: 9 -// DIO1 pin: 3 +// SX1262 has the following pin order: +// Module(NSS/CS, DIO1, RESET, BUSY) +// SX1262 radio = new Module(8, 14, 12, 13); + +// SX1278 has the following pin order: +// Module(NSS/CS, DIO0, RESET, DIO1) SX1278 radio = new Module(10, 2, 9, 3); // create the node instance on the EU-868 band @@ -69,6 +70,14 @@ void setup() { uint8_t appSKey[] = { 0x61, 0x44, 0x69, 0x66, 0x66, 0x65, 0x72, 0x65, 0x6E, 0x74, 0x4B, 0x65, 0x79, 0x41, 0x42, 0x43 }; + // network key 2 is the ASCII string "topSecretKey5678" + uint8_t fNwkSIntKey[] = { 0x61, 0x44, 0x69, 0x66, 0x66, 0x65, 0x72, 0x65, + 0x6E, 0x74, 0x4B, 0x65, 0x35, 0x36, 0x37, 0x38 }; + + // network key 3 is the ASCII string "aDifferentKeyDEF" + uint8_t sNwkSIntKey[] = { 0x61, 0x44, 0x69, 0x66, 0x66, 0x65, 0x72, 0x65, + 0x6E, 0x74, 0x4B, 0x65, 0x79, 0x44, 0x45, 0x46 }; + // prior to LoRaWAN 1.1.0, only a single "nwkKey" is used // when connecting to LoRaWAN 1.0 network, "appKey" will be disregarded // and can be set to NULL @@ -86,15 +95,20 @@ void setup() { node.rx2.drMax = 3; */ - // to start a LoRaWAN v1.1 session, the user should also provide - // fNwkSIntKey and sNwkSIntKey similar to nwkSKey and appSKey + // on EEPROM-enabled boards, after the device has been activated, + // the session can be restored without rejoining after device power cycle + // this is intrinsically done when calling `beginABP()` with the same keys + // in that case, the function will not need to transmit a JoinRequest + + // to start a LoRaWAN v1.0 session, + // the user can remove the fNwkSIntKey and sNwkSIntKey /* - state = node.beginABP(devAddr, nwkSKey, appSKey, fNwkSIntKey, sNwkSIntKey); + state = node.beginABP(devAddr, nwkSKey, appSKey); */ // start the device by directly providing the encryption keys and device address Serial.print(F("[LoRaWAN] Attempting over-the-air activation ... ")); - state = node.beginABP(devAddr, nwkSKey, appSKey); + state = node.beginABP(devAddr, nwkSKey, appSKey, fNwkSIntKey, sNwkSIntKey); if(state == RADIOLIB_ERR_NONE) { Serial.println(F("success!")); } else { @@ -149,5 +163,9 @@ void loop() { } // wait before sending another packet - delay(30000); + uint32_t minimumDelay = 60000; // try to send once every minute + uint32_t interval = node.timeUntilUplink(); // calculate minimum duty cycle delay (per law!) + uint32_t delayMs = max(interval, minimumDelay); // cannot send faster than duty cycle allows + + delay(delayMs); } diff --git a/examples/LoRaWAN/LoRaWAN_End_Device_Persistent/LoRaWAN_End_Device_Persistent.ino b/examples/LoRaWAN/LoRaWAN_End_Device_Persistent/LoRaWAN_End_Device_Persistent.ino index 3c96fb913..983a82a08 100644 --- a/examples/LoRaWAN/LoRaWAN_End_Device_Persistent/LoRaWAN_End_Device_Persistent.ino +++ b/examples/LoRaWAN/LoRaWAN_End_Device_Persistent/LoRaWAN_End_Device_Persistent.ino @@ -12,7 +12,7 @@ NOTE: LoRaWAN requires storing some parameters persistently! RadioLib does this by using EEPROM, by default - starting at address 0 and using 384 bytes. + starting at address 0 and using 448 bytes. If you already use EEPROM in your application, you will have to either avoid this range, or change it by setting a different start address by changing the value of @@ -29,11 +29,12 @@ // include the library #include -// SX1278 has the following connections: -// NSS pin: 10 -// DIO0 pin: 2 -// RESET pin: 9 -// DIO1 pin: 3 +// SX1262 has the following pin order: +// Module(NSS/CS, DIO1, RESET, BUSY) +// SX1262 radio = new Module(8, 14, 12, 13); + +// SX1278 has the following pin order: +// Module(NSS/CS, DIO0, RESET, DIO1) SX1278 radio = new Module(10, 2, 9, 3); // create the node instance on the EU-868 band @@ -56,33 +57,31 @@ void setup() { while(true); } - // first we need to initialize the device storage - // this will reset all persistently stored parameters - // NOTE: This should only be done once prior to first joining a network! - // After wiping persistent storage, you will also have to reset - // the end device in TTN and perform the join procedure again! - // Here, a delay is added to make sure that during re-flashing - // the .wipe() is not triggered and the session is lost - //delay(5000); - //node.wipe(); - - // now we can start the activation + // start the activation // Serial.print(F("[LoRaWAN] Attempting over-the-air activation ... ")); // uint64_t joinEUI = 0x12AD1011B0C0FFEE; - // uint64_t devEUI = 0x70B3D57ED005E120; + // uint64_t devEUI = 0x70B3D57ED005E120; // uint8_t nwkKey[] = { 0x74, 0x6F, 0x70, 0x53, 0x65, 0x63, 0x72, 0x65, // 0x74, 0x4B, 0x65, 0x79, 0x31, 0x32, 0x33, 0x34 }; // uint8_t appKey[] = { 0x61, 0x44, 0x69, 0x66, 0x66, 0x65, 0x72, 0x65, // 0x6E, 0x74, 0x4B, 0x65, 0x79, 0x41, 0x42, 0x43 }; // state = node.beginOTAA(joinEUI, devEUI, nwkKey, appKey); - // after the device has been activated, + // on EEPROM-enabled boards, after the device has been activated, // the session can be restored without rejoining after device power cycle - // on EEPROM-enabled boards by calling "restore" + // by calling the same `beginOTAA()` or `beginABP()` function with the same keys + // or call `restore()` where it will restore any existing session + // `restore()` returns the active mode if it succeeded (OTAA or ABP) Serial.print(F("[LoRaWAN] Resuming previous session ... ")); state = node.restore(); - if(state == RADIOLIB_ERR_NONE) { + if(state >= RADIOLIB_ERR_NONE) { Serial.println(F("success!")); + Serial.print(F("Restored an ")); + if(state == RADIOLIB_LORAWAN_MODE_OTAA) + Serial.println(F("OTAA session.")); + else { + Serial.println(F("ABP session.")); + } } else { Serial.print(F("failed, code ")); Serial.println(state); @@ -141,5 +140,9 @@ void loop() { // wait before sending another packet // alternatively, call a deepsleep function here // make sure to send the radio to sleep as well using radio.sleep() - delay(30000); + uint32_t minimumDelay = 60000; // try to send once every minute + uint32_t interval = node.timeUntilUplink(); // calculate minimum duty cycle delay (per law!) + uint32_t delayMs = max(interval, minimumDelay); // cannot send faster than duty cycle allows + + delay(delayMs); } diff --git a/examples/LoRaWAN/LoRaWAN_End_Device_Reference/LoRaWAN_End_Device_Reference.ino b/examples/LoRaWAN/LoRaWAN_End_Device_Reference/LoRaWAN_End_Device_Reference.ino index bf70f1395..52d5904f2 100644 --- a/examples/LoRaWAN/LoRaWAN_End_Device_Reference/LoRaWAN_End_Device_Reference.ino +++ b/examples/LoRaWAN/LoRaWAN_End_Device_Reference/LoRaWAN_End_Device_Reference.ino @@ -27,11 +27,12 @@ // include the library #include -// SX1278 has the following connections: -// NSS pin: 10 -// DIO0 pin: 2 -// RESET pin: 9 -// DIO1 pin: 3 +// SX1262 has the following pin order: +// Module(NSS/CS, DIO1, RESET, BUSY) +// SX1262 radio = new Module(8, 14, 12, 13); + +// SX1278 has the following pin order: +// Module(NSS/CS, DIO0, RESET, DIO1) SX1278 radio = new Module(10, 2, 9, 3); // create the node instance on the EU-868 band @@ -106,9 +107,11 @@ void setup() { while(true); } - // after the device has been activated, + // on EEPROM-enabled boards, after the device has been activated, // the session can be restored without rejoining after device power cycle - // on EEPROM-enabled boards by calling "restore" + // this is intrinsically done when calling `beginOTAA()` with the same keys + // or if you 'lost' the keys or don't want them included in your sketch + // you can call `restore()` /* Serial.print(F("[LoRaWAN] Resuming previous session ... ")); state = node.restore(); @@ -131,6 +134,19 @@ void setup() { // this tries to minimize packet loss by searching for a free channel // before actually sending an uplink node.setCSMA(6, 2, true); + + // enable or disable the dutycycle + // the second argument specific allowed airtime per hour in milliseconds + // 1250 = TTN FUP (30 seconds / 24 hours) + // if not called, this corresponds to setDutyCycle(true, 0) + // setting this to 0 corresponds to the band's maximum allowed dutycycle by law + node.setDutyCycle(true, 1250); + + // enable or disable the dwell time limits + // the second argument specific allowed airtime per uplink in milliseconds + // if not called, this corresponds to setDwellTime(true, 0) + // setting this to 0 corresponds to the band's maximum allowed dwell time by law + node.setDwellTime(true, 1000); } void loop() { @@ -152,8 +168,11 @@ void loop() { String strUp = "Hello World! #" + String(fcntUp); // send a confirmed uplink to port 10 every 64th frame + // and also request the LinkCheck and DeviceTime MAC commands if(fcntUp % 64 == 0) { state = node.uplink(strUp, 10, true); + node.sendMacCommandReq(RADIOLIB_LORAWAN_MAC_LINK_CHECK); + node.sendMacCommandReq(RADIOLIB_LORAWAN_MAC_DEVICE_TIME); } else { state = node.uplink(strUp, 10); } @@ -228,6 +247,24 @@ void loop() { Serial.println(event.port); Serial.print(radio.getFrequencyError()); + + uint8_t margin = 0; + uint8_t gwCnt = 0; + if(node.getMacLinkCheckAns(&margin, &gwCnt)) { + 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)) { + Serial.print(F("[LoRaWAN] DeviceTime Unix:\t")); + Serial.println(networkTime); + Serial.print(F("[LoRaWAN] LinkCheck second:\t1/")); + Serial.println(fracSecond); + } } else if(state == RADIOLIB_ERR_RX_TIMEOUT) { Serial.println(F("timeout!")); @@ -244,5 +281,9 @@ void loop() { */ // wait before sending another packet - delay(30000); + uint32_t minimumDelay = 60000; // try to send once every minute + uint32_t interval = node.timeUntilUplink(); // calculate minimum duty cycle delay (per law!) + uint32_t delayMs = max(interval, minimumDelay); // cannot send faster than duty cycle allows + + delay(delayMs); } diff --git a/keywords.txt b/keywords.txt index 065991a09..3f2da44a5 100644 --- a/keywords.txt +++ b/keywords.txt @@ -295,6 +295,7 @@ restore KEYWORD2 beginOTAA KEYWORD2 beginABP KEYWORD2 saveSession KEYWORD2 +sendMacCommandReq KEYWORD2 uplink KEYWORD2 downlink KEYWORD2 sendReceive KEYWORD2 @@ -302,11 +303,19 @@ setDeviceStatus KEYWORD2 getFcntUp KEYWORD2 getNFcntDown KEYWORD2 getAFcntDown KEYWORD2 +resetFcntDown KEYWORD2 setDatarate KEYWORD2 setADR KEYWORD2 +setDutyCycle KEYWORD2 +dutyCycleInterval KEYWORD2 +timeUntilUplink KEYWORD2 +setDwellTime KEYWORD2 +maxPayloadDwellTime KEYWORD2 setTxPower KEYWORD2 selectSubband KEYWORD2 setCSMA KEYWORD2 +getMacLinkCheckAns KEYWORD2 +getMacDeviceTimeAns KEYWORD2 ####################################### # Constants (LITERAL1) diff --git a/src/BuildOpt.h b/src/BuildOpt.h index 308dc8969..a95b6cdd1 100644 --- a/src/BuildOpt.h +++ b/src/BuildOpt.h @@ -112,7 +112,7 @@ // the amount of space allocated to the persistent storage #if !defined(RADIOLIB_HAL_PERSISTENT_STORAGE_SIZE) - #define RADIOLIB_HAL_PERSISTENT_STORAGE_SIZE (0x0180) + #define RADIOLIB_HAL_PERSISTENT_STORAGE_SIZE (0x01C0) #endif /* diff --git a/src/Hal.h b/src/Hal.h index 780c34831..20d858ff7 100644 --- a/src/Hal.h +++ b/src/Hal.h @@ -6,60 +6,86 @@ #include "BuildOpt.h" +#define RADIOLIB_EEPROM_TABLE_VERSION (0x0002) + // list of persistent parameters -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_TABLE_VERSION_ID (0) // this is NOT the LoRaWAN version, but version of this table -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID (1) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION_ID (2) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXDR_RX2DR_ID (3) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXPWR_CUR_ID (4) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_RX1_DROFF_DEL_ID (5) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_RX2FREQ_ID (6) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_LIM_DEL_ID (7) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_NBTRANS_ID (8) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_ADDR_ID (9) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_APP_S_KEY_ID (10) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_FNWK_SINT_KEY_ID (11) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_SNWK_SINT_KEY_ID (12) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_NWK_SENC_KEY_ID (13) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_HOME_NET_ID (14) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_NONCE_ID (15) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_JOIN_NONCE_ID (16) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_A_FCNT_DOWN_ID (17) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_N_FCNT_DOWN_ID (18) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_CONF_FCNT_UP_ID (19) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_CONF_FCNT_DOWN_ID (20) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_FCNT_ID (21) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID (22) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_FOPTS_ID (23) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_FREQS_ID (24) +enum RADIOLIB_EEPROM_PARAMS { + RADIOLIB_EEPROM_TABLE_VERSION_ID, // table layout version + RADIOLIB_EEPROM_LORAWAN_CLASS_ID, // class A, B or C + RADIOLIB_EEPROM_LORAWAN_MODE_ID, // none, OTAA or ABP + RADIOLIB_EEPROM_LORAWAN_CHECKSUM_ID, // checksum of keys used for device activation + RADIOLIB_EEPROM_LORAWAN_VERSION_ID, // LoRaWAN version + RADIOLIB_EEPROM_LORAWAN_LAST_TIME_ID, // last heard time through DeviceTimeReq or Beacon + RADIOLIB_EEPROM_LORAWAN_DEV_ADDR_ID, + RADIOLIB_EEPROM_LORAWAN_APP_S_KEY_ID, + RADIOLIB_EEPROM_LORAWAN_FNWK_SINT_KEY_ID, + RADIOLIB_EEPROM_LORAWAN_SNWK_SINT_KEY_ID, + RADIOLIB_EEPROM_LORAWAN_NWK_SENC_KEY_ID, + RADIOLIB_EEPROM_LORAWAN_DEV_NONCE_ID, + RADIOLIB_EEPROM_LORAWAN_JOIN_NONCE_ID, + RADIOLIB_EEPROM_LORAWAN_HOME_NET_ID, + RADIOLIB_EEPROM_LORAWAN_A_FCNT_DOWN_ID, + RADIOLIB_EEPROM_LORAWAN_N_FCNT_DOWN_ID, + RADIOLIB_EEPROM_LORAWAN_CONF_FCNT_UP_ID, + RADIOLIB_EEPROM_LORAWAN_CONF_FCNT_DOWN_ID, + RADIOLIB_EEPROM_LORAWAN_ADR_FCNT_ID, + RADIOLIB_EEPROM_LORAWAN_RJ_COUNT0_ID, + RADIOLIB_EEPROM_LORAWAN_RJ_COUNT1_ID, + RADIOLIB_EEPROM_LORAWAN_FCNT_UP_ID, + RADIOLIB_EEPROM_LORAWAN_LINK_ADR_ID, + RADIOLIB_EEPROM_LORAWAN_DUTY_CYCLE_ID, + RADIOLIB_EEPROM_LORAWAN_RX_PARAM_SETUP_ID, + RADIOLIB_EEPROM_LORAWAN_RX_TIMING_SETUP_ID, + RADIOLIB_EEPROM_LORAWAN_TX_PARAM_SETUP_ID, + RADIOLIB_EEPROM_LORAWAN_ADR_PARAM_SETUP_ID, + RADIOLIB_EEPROM_LORAWAN_REJOIN_PARAM_SETUP_ID, + RADIOLIB_EEPROM_LORAWAN_BEACON_FREQ_ID, + RADIOLIB_EEPROM_LORAWAN_PING_SLOT_CHANNEL_ID, + RADIOLIB_EEPROM_LORAWAN_PERIODICITY_ID, + RADIOLIB_EEPROM_LORAWAN_NUM_ADR_MASKS_ID, + RADIOLIB_EEPROM_LORAWAN_MAC_QUEUE_UL_ID, + RADIOLIB_EEPROM_LORAWAN_UL_CHANNELS_ID, + RADIOLIB_EEPROM_LORAWAN_DL_CHANNELS_ID +}; static const uint32_t RadioLibPersistentParamTable[] = { - 0x00, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_TABLE_VERSION_ID - 0x01, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID - 0x03, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION - 0x04, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXDR_RX2DR_ID - 0x05, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXPWR_CUR_ID - 0x06, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_RX1_DROFF_DEL_ID - 0x07, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_RX2FREQ_ID - 0x0A, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_LIM_DEL_ID - 0x0B, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_NBTRANS_ID - 0x0C, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_ADDR_ID - 0x10, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_APP_S_KEY_ID - 0x20, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_FNWK_SINT_KEY_ID - 0x30, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_SNWK_SINT_KEY_ID - 0x40, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_NWK_SENC_KEY_ID - 0x50, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_HOME_NET_ID - 0x54, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_NONCE_ID - 0x58, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_JOIN_NONCE_ID - 0x5C, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_A_FCNT_DOWN_ID - 0x60, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_N_FCNT_DOWN_ID - 0x64, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_CONF_FCNT_UP_ID - 0x68, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_CONF_FCNT_DOWN_ID - 0x6C, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_FCNT_ID - 0x70, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID - 0x8E, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_FOPTS_ID - 0xD0, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_FREQS_ID - 0x0180, // end + 0x00, // RADIOLIB_EEPROM_LORAWAN_TABLE_VERSION_ID + 0x02, // RADIOLIB_EEPROM_LORAWAN_CLASS_ID + 0x03, // RADIOLIB_EEPROM_LORAWAN_MODE_ID + 0x05, // RADIOLIB_EEPROM_LORAWAN_CHECKSUM_ID + 0x07, // RADIOLIB_EEPROM_LORAWAN_VERSION_ID + 0x08, // RADIOLIB_EEPROM_LORAWAN_LAST_TIME_ID + 0x0C, // RADIOLIB_EEPROM_LORAWAN_DEV_ADDR_ID + 0x10, // RADIOLIB_EEPROM_LORAWAN_APP_S_KEY_ID + 0x20, // RADIOLIB_EEPROM_LORAWAN_FNWK_SINT_KEY_ID + 0x30, // RADIOLIB_EEPROM_LORAWAN_SNWK_SINT_KEY_ID + 0x40, // RADIOLIB_EEPROM_LORAWAN_NWK_SENC_KEY_ID + 0x50, // RADIOLIB_EEPROM_LORAWAN_DEV_NONCE_ID + 0x54, // RADIOLIB_EEPROM_LORAWAN_JOIN_NONCE_ID + 0x58, // RADIOLIB_EEPROM_LORAWAN_HOME_NET_ID + 0x5C, // RADIOLIB_EEPROM_LORAWAN_A_FCNT_DOWN_ID + 0x60, // RADIOLIB_EEPROM_LORAWAN_N_FCNT_DOWN_ID + 0x64, // RADIOLIB_EEPROM_LORAWAN_CONF_FCNT_UP_ID + 0x68, // RADIOLIB_EEPROM_LORAWAN_CONF_FCNT_DOWN_ID + 0x6C, // RADIOLIB_EEPROM_LORAWAN_ADR_FCNT_ID + 0x70, // RADIOLIB_EEPROM_LORAWAN_RJ_COUNT0_ID + 0x72, // RADIOLIB_EEPROM_LORAWAN_RJ_COUNT1_ID + 0x74, // RADIOLIB_EEPROM_LORAWAN_FCNT_UP_ID + 0xA0, // RADIOLIB_EEPROM_LORAWAN_LINK_ADR_ID - NOTE change upon ADR Ack Req + 0xA4, // RADIOLIB_EEPROM_LORAWAN_DUTY_CYCLE_ID + 0xA5, // RADIOLIB_EEPROM_LORAWAN_RX_PARAM_SETUP_ID + 0xA9, // RADIOLIB_EEPROM_LORAWAN_RX_TIMING_SETUP_ID + 0xAA, // RADIOLIB_EEPROM_LORAWAN_TX_PARAM_SETUP_ID + 0xAB, // RADIOLIB_EEPROM_LORAWAN_ADR_PARAM_SETUP_ID + 0xAC, // RADIOLIB_EEPROM_LORAWAN_REJOIN_PARAM_SETUP_ID + 0xAD, // RADIOLIB_EEPROM_LORAWAN_BEACON_FREQ_ID + 0xB0, // RADIOLIB_EEPROM_LORAWAN_PING_SLOT_CHANNEL_ID + 0xB4, // RADIOLIB_EEPROM_LORAWAN_PERIODICITY_ID + 0xB5, // RADIOLIB_EEPROM_LORAWAN_NUM_ADR_MASKS_ID + 0xB6, // RADIOLIB_EEPROM_LORAWAN_MAC_QUEUE_UL_ID + 0x0100, // RADIOLIB_EEPROM_LORAWAN_UL_CHANNELS_ID + 0x0180, // RADIOLIB_EEPROM_LORAWAN_DL_CHANNELS_ID + 0x01C0, // end }; /*! diff --git a/src/TypeDef.h b/src/TypeDef.h index 2e16145c0..64d1a1a29 100644 --- a/src/TypeDef.h +++ b/src/TypeDef.h @@ -519,7 +519,7 @@ #define RADIOLIB_ERR_INVALID_CID (-1107) /*! - \brief User requested to start uplink while still inside RX window. + \brief User requested to start uplink while still inside RX window or under dutycycle. */ #define RADIOLIB_ERR_UPLINK_UNAVAILABLE (-1108) diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index 89117a4c8..e7217f4e9 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -28,10 +28,57 @@ uint8_t getDownlinkDataRate(uint8_t uplink, uint8_t offset, uint8_t base, uint8_ return(dr); } +uint16_t checkSum16(uint32_t key) { + uint8_t bufLen = 2; + uint16_t buf16[bufLen]; + memcpy(buf16, &key, bufLen); + uint16_t checkSum = 0; + for(int i = 0; i < bufLen; i++) { + checkSum ^= buf16[i]; + } + return(checkSum); +} + +uint16_t checkSum16(uint64_t key) { + uint8_t bufLen = 4; + uint16_t buf16[bufLen]; + memcpy(buf16, &key, bufLen); + uint16_t checkSum = 0; + for(int i = 0; i < bufLen; i++) { + checkSum ^= buf16[i]; + } + return(checkSum); +} + +uint16_t checkSum16(uint8_t *key, uint8_t keyLen) { + uint8_t bufLen = keyLen / 2; + uint16_t buf16[bufLen]; + memcpy(buf16, key, bufLen); + uint16_t checkSum = 0; + for(int i = 0; i < bufLen; i++) { + checkSum ^= buf16[i]; + } + return(checkSum); +} + LoRaWANNode::LoRaWANNode(PhysicalLayer* phy, const LoRaWANBand_t* band) { this->phyLayer = phy; this->band = band; this->rx2 = this->band->rx2; + this->dutyCycle = this->band->dutyCycle; + if(this->dutyCycle > 0) { + this->dutyCycleEnabled = true; + } + this->dwellTimeUp = this->band->dwellTimeUp; + if(this->dwellTimeUp > 0) { + this->dwellTimeEnabledUp = true; + } + this->dwellTimeDn = this->band->dwellTimeDn; + if(this->dwellTimeDn) { + this->dwellTimeEnabledDn = true; + } + this->txPowerMax = this->band->powerMax; + this->txPowerCur = 0; // start at 0 offset = full power this->difsSlots = 2; this->backoffMax = 6; this->enableCSMA = false; @@ -49,22 +96,24 @@ void LoRaWANNode::wipe() { mod->hal->wipePersistentStorage(); } +// TODO do not return status code, but return LoRaWAN mode (OTAA, ABP, none) int16_t LoRaWANNode::restore() { // if already joined, ignore - if(this->isJoinedFlag) { - return(RADIOLIB_ERR_NONE); + if(this->activeMode != RADIOLIB_LORAWAN_MODE_NONE) { + return(this->activeMode); } Module* mod = this->phyLayer->getMod(); - uint8_t nvm_table_version = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TABLE_VERSION_ID); - // if (RADIOLIB_PERSISTENT_PARAM_LORAWAN_TABLE_VERSION > nvm_table_version) { + uint8_t nvm_table_version = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_TABLE_VERSION_ID); + // if (RADIOLIB_EEPROM_LORAWAN_TABLE_VERSION > nvm_table_version) { // // set default values for variables that are new or something // } (void)nvm_table_version; // check the magic value - if(mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID) != RADIOLIB_LORAWAN_MAGIC) { + uint8_t lwMode = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_MODE_ID); + if(lwMode == RADIOLIB_LORAWAN_MODE_NONE) { #if RADIOLIB_DEBUG RADIOLIB_DEBUG_PRINTLN("magic id not set (no saved session)"); RADIOLIB_DEBUG_PRINTLN("first 16 bytes of NVM:"); @@ -75,85 +124,95 @@ int16_t LoRaWANNode::restore() { // the magic value is not set, user will have to do perform the join procedure return(RADIOLIB_ERR_NETWORK_NOT_JOINED); } - + // pull all authentication keys from persistent storage - this->devAddr = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_ADDR_ID); - mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_APP_S_KEY_ID), this->appSKey, RADIOLIB_AES128_BLOCK_SIZE); - mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FNWK_SINT_KEY_ID), this->fNwkSIntKey, RADIOLIB_AES128_BLOCK_SIZE); - mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_SNWK_SINT_KEY_ID), this->sNwkSIntKey, RADIOLIB_AES128_BLOCK_SIZE); - mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_NWK_SENC_KEY_ID), this->nwkSEncKey, RADIOLIB_AES128_BLOCK_SIZE); + this->devAddr = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_DEV_ADDR_ID); + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_APP_S_KEY_ID), this->appSKey, RADIOLIB_AES128_BLOCK_SIZE); + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_FNWK_SINT_KEY_ID), this->fNwkSIntKey, RADIOLIB_AES128_BLOCK_SIZE); + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_SNWK_SINT_KEY_ID), this->sNwkSIntKey, RADIOLIB_AES128_BLOCK_SIZE); + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_NWK_SENC_KEY_ID), this->nwkSEncKey, RADIOLIB_AES128_BLOCK_SIZE); // get session parameters - this->rev = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION_ID); + this->rev = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_VERSION_ID); RADIOLIB_DEBUG_PRINTLN("LoRaWAN session: v1.%d", this->rev); - this->devNonce = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_NONCE_ID); - this->joinNonce = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_JOIN_NONCE_ID); - - // get MAC state - uint8_t txDrRx2Dr = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXDR_RX2DR_ID); - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = (txDrRx2Dr >> 4) & 0x0F; - - this->txPwrCur = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXPWR_CUR_ID); - - uint8_t rx1DrOffDel = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_RX1_DROFF_DEL_ID); - this->rx1DrOffset = (rx1DrOffDel >> 4) & 0x0F; - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] + this->band->rx1DataRateBase + this->rx1DrOffset; - this->rxDelays[0] = ((rx1DrOffDel >> 0) & 0x0F) * 1000; - if(this->rxDelays[0] == 0) { - this->rxDelays[0] = RADIOLIB_LORAWAN_RECEIVE_DELAY_1_MS; - } - this->rxDelays[1] = this->rxDelays[0] + 1000; - - uint8_t rx2FreqBuf[3]; - mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_RX2FREQ_ID), rx2FreqBuf, 3); - uint32_t rx2Freq = LoRaWANNode::ntoh(&rx2FreqBuf[0], 3); - this->rx2.drMax = (txDrRx2Dr >> 0) & 0x0F; - this->rx2.freq = (float)rx2Freq / 10000.0; + this->devNonce = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_DEV_NONCE_ID); + this->joinNonce = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_JOIN_NONCE_ID); + this->aFcntDown = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_A_FCNT_DOWN_ID); + this->nFcntDown = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_N_FCNT_DOWN_ID); + this->confFcntUp = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_CONF_FCNT_UP_ID); + this->confFcntDown = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_CONF_FCNT_DOWN_ID); + this->adrFcnt = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_ADR_FCNT_ID); - uint8_t adrLimDel = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_LIM_DEL_ID); - this->adrLimitExp = (adrLimDel >> 4) & 0x0F; - this->adrDelayExp = (adrLimDel >> 0) & 0x0F; - - this->nbTrans = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_NBTRANS_ID); - - this->aFcntDown = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_A_FCNT_DOWN_ID); - this->nFcntDown = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_N_FCNT_DOWN_ID); - this->confFcntUp = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_CONF_FCNT_UP_ID); - this->confFcntDown = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_CONF_FCNT_DOWN_ID); - this->adrFcnt = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_FCNT_ID); - - // fcntUp is stored in highly efficient wear-leveling system, so parse it as required + // fcntUp is stored in highly efficient wear-leveling system, so parse it this->restoreFcntUp(); + // get the defined channels + int16_t state = this->restoreChannels(); + RADIOLIB_ASSERT(state); + + // get MAC state + LoRaWANMacCommand_t cmd; + cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_LINK_ADR_ID), cmd.payload, cmd.len); + execMacCommand(&cmd, false); + + cmd.cid = RADIOLIB_LORAWAN_MAC_DUTY_CYCLE; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_DUTY_CYCLE].lenDn; + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_DUTY_CYCLE_ID), cmd.payload, cmd.len); + execMacCommand(&cmd, false); + + cmd.cid = RADIOLIB_LORAWAN_MAC_RX_PARAM_SETUP; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_RX_PARAM_SETUP].lenDn; + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_RX_PARAM_SETUP_ID), cmd.payload, cmd.len); + execMacCommand(&cmd, false); + + cmd.cid = RADIOLIB_LORAWAN_MAC_RX_TIMING_SETUP; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_RX_TIMING_SETUP].lenDn; + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_RX_TIMING_SETUP_ID), cmd.payload, cmd.len); + execMacCommand(&cmd, false); + + cmd.cid = RADIOLIB_LORAWAN_MAC_TX_PARAM_SETUP; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_TX_PARAM_SETUP].lenDn; + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_TX_PARAM_SETUP_ID), cmd.payload, cmd.len); + execMacCommand(&cmd, false); + + cmd.cid = RADIOLIB_LORAWAN_MAC_ADR_PARAM_SETUP; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_ADR_PARAM_SETUP].lenDn; + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_ADR_PARAM_SETUP_ID), cmd.payload, cmd.len); + execMacCommand(&cmd, false); + + cmd.cid = RADIOLIB_LORAWAN_MAC_REJOIN_PARAM_SETUP; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_REJOIN_PARAM_SETUP].lenDn; + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_REJOIN_PARAM_SETUP_ID), cmd.payload, cmd.len); + execMacCommand(&cmd, false); + uint8_t queueBuff[sizeof(LoRaWANMacCommandQueue_t)] = { 0 }; - mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FOPTS_ID), queueBuff, sizeof(LoRaWANMacCommandQueue_t)); + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_MAC_QUEUE_UL_ID), queueBuff, sizeof(LoRaWANMacCommandQueue_t)); memcpy(&this->commandsUp, queueBuff, sizeof(LoRaWANMacCommandQueue_t)); RADIOLIB_DEBUG_PRINTLN("Number of MAC commands: %d", this->commandsUp.numCommands); - - int16_t state = this->restoreChannels(); - RADIOLIB_ASSERT(state); state = this->setPhyProperties(); RADIOLIB_ASSERT(state); - // full session is restored, so set joined flag - this->isJoinedFlag = true; + // full session is restored, so set joined flag to whichever mode is restored + this->activeMode = lwMode; - return(RADIOLIB_ERR_NONE); + return(this->activeMode); } int16_t LoRaWANNode::restoreFcntUp() { Module* mod = this->phyLayer->getMod(); - uint8_t fcntBuffStart = mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID); - uint8_t fcntBuffEnd = mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID + 1); + uint8_t fcntBuffStart = mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_FCNT_UP_ID); + uint8_t fcntBuffEnd = mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_FCNT_UP_ID + 1); uint8_t buffSize = fcntBuffEnd - fcntBuffStart; #if RADIOLIB_STATIC_ONLY uint8_t fcntBuff[RADIOLIB_STATIC_ARRAY_SIZE]; #else uint8_t* fcntBuff = new uint8_t[buffSize]; #endif - mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID), fcntBuff, buffSize); + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_FCNT_UP_ID), fcntBuff, buffSize); // copy the two most significant bytes from the first two bytes uint32_t bits_30_22 = (uint32_t)fcntBuff[0]; @@ -191,50 +250,193 @@ int16_t LoRaWANNode::restoreFcntUp() { } int16_t LoRaWANNode::restoreChannels() { - const uint8_t bytesPerChannel = 5; - const uint8_t numBytes = 2 * RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS * bytesPerChannel; - uint8_t buffer[numBytes] = { 0 }; + // first do the default channels + this->setupChannels(nullptr); + Module* mod = this->phyLayer->getMod(); - mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FREQS_ID), buffer, numBytes); - for(uint8_t dir = 0; dir < 2; dir++) { - for(uint8_t i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { - uint8_t chBuff[bytesPerChannel] = { 0 }; - memcpy(chBuff, &buffer[(dir * RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS * bytesPerChannel) + i * bytesPerChannel], bytesPerChannel); - this->availableChannels[dir][i].enabled = (chBuff[0] & 0x80) >> 7; - this->availableChannels[dir][i].idx = chBuff[0] & 0x7F; - uint32_t freq = LoRaWANNode::ntoh(&chBuff[1], 3); - this->availableChannels[dir][i].freq = (float)freq/10000.0; - this->availableChannels[dir][i].drMax = (chBuff[4] & 0xF0) >> 4; - this->availableChannels[dir][i].drMin = (chBuff[4] & 0x0F) >> 0; + uint8_t bufferZeroes[5] = { 0 }; + if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) { + uint8_t numBytesUp = RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS * MacTable[RADIOLIB_LORAWAN_MAC_NEW_CHANNEL].lenDn; + uint8_t bufferUp[numBytesUp] = { 0 }; + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_UL_CHANNELS_ID), bufferUp, numBytesUp); + + LoRaWANMacCommand_t cmd; + cmd.cid = RADIOLIB_LORAWAN_MAC_NEW_CHANNEL; + + for(int i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_NEW_CHANNEL].lenDn; + memcpy(cmd.payload, &(bufferUp[i * cmd.len]), cmd.len); + if(memcmp(cmd.payload, bufferZeroes, cmd.len) != 0) { // only execute if it is not all zeroes + (void)execMacCommand(&cmd, false); + } + } + + uint8_t numBytesDn = RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS * MacTable[RADIOLIB_LORAWAN_MAC_DL_CHANNEL].lenDn; + uint8_t bufferDn[numBytesDn] = { 0 }; + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_DL_CHANNELS_ID), bufferDn, numBytesDn); + + cmd.cid = RADIOLIB_LORAWAN_MAC_DL_CHANNEL; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_DL_CHANNEL].lenDn; + + for(int i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { + memcpy(cmd.payload, &bufferDn[i * cmd.len], cmd.len); + if(memcmp(cmd.payload, bufferZeroes, cmd.len) != 0) { // only execute if it is not all zeroes + (void)execMacCommand(&cmd, false); + } + } + + } else { + uint8_t numBytes = 8 * MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; + uint8_t buffer[numBytes] = { 0 }; + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_UL_CHANNELS_ID), buffer, numBytes); + + LoRaWANMacCommand_t cmd; + cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; + + for(int i = 0; i < 8; i++) { + memcpy(cmd.payload, &buffer[i * cmd.len], cmd.len); + // there COULD, according to spec, be an all zeroes ADR command - meh + if(memcmp(cmd.payload, bufferZeroes, cmd.len) != 0) { + execMacCommand(&cmd, false); + } } } return(RADIOLIB_ERR_NONE); } #endif +void LoRaWANNode::beginCommon() { + LoRaWANMacCommand_t cmd; + cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; + cmd.payload[0] = (this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] << 4); + cmd.payload[0] |= 0; // default to max Tx Power + cmd.payload[3] |= (1 << 7); // set the RFU bit, which means that the channel mask gets ignored + (void)execMacCommand(&cmd); + + cmd.cid = RADIOLIB_LORAWAN_MAC_DUTY_CYCLE; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_DUTY_CYCLE].lenDn; + uint8_t maxDCyclePower; + switch(this->band->dutyCycle) { + case(0): + maxDCyclePower = 0; + break; + case(3600): + maxDCyclePower = 10; + break; + case(36000): + maxDCyclePower = 7; + break; + default: + maxDCyclePower = 0; + break; + } + cmd.payload[0] = maxDCyclePower; + (void)execMacCommand(&cmd); + + cmd.cid = RADIOLIB_LORAWAN_MAC_RX_PARAM_SETUP; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_RX_PARAM_SETUP].lenDn; + cmd.payload[0] = (RADIOLIB_LORAWAN_RX1_DR_OFFSET << 4); + cmd.payload[0] |= this->rx2.drMax; // may be set by user, otherwise band's default upon initialization + uint32_t rx2Freq = uint32_t(this->rx2.freq * 10000); + LoRaWANNode::hton(&cmd.payload[1], rx2Freq, 3); + (void)execMacCommand(&cmd); + + cmd.cid = RADIOLIB_LORAWAN_MAC_RX_TIMING_SETUP; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_RX_TIMING_SETUP].lenDn; + cmd.payload[0] = (RADIOLIB_LORAWAN_RECEIVE_DELAY_1_MS / 1000); + (void)execMacCommand(&cmd); + + cmd.cid = RADIOLIB_LORAWAN_MAC_TX_PARAM_SETUP; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_TX_PARAM_SETUP].lenDn; + cmd.payload[0] = (this->band->dwellTimeDn > 0 ? 1 : 0) << 5; + cmd.payload[0] |= (this->band->dwellTimeUp > 0 ? 1 : 0) << 4; + uint8_t maxEIRPRaw; + switch(this->band->powerMax) { + case(12): + maxEIRPRaw = 2; + break; + case(14): + maxEIRPRaw = 4; + break; + case(16): + maxEIRPRaw = 5; + break; + case(19): // this option does not exist for the TxParamSetupReq but will be caught during execution + maxEIRPRaw = 7; + break; + case(30): + maxEIRPRaw = 13; + break; + default: + maxEIRPRaw = 2; + break; + } + cmd.payload[0] |= maxEIRPRaw; + (void)execMacCommand(&cmd); + + cmd.cid = RADIOLIB_LORAWAN_MAC_ADR_PARAM_SETUP; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_ADR_PARAM_SETUP].lenDn; + cmd.payload[0] = (RADIOLIB_LORAWAN_ADR_ACK_LIMIT_EXP << 4); + cmd.payload[0] |= RADIOLIB_LORAWAN_ADR_ACK_DELAY_EXP; + (void)execMacCommand(&cmd); + + cmd.cid = RADIOLIB_LORAWAN_MAC_REJOIN_PARAM_SETUP; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_REJOIN_PARAM_SETUP].lenDn; + cmd.payload[0] = (RADIOLIB_LORAWAN_REJOIN_MAX_TIME_N << 4); + cmd.payload[0] |= RADIOLIB_LORAWAN_REJOIN_MAX_COUNT_N; + (void)execMacCommand(&cmd); +} + int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKey, uint8_t* appKey, uint8_t joinDr, bool force) { // check if we actually need to send the join request Module* mod = this->phyLayer->getMod(); #if !defined(RADIOLIB_EEPROM_UNSUPPORTED) - if(!force && (mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID) == RADIOLIB_LORAWAN_MAGIC)) { + uint16_t checkSum = 0; + checkSum ^= checkSum16(joinEUI); + checkSum ^= checkSum16(devEUI); + checkSum ^= checkSum16(nwkKey, 16); + checkSum ^= checkSum16(appKey, 16); + + bool validCheckSum = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_CHECKSUM_ID) == checkSum; + bool validMode = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_MODE_ID) == RADIOLIB_LORAWAN_MODE_OTAA; + + if(!force && validCheckSum && validMode) { // the device has joined already, we can just pull the data from persistent storage RADIOLIB_DEBUG_PRINTLN("Found existing session; restoring..."); + return(this->restore()); + } else { + #if RADIOLIB_DEBUG + RADIOLIB_DEBUG_PRINTLN("Failed to restore session (checksum: %d, mode: %d)", validCheckSum, validMode); + RADIOLIB_DEBUG_PRINTLN("First 16 bytes of NVM:"); + uint8_t nvmBuff[16]; + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(0), nvmBuff, 16); + RADIOLIB_DEBUG_HEXDUMP(nvmBuff, 16); + RADIOLIB_DEBUG_PRINTLN("Wiping EEPROM and starting a clean session"); + #endif + + this->wipe(); } #else (void)force; #endif - // set the physical layer configuration - this->txPwrCur = this->band->powerMax; - int16_t state = this->setPhyProperties(); - RADIOLIB_ASSERT(state); + int16_t state = RADIOLIB_ERR_NONE; // setup uplink/downlink frequencies and datarates state = this->selectChannelsJR(this->devNonce, joinDr); RADIOLIB_ASSERT(state); + // setup all MAC properties to default values + this->beginCommon(); + + // set the physical layer configuration + state = this->setPhyProperties(); + RADIOLIB_ASSERT(state); + // configure for uplink with default configuration state = this->configureChannel(RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK); RADIOLIB_ASSERT(state); @@ -314,12 +516,13 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe } this->joinNonce = joinNonceNew; + this->homeNetId = LoRaWANNode::ntoh(&joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_HOME_NET_ID_POS], 3); + this->devAddr = LoRaWANNode::ntoh(&joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_DEV_ADDR_POS]); + // check LoRaWAN revision (the MIC verification depends on this) uint8_t dlSettings = joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_DL_SETTINGS_POS]; this->rev = (dlSettings & RADIOLIB_LORAWAN_JOIN_ACCEPT_R_1_1) >> 7; RADIOLIB_DEBUG_PRINTLN("LoRaWAN revision: 1.%d", this->rev); - this->rx1DrOffset = (dlSettings & 0x70) >> 4; - this->rx2.drMax = dlSettings & 0x0F; // verify MIC if(this->rev == 1) { @@ -348,18 +551,20 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe } } - - // parse other contents - this->homeNetId = LoRaWANNode::ntoh(&joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_HOME_NET_ID_POS], 3); - this->devAddr = LoRaWANNode::ntoh(&joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_DEV_ADDR_POS]); - // parse Rx1 delay (and subsequently Rx2) - this->rxDelays[0] = joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_RX_DELAY_POS]*1000; - if(this->rxDelays[0] == 0) { - this->rxDelays[0] = RADIOLIB_LORAWAN_RECEIVE_DELAY_1_MS; - } - this->rxDelays[1] = this->rxDelays[0] + 1000; + LoRaWANMacCommand_t cmd; + cmd.cid = RADIOLIB_LORAWAN_MAC_RX_PARAM_SETUP; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_RX_PARAM_SETUP].lenDn; + cmd.payload[0] = dlSettings & 0x7F; + uint32_t rx2Freq = uint32_t(this->rx2.freq * 10000); // default Rx2 frequency + LoRaWANNode::hton(&cmd.payload[1], rx2Freq, 3); + (void)execMacCommand(&cmd); + cmd.cid = RADIOLIB_LORAWAN_MAC_RX_TIMING_SETUP; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_RX_TIMING_SETUP].lenDn; + cmd.payload[0] = joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_RX_DELAY_POS]; + (void)execMacCommand(&cmd); + // process CFlist if present if(lenRx == RADIOLIB_LORAWAN_JOIN_ACCEPT_MAX_LEN) { uint8_t cfList[RADIOLIB_LORAWAN_JOIN_ACCEPT_CFLIST_LEN] = { 0 }; @@ -397,7 +602,7 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe // enqueue the RekeyInd MAC command to be sent in the next uplink LoRaWANMacCommand_t cmd = { - .cid = RADIOLIB_LORAWAN_MAC_CMD_REKEY, + .cid = RADIOLIB_LORAWAN_MAC_REKEY, .payload = { this->rev }, .len = sizeof(uint8_t), .repeat = 0x01 << RADIOLIB_LORAWAN_ADR_ACK_LIMIT_EXP, @@ -431,39 +636,63 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe this->adrFcnt = 0; #if !defined(RADIOLIB_EEPROM_UNSUPPORTED) - // save the device address & keys as well as JoinAccept values; these are only ever set when joining - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_ADDR_ID, this->devAddr); - mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_APP_S_KEY_ID), this->appSKey, RADIOLIB_AES128_BLOCK_SIZE); - mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FNWK_SINT_KEY_ID), this->fNwkSIntKey, RADIOLIB_AES128_BLOCK_SIZE); - mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_SNWK_SINT_KEY_ID), this->sNwkSIntKey, RADIOLIB_AES128_BLOCK_SIZE); - mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_NWK_SENC_KEY_ID), this->nwkSEncKey, RADIOLIB_AES128_BLOCK_SIZE); + // save the activation keys checksum, device address & keys as well as JoinAccept values; these are only ever set when joining + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_CHECKSUM_ID, checkSum); + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_DEV_ADDR_ID, this->devAddr); + mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_APP_S_KEY_ID), this->appSKey, RADIOLIB_AES128_BLOCK_SIZE); + mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_FNWK_SINT_KEY_ID), this->fNwkSIntKey, RADIOLIB_AES128_BLOCK_SIZE); + mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_SNWK_SINT_KEY_ID), this->sNwkSIntKey, RADIOLIB_AES128_BLOCK_SIZE); + mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_NWK_SENC_KEY_ID), this->nwkSEncKey, RADIOLIB_AES128_BLOCK_SIZE); // save join-request parameters - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_HOME_NET_ID, this->homeNetId); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_JOIN_NONCE_ID, this->joinNonce); + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_HOME_NET_ID, this->homeNetId); + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_JOIN_NONCE_ID, this->joinNonce); this->saveSession(); - this->saveChannels(); - // everything written to NVM, write current table version to persistent storage and set magic number - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TABLE_VERSION_ID, RADIOLIB_PERSISTENT_PARAM_LORAWAN_TABLE_VERSION); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID, RADIOLIB_LORAWAN_MAGIC); + // everything written to NVM, write current table version to persistent storage and set mode + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_TABLE_VERSION_ID, RADIOLIB_EEPROM_TABLE_VERSION); + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_MODE_ID, RADIOLIB_LORAWAN_MODE_OTAA); #endif - this->isJoinedFlag = true; + this->activeMode = RADIOLIB_LORAWAN_MODE_OTAA; return(RADIOLIB_ERR_NONE); } int16_t LoRaWANNode::beginABP(uint32_t addr, uint8_t* nwkSKey, uint8_t* appSKey, uint8_t* fNwkSIntKey, uint8_t* sNwkSIntKey, bool force) { + Module* mod = this->phyLayer->getMod(); #if !defined(RADIOLIB_EEPROM_UNSUPPORTED) // check if we actually need to restart from a clean session - Module* mod = this->phyLayer->getMod(); - if(!force && (mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID) == RADIOLIB_LORAWAN_MAGIC)) { + uint16_t checkSum = 0; + checkSum ^= checkSum16(addr); + checkSum ^= checkSum16(nwkSKey, 16); + checkSum ^= checkSum16(appSKey, 16); + if(fNwkSIntKey) + checkSum ^= checkSum16(fNwkSIntKey, 16); + if(sNwkSIntKey) + checkSum ^= checkSum16(sNwkSIntKey, 16); + + bool validCheckSum = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_CHECKSUM_ID) == checkSum; + bool validMode = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_MODE_ID) == RADIOLIB_LORAWAN_MODE_ABP; + + if(!force && validCheckSum && validMode) { // the device has joined already, we can just pull the data from persistent storage RADIOLIB_DEBUG_PRINTLN("Found existing session; restoring..."); + return(this->restore()); + } else { + #if RADIOLIB_DEBUG + RADIOLIB_DEBUG_PRINTLN("Failed to restore session (checksum: %d, mode: %d)", validCheckSum, validMode); + RADIOLIB_DEBUG_PRINTLN("First 16 bytes of NVM:"); + uint8_t nvmBuff[16]; + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(0), nvmBuff, 16); + RADIOLIB_DEBUG_HEXDUMP(nvmBuff, 16); + RADIOLIB_DEBUG_PRINTLN("Wiping EEPROM and starting a clean session"); + #endif + + this->wipe(); } #else (void)force; @@ -482,41 +711,43 @@ int16_t LoRaWANNode::beginABP(uint32_t addr, uint8_t* nwkSKey, uint8_t* appSKey, memcpy(this->sNwkSIntKey, sNwkSIntKey, RADIOLIB_AES128_KEY_SIZE); } - // set the physical layer configuration - this->txPwrCur = this->band->powerMax; - int16_t state = this->setPhyProperties(); - RADIOLIB_ASSERT(state); - - // setup uplink/downlink frequencies and datarates - state = this->setupChannels(nullptr); - RADIOLIB_ASSERT(state); + int16_t state = RADIOLIB_ERR_NONE; - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = (this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][0].drMax + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][0].drMin) / 2; + // calculate initial datarate - in case of fixed bands, this requires a subband to be selected // downlink datarate is calculated using a specific uplink channel, so don't care here + this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = (this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][0].drMax + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][0].drMin) / 2; -#if !defined(RADIOLIB_EEPROM_UNSUPPORTED) - // save the device address & keys - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_ADDR_ID, this->devAddr); - mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_APP_S_KEY_ID), this->appSKey, RADIOLIB_AES128_BLOCK_SIZE); - mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FNWK_SINT_KEY_ID), this->fNwkSIntKey, RADIOLIB_AES128_BLOCK_SIZE); - mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_SNWK_SINT_KEY_ID), this->sNwkSIntKey, RADIOLIB_AES128_BLOCK_SIZE); - mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_NWK_SENC_KEY_ID), this->nwkSEncKey, RADIOLIB_AES128_BLOCK_SIZE); + // setup all MAC properties to default values + this->beginCommon(); + // set the physical layer configuration + state = this->setPhyProperties(); + RADIOLIB_ASSERT(state); + +#if !defined(RADIOLIB_EEPROM_UNSUPPORTED) + // save the activation keys checksum, device address & keys + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_CHECKSUM_ID, checkSum); + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_DEV_ADDR_ID, this->devAddr); + mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_APP_S_KEY_ID), this->appSKey, RADIOLIB_AES128_BLOCK_SIZE); + mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_FNWK_SINT_KEY_ID), this->fNwkSIntKey, RADIOLIB_AES128_BLOCK_SIZE); + mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_SNWK_SINT_KEY_ID), this->sNwkSIntKey, RADIOLIB_AES128_BLOCK_SIZE); + mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_NWK_SENC_KEY_ID), this->nwkSEncKey, RADIOLIB_AES128_BLOCK_SIZE); + + // save all new frame counters this->saveSession(); - this->saveChannels(); - // everything written to NVM, write current table version to persistent storage and set magic number - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TABLE_VERSION_ID, RADIOLIB_PERSISTENT_PARAM_LORAWAN_TABLE_VERSION); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID, RADIOLIB_LORAWAN_MAGIC); + // everything written to NVM, write current table version to persistent storage and set mode + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_TABLE_VERSION_ID, RADIOLIB_EEPROM_TABLE_VERSION); + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_MODE_ID, RADIOLIB_LORAWAN_MODE_ABP); #endif - this->isJoinedFlag = true; + this->activeMode = RADIOLIB_LORAWAN_MODE_ABP; return(RADIOLIB_ERR_NONE); } bool LoRaWANNode::isJoined() { - return(this->isJoinedFlag); + return(this->activeMode != RADIOLIB_LORAWAN_MODE_NONE); } #if !defined(RADIOLIB_EEPROM_UNSUPPORTED) @@ -524,66 +755,39 @@ int16_t LoRaWANNode::saveSession() { Module* mod = this->phyLayer->getMod(); // store session configuration (MAC commands) - if(mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION_ID) != this->rev) - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION_ID, this->rev); + if(mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_VERSION_ID) != this->rev) + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_VERSION_ID, this->rev); - if(mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_NONCE_ID) != this->devNonce) - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_NONCE_ID, this->devNonce); + if(mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_DEV_NONCE_ID) != this->devNonce) + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_DEV_NONCE_ID, this->devNonce); - uint8_t txDrRx2Dr = (this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] << 4) | this->rx2.drMax; - if(mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXDR_RX2DR_ID) != txDrRx2Dr) - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXDR_RX2DR_ID, txDrRx2Dr); - - if(mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXPWR_CUR_ID) != this->txPwrCur) - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXPWR_CUR_ID, this->txPwrCur); - - uint8_t rx1DrOffDel = (this->rx1DrOffset << 4) | (this->rxDelays[0] / 1000); - if(mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_RX1_DROFF_DEL_ID) != rx1DrOffDel) - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_RX1_DROFF_DEL_ID, rx1DrOffDel); - - uint8_t rx2FreqBuf[3]; - mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_RX2FREQ_ID), rx2FreqBuf, 3); - uint32_t rx2Freq = LoRaWANNode::ntoh(&rx2FreqBuf[0], 3); - if(rx2Freq != uint32_t(this->rx2.freq * 10000)) { - rx2Freq = uint32_t(this->rx2.freq * 10000); - LoRaWANNode::hton(&rx2FreqBuf[0], rx2Freq, 3); - mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_RX2FREQ_ID), rx2FreqBuf, 3); - } - - uint8_t adrLimDel = (this->adrLimitExp << 4) | (this->adrDelayExp << 0); - if(mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_LIM_DEL_ID) != adrLimDel) - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_LIM_DEL_ID, adrLimDel); - - if(mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_NBTRANS_ID) != this->nbTrans) - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_NBTRANS_ID, this->nbTrans); - // store all frame counters - if(mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_A_FCNT_DOWN_ID) != this->aFcntDown) - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_A_FCNT_DOWN_ID, this->aFcntDown); + if(mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_A_FCNT_DOWN_ID) != this->aFcntDown) + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_A_FCNT_DOWN_ID, this->aFcntDown); - if(mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_N_FCNT_DOWN_ID) != this->nFcntDown) - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_N_FCNT_DOWN_ID, this->nFcntDown); + if(mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_N_FCNT_DOWN_ID) != this->nFcntDown) + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_N_FCNT_DOWN_ID, this->nFcntDown); - if(mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_CONF_FCNT_UP_ID) != this->confFcntUp) - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_CONF_FCNT_UP_ID, this->confFcntUp); + if(mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_CONF_FCNT_UP_ID) != this->confFcntUp) + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_CONF_FCNT_UP_ID, this->confFcntUp); - if(mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_CONF_FCNT_DOWN_ID) != this->confFcntDown) - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_CONF_FCNT_DOWN_ID, this->confFcntDown); + if(mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_CONF_FCNT_DOWN_ID) != this->confFcntDown) + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_CONF_FCNT_DOWN_ID, this->confFcntDown); - if(mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_FCNT_ID) != this->adrFcnt) - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_FCNT_ID, this->adrFcnt); + if(mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_ADR_FCNT_ID) != this->adrFcnt) + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_ADR_FCNT_ID, this->adrFcnt); // fcntUp is saved using highly efficient wear-leveling as this is by far going to be written most often this->saveFcntUp(); // if there is, or was, any MAC command in the queue, overwrite with the current MAC queue uint8_t queueBuff[sizeof(LoRaWANMacCommandQueue_t)] = { 0 }; - mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FOPTS_ID), queueBuff, sizeof(LoRaWANMacCommandQueue_t)); + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_MAC_QUEUE_UL_ID), queueBuff, sizeof(LoRaWANMacCommandQueue_t)); LoRaWANMacCommandQueue_t cmdTemp; memcpy(&cmdTemp, queueBuff, sizeof(LoRaWANMacCommandQueue_t)); if(this->commandsUp.numCommands > 0 || cmdTemp.numCommands > 0) { memcpy(queueBuff, &this->commandsUp, sizeof(LoRaWANMacCommandQueue_t)); - mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FOPTS_ID), queueBuff, sizeof(LoRaWANMacCommandQueue_t)); + mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_MAC_QUEUE_UL_ID), queueBuff, sizeof(LoRaWANMacCommandQueue_t)); } return(RADIOLIB_ERR_NONE); @@ -593,17 +797,17 @@ int16_t LoRaWANNode::saveFcntUp() { Module* mod = this->phyLayer->getMod(); uint8_t fcntBuff[30] = { 0 }; - mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID), fcntBuff, 30); + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_FCNT_UP_ID), fcntBuff, 30); // we discard the first two bits - your flash will likely be far dead by the time you reach 2^30 uplinks // the first two bytes of the remaining 30 bytes are stored straight into storage without additional wear leveling // because they hardly ever change uint8_t bits_30_22 = (uint8_t)(this->fcntUp >> 22); if(fcntBuff[0] != bits_30_22) - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID, bits_30_22, 0); + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_FCNT_UP_ID, bits_30_22, 0); uint8_t bits_22_14 = (uint8_t)(this->fcntUp >> 14); if(fcntBuff[1] != bits_22_14) - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID, bits_22_14, 1); + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_FCNT_UP_ID, bits_22_14, 1); // the next 7 bits are stored into one of few indices // this index is indicated by the first byte that has its state (most significant bit) different from its predecessor @@ -624,7 +828,7 @@ int16_t LoRaWANNode::saveFcntUp() { // flip the first bit of this byte to indicate that we just wrote here bits_14_7 |= (~(fcntBuff[idx] >> 7)) << 7; - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID, bits_14_7, idx); + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_FCNT_UP_ID, bits_14_7, idx); } // equally, the last 7 bits are stored into one of many indices @@ -643,31 +847,10 @@ int16_t LoRaWANNode::saveFcntUp() { // flip the first bit of this byte to indicate that we just wrote here bits_7_0 |= (~(fcntBuff[idx] >> 7)) << 7; - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID, bits_7_0, idx); + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_FCNT_UP_ID, bits_7_0, idx); return(RADIOLIB_ERR_NONE); } - -int16_t LoRaWANNode::saveChannels() { - const uint8_t bytesPerChannel = 5; - const uint8_t numBytes = 2 * RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS * bytesPerChannel; - uint8_t buffer[numBytes] = { 0 }; - for(uint8_t dir = 0; dir < 2; dir++) { - for(uint8_t i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { - uint8_t chBuff[bytesPerChannel] = { 0 }; - chBuff[0] = (uint8_t)this->availableChannels[dir][i].enabled << 7; - chBuff[0] |= this->availableChannels[dir][i].idx; - uint32_t freq = this->availableChannels[dir][i].freq*10000.0; - LoRaWANNode::hton(&chBuff[1], freq, 3); - chBuff[4] = this->availableChannels[dir][i].drMax << 4; - chBuff[4] |= this->availableChannels[dir][i].drMin << 0; - memcpy(&buffer[(dir * RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS * bytesPerChannel) + i * bytesPerChannel], chBuff, bytesPerChannel); - } - } - Module* mod = this->phyLayer->getMod(); - mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FREQS_ID), buffer, numBytes); - return(RADIOLIB_ERR_NONE); -} #endif // RADIOLIB_EEPROM_UNSUPPORTED #if defined(RADIOLIB_BUILD_ARDUINO) @@ -690,6 +873,11 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf return(RADIOLIB_ERR_UPLINK_UNAVAILABLE); } + // if adhering to dutyCycle and the time since last uplink + interval has not elapsed, return an error + if(this->dutyCycleEnabled && this->rxDelayStart + dutyCycleInterval(this->dutyCycle, this->lastToA) > mod->hal->millis()) { + return(RADIOLIB_ERR_UPLINK_UNAVAILABLE); + } + // check destination port if(port > 0xDF) { return(RADIOLIB_ERR_INVALID_PORT); @@ -716,8 +904,10 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf // check maximum payload len as defined in phy if(len > this->band->payloadLenMax[this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]]) { - return(RADIOLIB_ERR_PACKET_TOO_LONG); + len = this->band->payloadLenMax[this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]]; + // return(RADIOLIB_ERR_PACKET_TOO_LONG); } + if(RADIOLIB_LORAWAN_FRAME_LEN(len, foptsLen)) // increase frame counter by one this->fcntUp += 1; @@ -732,14 +922,15 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf // if we hit the Limit + Delay, try one of three, in order: // set TxPower to max, set DR to min, enable all defined channels if ((this->fcntUp - this->adrFcnt) == (adrLimit + adrDelay)) { - - // set the maximum power supported by both the module and the band - int8_t pwrPrev = this->txPwrCur; - state = this->setTxPower(this->band->powerMax); - RADIOLIB_ASSERT(state); - - if(this->txPwrCur == pwrPrev) { - + + // if the TxPower field has some offset, remove it and switch to maximum power + if(this->txPowerCur > 0) { + this->txPowerCur = 0; + // set the maximum power supported by both the module and the band + state = this->setTxPower(this->txPowerMax); + RADIOLIB_ASSERT(state); + + } else { // failed to increase Tx power, so try to decrease the datarate if(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] > this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK].drMin) { this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]--; @@ -757,6 +948,14 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf } + LoRaWANMacCommand_t cmd; + cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; + cmd.payload[0] = (this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] << 4); + cmd.payload[0] |= 0; // default to max Tx Power + cmd.payload[3] |= (1 << 7); // set the RFU bit, which means that the channel mask gets ignored + (void)execMacCommand(&cmd); + // we tried something to improve the range, so increase the ADR frame counter by 'ADR delay' this->adrFcnt += adrDelay; } @@ -766,6 +965,11 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf state = this->configureChannel(RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK); RADIOLIB_ASSERT(state); + // if dwell time is imposed, calculated expected time on air and cancel if exceeds + if(this->dwellTimeEnabledUp && this->phyLayer->getTimeOnAir(RADIOLIB_LORAWAN_FRAME_LEN(len, foptsLen) - 16)/1000 > this->dwellTimeUp) { + return(RADIOLIB_ERR_PACKET_TOO_LONG); + } + // build the uplink message // the first 16 bytes are reserved for MIC calculation blocks size_t uplinkMsgLen = RADIOLIB_LORAWAN_FRAME_LEN(len, foptsBufSize); @@ -896,6 +1100,9 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf this->rxDelayStart = mod->hal->millis(); RADIOLIB_DEBUG_PRINTLN("Uplink sent <-- Rx Delay start"); + // calculate Time on Air of this uplink in milliseconds + this->lastToA = this->phyLayer->getTimeOnAir(uplinkMsgLen - RADIOLIB_LORAWAN_FHDR_LEN_START_OFFS) / 1000; + #if !RADIOLIB_STATIC_ONLY delete[] uplinkMsg; #endif @@ -911,7 +1118,7 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf event->confirming = isConfirmingDown; event->datarate = this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]; event->freq = currentChannels[event->dir].freq; - event->power = this->txPwrCur; + event->power = this->txPowerMax - this->txPowerCur * 2; event->fcnt = this->fcntUp; event->port = port; } @@ -1115,6 +1322,8 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len, LoRaWANEvent_t* event) // in LoRaWAN v1.1, a frame can be a network frame if there is no Application payload // i.e., no payload at all (empty frame or FOpts only), or MAC only payload (FPort = 0) + // TODO "NFCntDown is used for MAC communication on port 0 and when the FPort field is missing" + // so what about empty frames for ACK? Per TS008, these should be Application downlinks bool isAppDownlink = true; if(payLen <= 0) { if(this->rev == 1) { @@ -1213,26 +1422,39 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len, LoRaWANEvent_t* event) int8_t remLen = foptsLen; uint8_t* foptsPtr = fopts; while(remLen > 0) { + uint8_t cid = *foptsPtr; + uint8_t macLen = getMacPayloadLength(cid); + if(macLen + 1 > remLen) + break; LoRaWANMacCommand_t cmd = { - .cid = *foptsPtr, + .cid = cid, .payload = { 0 }, - .len = (uint8_t)RADIOLIB_MIN((remLen - 1), 5), + .len = macLen, .repeat = 0, }; - memcpy(cmd.payload, foptsPtr + 1, cmd.len); + memcpy(cmd.payload, foptsPtr + 1, macLen); RADIOLIB_DEBUG_PRINTLN("[%02X]: %02X %02X %02X %02X %02X (%d)", cmd.cid, cmd.payload[0], cmd.payload[1], cmd.payload[2], cmd.payload[3], cmd.payload[4], cmd.len); - // try to process the mac command - // TODO how to handle incomplete commands? - size_t processedLen = execMacCommand(&cmd) + 1; + // process the MAC command + bool sendUp = execMacCommand(&cmd); + if(sendUp) { + pushMacCommand(&cmd, &this->commandsUp); + } // processing succeeded, move in the buffer to the next command - remLen -= processedLen; - foptsPtr += processedLen; - RADIOLIB_DEBUG_PRINTLN("Processed: %d, remaining: %d", processedLen, remLen); + remLen -= (macLen + 1); + foptsPtr += (macLen + 1); + RADIOLIB_DEBUG_PRINTLN("Processed: %d, remaining: %d", (macLen + 1), remLen); } + RADIOLIB_DEBUG_PRINTLN("MAC response:"); + for (int x; x < this->commandsUp.numCommands; x++) { + LoRaWANMacCommand_t cmd = this->commandsUp.commands[x]; + RADIOLIB_DEBUG_PRINTLN("[%02x] %02x %02x %02x %02x %02x (%02x)", cmd.cid, + cmd.payload[0], cmd.payload[1], cmd.payload[2], cmd.payload[3], cmd.payload[4], cmd.len); + } + // if FOptsLen for the next uplink is larger than can be piggybacked onto an uplink, send separate uplink if(this->commandsUp.len > 15) { size_t foptsBufSize = this->commandsUp.len; @@ -1262,7 +1484,14 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len, LoRaWANEvent_t* event) } this->isMACPayload = true; - this->uplink(foptsBuff, foptsBufSize, RADIOLIB_LORAWAN_FPORT_MAC_COMMAND); + // temporarily lift dutyCycle restrictions to allow immediate MAC response + bool prevDC = this->dutyCycleEnabled; + this->dutyCycleEnabled = false; + RADIOLIB_DEBUG_PRINTLN("Sending MAC-only uplink .. "); + state = this->uplink(foptsBuff, foptsBufSize, RADIOLIB_LORAWAN_FPORT_MAC_COMMAND); + RADIOLIB_DEBUG_PRINTLN(" .. state: %d", state); + this->dutyCycleEnabled = prevDC; + #if !RADIOLIB_STATIC_ONLY delete[] foptsBuff; #endif @@ -1273,7 +1502,9 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len, LoRaWANEvent_t* event) uint8_t* strDown = new uint8_t[this->band->payloadLenMax[this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK]]]; #endif size_t lenDown = 0; + RADIOLIB_DEBUG_PRINTLN("Receiving after MAC-only uplink .. "); state = this->downlink(strDown, &lenDown); + RADIOLIB_DEBUG_PRINTLN(" .. state: %d", state); #if !RADIOLIB_STATIC_ONLY delete[] strDown; #endif @@ -1292,7 +1523,7 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len, LoRaWANEvent_t* event) event->confirming = isConfirmingUp; event->datarate = this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK]; event->freq = currentChannels[event->dir].freq; - event->power = this->txPwrCur; + event->power = this->txPowerMax - this->txPowerCur * 2; event->fcnt = isAppDownlink ? this->aFcntDown : this->nFcntDown; event->port = downlinkMsg[RADIOLIB_LORAWAN_FHDR_FPORT_POS(foptsLen)]; } @@ -1370,6 +1601,11 @@ uint32_t LoRaWANNode::getAFcntDown() { return(this->aFcntDown); } +void LoRaWANNode::resetFcntDown() { + this->nFcntDown = 0; + this->aFcntDown = 0; +} + uint32_t LoRaWANNode::generateMIC(uint8_t* msg, size_t len, uint8_t* key) { if((msg == NULL) || (len == 0)) { return(0); @@ -1401,7 +1637,8 @@ bool LoRaWANNode::verifyMIC(uint8_t* msg, size_t len, uint8_t* key) { int16_t LoRaWANNode::setPhyProperties() { // set the physical layer configuration - int16_t state = this->setTxPower(this->txPwrCur); + int8_t pwr = this->txPowerMax - this->txPowerCur * 2; + int16_t state = this->setTxPower(pwr); RADIOLIB_ASSERT(state); uint8_t syncWord[3] = { 0 }; @@ -1429,12 +1666,12 @@ int16_t LoRaWANNode::setPhyProperties() { } int16_t LoRaWANNode::setupChannels(uint8_t* cfList) { - size_t num = 0; RADIOLIB_DEBUG_PRINTLN("Setting up channels"); // in case of frequency list-type band, copy the default TX channels into the available channels, with RX1 = TX if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) { RADIOLIB_DEBUG_PRINTLN("Dynamic band"); + size_t num = 0; // copy the default defined channels into the first slots for(; num < 3 && this->band->txFreqs[num].enabled; num++) { this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num] = this->band->txFreqs[num]; @@ -1444,59 +1681,36 @@ int16_t LoRaWANNode::setupChannels(uint8_t* cfList) { // if there is a cflist present, parse its frequencies into the next five slots, with datarate range copied from default channel 0 if(cfList != nullptr) { RADIOLIB_DEBUG_PRINTLN("CFList present"); + LoRaWANMacCommand_t cmd; + cmd.cid = RADIOLIB_LORAWAN_MAC_NEW_CHANNEL; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_NEW_CHANNEL].lenDn; + // datarate range for all new channels is equal to the default channels + cmd.payload[4] = (this->band->txFreqs[0].drMax << 4) | this->band->txFreqs[0].drMin; for(uint8_t i = 0; i < 5; i++, num++) { - LoRaWANChannel_t chnl; - chnl.enabled = true; - chnl.idx = num; - uint32_t freq = LoRaWANNode::ntoh(&cfList[3*i], 3); - chnl.freq = (float)freq/10000.0; - chnl.drMin = this->band->txFreqs[0].drMin; // drMin is equal for all channels - chnl.drMax = this->band->txFreqs[0].drMax; // drMax is equal for all channels - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num] = chnl; - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][num] = chnl; - RADIOLIB_DEBUG_PRINTLN("Channel UL/DL %d frequency = %f MHz", chnl.idx, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].freq); + cmd.payload[0] = num; + memcpy(&cmd.payload[1], &cfList[i*3], 3); + (void)execMacCommand(&cmd); } } for(; num < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; num++) { this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num] = RADIOLIB_LORAWAN_CHANNEL_NONE; } + } else { // RADIOLIB_LORAWAN_BAND_FIXED if(cfList != nullptr) { - uint8_t chSpan = 0; - uint8_t chNum = 0; - // in case of mask-type bands, copy those frequencies that are masked true into the available TX channels - for(size_t chMaskCntl = 0; chMaskCntl < 5; chMaskCntl++) { - uint16_t mask = LoRaWANNode::ntoh(&cfList[2*chMaskCntl]); - RADIOLIB_DEBUG_PRINTLN("mask[%d] = 0x%04x", chMaskCntl, mask); - for(size_t i = 0; i < 16; i++) { - // if we must roll over to next span, reset chNum and move to next channel span - if(chNum >= this->band->txSpans[chSpan].numChannels) { - chNum = 0; - chSpan++; - } + RADIOLIB_DEBUG_PRINTLN("CFList present"); + LoRaWANMacCommand_t cmd; + cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; + cmd.payload[0] = 0xFF; // same datarate and payload - if(mask & (1UL << i)) { - if(chSpan >= this->band->numTxSpans) { - RADIOLIB_DEBUG_PRINTLN("channel bitmask overrun!"); - return(RADIOLIB_ERR_UNKNOWN); - } - LoRaWANChannel_t chnl; - chnl.enabled = true; - chnl.idx = chMaskCntl*16 + i; - chnl.freq = this->band->txSpans[chSpan].freqStart + chNum*this->band->txSpans[chSpan].freqStep; - chnl.drMin = this->band->txSpans[chSpan].drMin; - chnl.drMax = this->band->txSpans[chSpan].drMax; - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num] = chnl; - // downlink channels are dynamically calculated on each uplink in selectChannels() - RADIOLIB_DEBUG_PRINTLN("Channel UL %d frequency = %f MHz", num, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].freq); - num++; - } - chNum++; - } - } - for(; chNum < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; chNum++) { - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chNum] = RADIOLIB_LORAWAN_CHANNEL_NONE; + // in case of mask-type bands, copy those frequencies that are masked true into the available TX channels + size_t numChMasks = 3 + this->band->numTxSpans; // 4 masks for bands with 2 spans, 5 spans for bands with 1 span + for(size_t chMaskCntl = 0; chMaskCntl < numChMasks; chMaskCntl++) { + cmd.payload[3] = chMaskCntl << 4; // NbTrans = 0 -> keep the same + memcpy(&cmd.payload[1], &cfList[chMaskCntl*2], 2); + (void)execMacCommand(&cmd); } } } @@ -1524,7 +1738,7 @@ int16_t LoRaWANNode::selectSubband(uint8_t idx) { } int16_t LoRaWANNode::selectSubband(uint8_t startChannel, uint8_t endChannel) { - if(this->isJoinedFlag) { + if(this->activeMode != RADIOLIB_LORAWAN_MODE_NONE) { RADIOLIB_DEBUG_PRINTLN("There is already an active session - cannot change subband"); return(RADIOLIB_ERR_INVALID_CHANNEL); } @@ -1554,9 +1768,9 @@ int16_t LoRaWANNode::selectSubband(uint8_t startChannel, uint8_t endChannel) { chnl.freq = this->band->txSpans[0].freqStart + chnl.idx*this->band->txSpans[0].freqStep; chnl.drMin = this->band->txSpans[0].drMin; chnl.drMax = this->band->txSpans[0].drMax; - availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chNum] = chnl; + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chNum] = chnl; // downlink channel is dynamically calculated on each uplink in selectChannels() - RADIOLIB_DEBUG_PRINTLN("Channel UL %d frequency = %f MHz", chNum, availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chNum].freq); + RADIOLIB_DEBUG_PRINTLN("Channel UL %d frequency = %f MHz", chNum, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chNum].freq); } return(RADIOLIB_ERR_NONE); } @@ -1685,11 +1899,11 @@ int16_t LoRaWANNode::selectChannels() { } // select a random ID & channel from the list of enabled and possible channels uint8_t channelID = channelsEnabled[this->phyLayer->random(numChannels)]; - this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][channelID]; + this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][channelID]; if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) { // for dynamic bands, the downlink channel is the one matched to the uplink channel - this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][channelID]; + this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][channelID]; } else { // RADIOLIB_LORAWAN_BAND_FIXED // for fixed bands, the downlink channel is the uplink channel ID `modulo` number of downlink channels @@ -1734,16 +1948,69 @@ void LoRaWANNode::setADR(bool enable) { this->adrEnabled = enable; } +void LoRaWANNode::setDutyCycle(bool enable, uint32_t msPerHour) { + this->dutyCycleEnabled = true; + if(msPerHour <= 0) { + this->dutyCycle = this->band->dutyCycle; + } else { + this->dutyCycle = msPerHour; + } +} + +// given an airtime in milliseconds, calculate the minimum uplink interval +// to adhere to a given dutyCycle +uint32_t LoRaWANNode::dutyCycleInterval(uint32_t msPerHour, uint32_t airtime) { + if(msPerHour == 0 || airtime == 0) { + return(0); + } + uint32_t oneHourInMs = 60 * 60 * 1000; + float numPackets = msPerHour / airtime; + uint32_t delayMs = oneHourInMs / numPackets + 1; // + 1 to prevent rounding problems + return(delayMs); +} + +uint32_t LoRaWANNode::timeUntilUplink() { + Module* mod = this->phyLayer->getMod(); + uint32_t nextUplink = this->rxDelayStart + dutyCycleInterval(this->dutyCycle, this->lastToA); + uint32_t timeRemaining = RADIOLIB_MAX(0, nextUplink - mod->hal->millis() + 1); + return(timeRemaining); +} + +void LoRaWANNode::setDwellTime(bool enable, uint32_t msPerUplink) { + this->dwellTimeEnabledUp = enable; + if(msPerUplink <= 0) { + this->dwellTimeUp = this->band->dwellTimeUp; + } else { + this->dwellTimeUp = msPerUplink; + } +} + +uint8_t LoRaWANNode::maxPayloadDwellTime() { + // configure current datarate + DataRate_t dr; + findDataRate(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK], &dr); + (void)this->phyLayer->setDataRate(dr); + uint8_t minPayLen = 0; + uint8_t maxPayLen = 255; + uint8_t payLen = (minPayLen + maxPayLen) / 2; + // do some binary search to find maximum allowed payload length + while(payLen != minPayLen && payLen != maxPayLen) { + if(this->phyLayer->getTimeOnAir(payLen) > this->dwellTimeUp) { + maxPayLen = payLen; + } else { + minPayLen = payLen; + } + payLen = (minPayLen + maxPayLen) / 2; + } + return(payLen - 13); // fixed 13-byte header +} + int16_t LoRaWANNode::setTxPower(int8_t txPower) { int16_t state = RADIOLIB_ERR_INVALID_OUTPUT_POWER; while(state == RADIOLIB_ERR_INVALID_OUTPUT_POWER) { // go from the highest power and lower it until we hit one supported by the module state = this->phyLayer->setOutputPower(txPower--); } - if(state == RADIOLIB_ERR_NONE) { - txPower++; - this->txPwrCur = txPower; - } return(state); } @@ -1807,6 +2074,21 @@ int16_t LoRaWANNode::configureChannel(uint8_t dir) { return(state); } +bool LoRaWANNode::sendMacCommandReq(uint8_t cid) { + bool valid = false; + for(size_t i = 0; i < RADIOLIB_LORAWAN_NUM_MAC_COMMANDS; i++) { + if(MacTable[i].cid == cid) { + valid = MacTable[i].user; + } + } + if(!valid) + return(false); + + LoRaWANMacCommand_t cmd = { cid, 0, 0, 0 }; + pushMacCommand(&cmd, &this->commandsUp); + return(true); +} + int16_t LoRaWANNode::pushMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQueue_t* queue) { if(queue->numCommands >= RADIOLIB_LORAWAN_MAC_COMMAND_QUEUE_SIZE) { return(RADIOLIB_ERR_COMMAND_QUEUE_FULL); @@ -1819,13 +2101,17 @@ int16_t LoRaWANNode::pushMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQ return(RADIOLIB_ERR_NONE); } -int16_t LoRaWANNode::deleteMacCommand(uint8_t cid, LoRaWANMacCommandQueue_t* queue) { +int16_t LoRaWANNode::deleteMacCommand(uint8_t cid, LoRaWANMacCommandQueue_t* queue, uint8_t payload[5]) { if(queue->numCommands == 0) { return(RADIOLIB_ERR_COMMAND_QUEUE_EMPTY); } for(size_t index = 0; index < queue->numCommands; index++) { if(queue->commands[index].cid == cid) { + // if a pointer to a payload is supplied, copy the command's payload over + if(payload) { + memcpy(payload, queue->commands[index].payload, queue->commands[index].len); + } queue->len -= (1 + queue->commands[index].len); // 1 byte for command ID, len for payload // move all subsequent commands one forward in the queue if(index < RADIOLIB_LORAWAN_MAC_COMMAND_QUEUE_SIZE - 1) { @@ -1841,39 +2127,41 @@ int16_t LoRaWANNode::deleteMacCommand(uint8_t cid, LoRaWANMacCommandQueue_t* que return(RADIOLIB_ERR_COMMAND_QUEUE_ITEM_NOT_FOUND); } -size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { +bool LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd, bool saveToEeprom) { RADIOLIB_DEBUG_PRINTLN("exe MAC CID = %02x, len = %d", cmd->cid, cmd->len); - if(cmd->cid >= RADIOLIB_LORAWAN_MAC_CMD_PROPRIETARY) { + Module* mod = this->phyLayer->getMod(); +#if defined(RADIOLIB_EEPROM_UNSUPPORTED) + (void)saveToEeprom; + (void)mod; +#endif + + if(cmd->cid >= RADIOLIB_LORAWAN_MAC_PROPRIETARY) { // TODO call user-provided callback for proprietary MAC commands? - return(cmd->len - 1); + return(false); } switch(cmd->cid) { - case(RADIOLIB_LORAWAN_MAC_CMD_RESET): { + case(RADIOLIB_LORAWAN_MAC_RESET): { // get the server version uint8_t srvVersion = cmd->payload[0]; RADIOLIB_DEBUG_PRINTLN("Server version: 1.%d", srvVersion); if(srvVersion == this->rev) { // valid server version, stop sending the ResetInd MAC command - deleteMacCommand(RADIOLIB_LORAWAN_MAC_CMD_RESET, &this->commandsUp); + deleteMacCommand(RADIOLIB_LORAWAN_MAC_RESET, &this->commandsUp); } - return(1); + return(false); } break; - case(RADIOLIB_LORAWAN_MAC_CMD_LINK_CHECK): { - // TODO sent by gateway as reply to node request, how to get this info to the user? - uint8_t margin = cmd->payload[0]; - uint8_t gwCnt = cmd->payload[1]; - RADIOLIB_DEBUG_PRINTLN("Link check: margin = %d dB, gwCnt = %d", margin, gwCnt); - (void)margin; - (void)gwCnt; - return(2); + case(RADIOLIB_LORAWAN_MAC_LINK_CHECK): { + pushMacCommand(cmd, &this->commandsDown); + return(false); } break; - case(RADIOLIB_LORAWAN_MAC_CMD_LINK_ADR): { + case(RADIOLIB_LORAWAN_MAC_LINK_ADR): { // get the ADR configuration - // TODO all these configuration should only be set if all ACKs are set, otherwise retain previous state (per spec) + // per spec, all these configuration should only be set if all ACKs are set, otherwise retain previous state + // but we don't bother and try to set each individual command uint8_t drUp = (cmd->payload[0] & 0xF0) >> 4; uint8_t txPower = cmd->payload[0] & 0x0F; uint16_t chMask = LoRaWANNode::ntoh(&cmd->payload[1]); @@ -1883,8 +2171,12 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { // apply the configuration uint8_t drAck = 0; - if(drUp == 0x0F) { + if(drUp == 0x0F) { // keep the same drAck = 1; + + // replace the 'placeholder' with the current actual value for saving + cmd->payload[0] = (cmd->payload[0] & 0x0F) | (this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] << 4); + } else if (this->band->dataRates[drUp] != RADIOLIB_LORAWAN_DATA_RATE_UNUSED) { uint8_t drDown = getDownlinkDataRate(drUp, this->rx1DrOffset, this->band->rx1DataRateBase, this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK].drMin, this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK].drMax); @@ -1898,98 +2190,158 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { if(txPower == 0x0F) { pwrAck = 1; + // replace the 'placeholder' with the current actual value for saving + cmd->payload[0] = (cmd->payload[0] & 0xF0) | this->txPowerCur; + } else { - int8_t pwr = this->band->powerMax - 2*txPower; + int8_t pwr = this->txPowerMax - 2*txPower; int16_t state = this->setTxPower(pwr); // only acknowledge if the requested datarate was succesfully configured - if((state == RADIOLIB_ERR_NONE) && (this->txPwrCur == pwr)) { + if(state == RADIOLIB_ERR_NONE) { pwrAck = 1; + this->txPowerCur = txPower; } } + bool isSuccessive = false; uint8_t chMaskAck = 1; - if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) { - for(size_t i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { - if(chMaskCntl == 0) { - // if chMaskCntl == 0, apply the mask by looking at each channel bit - RADIOLIB_DEBUG_PRINTLN("ADR channel %d: %d --> %d", this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled, (chMask >> i) & 0x01); - if(chMask & (1UL << i)) { - // if it should be enabled but is not currently defined, stop immediately - if(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx == RADIOLIB_LORAWAN_CHANNEL_INDEX_NONE) { - chMaskAck = 0; - break; + // only apply channel mask when the RFU bit is not set + // (which is set on the internal MAC command when creating new session) + if((cmd->payload[3] >> 7) == 0) { + if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) { + for(size_t i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { + if(chMaskCntl == 0) { + // if chMaskCntl == 0, apply the mask by looking at each channel bit + RADIOLIB_DEBUG_PRINTLN("ADR channel %d: %d --> %d", this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled, (chMask >> i) & 0x01); + if(chMask & (1UL << i)) { + // if it should be enabled but is not currently defined, stop immediately + if(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx == RADIOLIB_LORAWAN_CHANNEL_INDEX_NONE) { + chMaskAck = 0; + break; + } + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = true; + } else { + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = false; } - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = true; - } else { - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = false; - } - } else if(chMaskCntl == 6) { - // if chMaskCntl == 6, enable all defined channels - if(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx != RADIOLIB_LORAWAN_CHANNEL_INDEX_NONE) { - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = true; + } else if(chMaskCntl == 6) { + // if chMaskCntl == 6, enable all defined channels + if(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx != RADIOLIB_LORAWAN_CHANNEL_INDEX_NONE) { + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = true; + } } + } - - } - } else { // RADIOLIB_LORAWAN_BAND_FIXED - // delete any prior ADR responses from the uplink queue, but do not care if none is present yet - (void)deleteMacCommand(RADIOLIB_LORAWAN_MAC_CMD_LINK_ADR, &this->commandsUp); - RADIOLIB_DEBUG_PRINTLN("mask[%d] = 0x%04x", chMaskCntl, chMask); - uint8_t num = 0; - uint8_t chNum = chMaskCntl*16; - uint8_t chSpan = 0; - for(size_t i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { - RADIOLIB_DEBUG_PRINTLN("chNum: %d, chSpan: %d, i: %d, mask: %d", chNum, chSpan, i, chMask & (1UL << i)); - // if we must roll over to next span, reset chNum and move to next channel span - if(chNum >= this->band->txSpans[chSpan].numChannels) { - chNum = 0; - chSpan++; + } else { // RADIOLIB_LORAWAN_BAND_FIXED + // delete any prior ADR responses from the uplink queue, but do not care if none is present yet + int16_t state = deleteMacCommand(RADIOLIB_LORAWAN_MAC_LINK_ADR, &this->commandsUp); + if(state == RADIOLIB_ERR_NONE) { + isSuccessive = true; // if we found an ADR Ans in the uplink MAC queue, this is a successive ADR MAC request } + RADIOLIB_DEBUG_PRINTLN("mask[%d] = 0x%04x", chMaskCntl, chMask); + uint8_t num = 0; + uint8_t chNum = chMaskCntl*16; + uint8_t chSpan = 0; + for(size_t i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { + RADIOLIB_DEBUG_PRINTLN("chNum: %d, chSpan: %d, i: %d, mask: %d", chNum, chSpan, i, chMask & (1UL << i)); + // if we must roll over to next span, reset chNum and move to next channel span + if(chNum >= this->band->txSpans[chSpan].numChannels) { + chNum = 0; + chSpan++; + } - if(chMask & (1UL << i)) { - if(chSpan >= this->band->numTxSpans) { - RADIOLIB_DEBUG_PRINTLN("channel bitmask overrun!"); - return(RADIOLIB_ERR_UNKNOWN); + if(chMask & (1UL << i)) { + if(chSpan >= this->band->numTxSpans) { + RADIOLIB_DEBUG_PRINTLN("channel bitmask overrun!"); + return(RADIOLIB_ERR_UNKNOWN); + } + LoRaWANChannel_t chnl; + chnl.enabled = true; + chnl.idx = chMaskCntl*16 + i; + chnl.freq = this->band->txSpans[chSpan].freqStart + chNum*this->band->txSpans[chSpan].freqStep; + chnl.drMin = this->band->txSpans[chSpan].drMin; + chnl.drMax = this->band->txSpans[chSpan].drMax; + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num] = chnl; + // downlink channels are dynamically calculated on each uplink in selectChannels() + RADIOLIB_DEBUG_PRINTLN("Channel UL %d (%d) frequency = %f MHz", num, chnl.idx, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].freq); + num++; } - LoRaWANChannel_t chnl; - chnl.enabled = true; - chnl.idx = chMaskCntl*16 + i; - chnl.freq = this->band->txSpans[chSpan].freqStart + chNum*this->band->txSpans[chSpan].freqStep; - chnl.drMin = this->band->txSpans[chSpan].drMin; - chnl.drMax = this->band->txSpans[chSpan].drMax; - availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num] = chnl; - // downlink channels are dynamically calculated on each uplink in selectChannels() - RADIOLIB_DEBUG_PRINTLN("Channel UL %d (%d) frequency = %f MHz", num, chnl.idx, availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].freq); - num++; + chNum++; } - chNum++; } } - // TODO should we actually save the channels because the masks may have changed stuff? - // this may wear the storage quickly on more mobile devices / changing RF environment - this->nbTrans = nbTrans; + if(nbTrans == 0) { // keep the same + cmd->payload[3] = (cmd->payload[3] & 0xF0) | this->nbTrans; // set current number of retransmissions for saving + } else { + this->nbTrans = nbTrans; + } + +#if !defined(RADIOLIB_EEPROM_UNSUPPORTED) + if(saveToEeprom) { + uint8_t payLen = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; + if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) { + + // if RFU bit is set, this is just a change in Datarate or TxPower, so read ADR command and overwrite first byte + if((cmd->payload[3] >> 7) == 1) { + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_LINK_ADR_ID) + 1, &(cmd->payload[1]), 3); + } + + // save to the single ADR MAC location + mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_LINK_ADR_ID), &(cmd->payload[0]), payLen); + + } else { + // read how many ADR masks are already stored + uint8_t macNumADR = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_NUM_ADR_MASKS_ID); + + // if RFU bit is set, this is just a change in Datarate or TxPower, so read ADR command and overwrite first byte + if((cmd->payload[3] >> 7) == 1) { + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_UL_CHANNELS_ID) + macNumADR * payLen + 1, &(cmd->payload[1]), 3); + } else { + if(isSuccessive) { + // saved another ADR mask, so increase counter + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_NUM_ADR_MASKS_ID, macNumADR + 1); + } else { + // this is the first ADR mask in this downlink, so (re)set counter to 1 + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_NUM_ADR_MASKS_ID, 1); + } + } + + // save to the uplink channel location, to the macNumADR-th slot of 4 bytes + mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_UL_CHANNELS_ID) + macNumADR * payLen, &(cmd->payload[0]), payLen); + } + } +#endif // send the reply cmd->len = 1; cmd->payload[0] = (pwrAck << 2) | (drAck << 1) | (chMaskAck << 0); RADIOLIB_DEBUG_PRINTLN("ADR ANS: status = 0x%02x", cmd->payload[0]); - pushMacCommand(cmd, &this->commandsUp); - return(4); + return(true); } break; - case(RADIOLIB_LORAWAN_MAC_CMD_DUTY_CYCLE): { + case(RADIOLIB_LORAWAN_MAC_DUTY_CYCLE): { uint8_t maxDutyCycle = cmd->payload[0] & 0x0F; RADIOLIB_DEBUG_PRINTLN("Max duty cycle: 1/2^%d", maxDutyCycle); + if(maxDutyCycle == 0) { + this->dutyCycle = this->band->dutyCycle; + } else { + this->dutyCycle = 60 * 60 * 1000 / (1 << maxDutyCycle); + } - // TODO implement this - (void)maxDutyCycle; - return(1); +#if !defined(RADIOLIB_EEPROM_UNSUPPORTED) + if(saveToEeprom) { + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_DUTY_CYCLE_ID, cmd->payload[0]); + + } +#endif + + cmd->len = 0; + return(true); } break; - case(RADIOLIB_LORAWAN_MAC_CMD_RX_PARAM_SETUP): { + case(RADIOLIB_LORAWAN_MAC_RX_PARAM_SETUP): { // get the configuration this->rx1DrOffset = (cmd->payload[0] & 0x70) >> 4; uint8_t rx1OffsAck = 1; @@ -2006,29 +2358,33 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { this->phyLayer->setFrequency(this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK].freq); } +#if !defined(RADIOLIB_EEPROM_UNSUPPORTED) + if(saveToEeprom) { + uint8_t payLen = MacTable[RADIOLIB_LORAWAN_MAC_RX_PARAM_SETUP].lenDn; + mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_RX_PARAM_SETUP_ID), &(cmd->payload[0]), payLen); + + } +#endif + // TODO this should be sent repeatedly until the next downlink - // send the reply cmd->len = 1; cmd->payload[0] = (rx1OffsAck << 2) | (rx2Ack << 1) | (chanAck << 0); RADIOLIB_DEBUG_PRINTLN("Rx param ANS: status = 0x%02x", cmd->payload[0]); - pushMacCommand(cmd, &this->commandsUp); - return(4); + return(true); } break; - case(RADIOLIB_LORAWAN_MAC_CMD_DEV_STATUS): { + case(RADIOLIB_LORAWAN_MAC_DEV_STATUS): { // set the uplink reply cmd->len = 2; cmd->payload[1] = this->battLevel; int8_t snr = this->phyLayer->getSNR(); cmd->payload[0] = snr & 0x3F; - // push it to the uplink queue RADIOLIB_DEBUG_PRINTLN("DevStatus ANS: status = 0x%02x%02x", cmd->payload[0], cmd->payload[1]); - pushMacCommand(cmd, &this->commandsUp); - return(0); + return(true); } break; - case(RADIOLIB_LORAWAN_MAC_CMD_NEW_CHANNEL): { + case(RADIOLIB_LORAWAN_MAC_NEW_CHANNEL): { // get the configuration uint8_t chIndex = cmd->payload[0]; uint32_t freqRaw = LoRaWANNode::ntoh(&cmd->payload[1], 3); @@ -2038,44 +2394,56 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { RADIOLIB_DEBUG_PRINTLN("New channel: index = %d, freq = %f MHz, maxDr = %d, minDr = %d", chIndex, freq, maxDr, minDr); uint8_t newChAck = 0; uint8_t freqAck = 0; - for(int i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { - // find first empty channel and configure this as the new channel - if(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx == RADIOLIB_LORAWAN_CHANNEL_INDEX_NONE) { - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = true; - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx = chIndex; - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].freq = freq; - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].drMin = minDr; - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].drMax = maxDr; - - // downlink channel is identical to uplink channel - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i] = this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i]; - newChAck = 1; - - // check if the frequency is possible - if(this->phyLayer->setFrequency(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].freq) == RADIOLIB_ERR_NONE) { - freqAck = 1; - this->phyLayer->setFrequency(this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK].freq); - } - break; - } + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chIndex].enabled = true; + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chIndex].idx = chIndex; + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chIndex].freq = freq; + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chIndex].drMin = minDr; + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chIndex].drMax = maxDr; + + // downlink channel is identical to uplink channel + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][chIndex] = this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chIndex]; + newChAck = 1; + + // check if the frequency is possible + if(this->phyLayer->setFrequency(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chIndex].freq) == RADIOLIB_ERR_NONE) { + freqAck = 1; + this->phyLayer->setFrequency(this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK].freq); } + + RADIOLIB_DEBUG_PRINTLN("UL: %d %d %5.2f (%d - %d) | DL: %d %d %5.2f (%d - %d)", + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chIndex].idx, + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chIndex].enabled, + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chIndex].freq, + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chIndex].drMin, + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chIndex].drMax, + + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][chIndex].idx, + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][chIndex].enabled, + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][chIndex].freq, + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][chIndex].drMin, + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][chIndex].drMax + ); #if !defined(RADIOLIB_EEPROM_UNSUPPORTED) - // update saved frequencies - this->saveChannels(); + if(saveToEeprom) { + // save to uplink channels location, to the chIndex-th slot of 5 bytes + uint8_t payLen = MacTable[RADIOLIB_LORAWAN_MAC_NEW_CHANNEL].lenDn; + RADIOLIB_DEBUG_PRINTLN("Saving channel:"); + RADIOLIB_DEBUG_HEXDUMP(&(cmd->payload[0]), payLen); + mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_UL_CHANNELS_ID) + chIndex * payLen, &(cmd->payload[0]), payLen); + + } #endif // send the reply cmd->len = 1; cmd->payload[0] = (newChAck << 1) | (freqAck << 0); - pushMacCommand(cmd, &this->commandsUp); - - return(5); + return(true); } break; - case(RADIOLIB_LORAWAN_MAC_CMD_DL_CHANNEL): { + case(RADIOLIB_LORAWAN_MAC_DL_CHANNEL): { // get the configuration uint8_t chIndex = cmd->payload[0]; uint32_t freqRaw = LoRaWANNode::ntoh(&cmd->payload[1], 3); @@ -2102,20 +2470,22 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { } #if !defined(RADIOLIB_EEPROM_UNSUPPORTED) - // update saved frequencies - this->saveChannels(); -#endif + if(saveToEeprom) { + // save to downlink channels location, to the chIndex-th slot of 4 bytes + uint8_t payLen = MacTable[RADIOLIB_LORAWAN_MAC_DL_CHANNEL].lenDn; + mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_DL_CHANNELS_ID) + chIndex * payLen, &(cmd->payload[0]), payLen); - // send the reply + } +#endif + + // TODO send this repeatedly until a downlink is received cmd->len = 1; cmd->payload[0] = (freqUlAck << 1) | (freqDlAck << 0); - pushMacCommand(cmd, &this->commandsUp); - - return(4); + return(true); } break; - case(RADIOLIB_LORAWAN_MAC_CMD_RX_TIMING_SETUP): { + case(RADIOLIB_LORAWAN_MAC_RX_TIMING_SETUP): { // get the configuration uint8_t delay = cmd->payload[0] & 0x0F; RADIOLIB_DEBUG_PRINTLN("RX timing: delay = %d sec", delay); @@ -2127,62 +2497,80 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { this->rxDelays[0] = delay * 1000; this->rxDelays[1] = this->rxDelays[0] + 1000; +#if !defined(RADIOLIB_EEPROM_UNSUPPORTED) + if(saveToEeprom) { + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_RX_TIMING_SETUP_ID, cmd->payload[0]); + + } +#endif + // send the reply cmd->len = 0; - // TODO this should be sent repeatedly until the next downlink - pushMacCommand(cmd, &this->commandsUp); - - return(1); + // TODO send this repeatedly until a downlink is received + return(true); } break; - case(RADIOLIB_LORAWAN_MAC_CMD_TX_PARAM_SETUP): { + case(RADIOLIB_LORAWAN_MAC_TX_PARAM_SETUP): { uint8_t dlDwell = (cmd->payload[0] & 0x20) >> 5; uint8_t ulDwell = (cmd->payload[0] & 0x10) >> 4; uint8_t maxEirpRaw = cmd->payload[0] & 0x0F; // who the f came up with this ... const uint8_t eirpEncoding[] = { 8, 10, 12, 13, 14, 16, 18, 20, 21, 24, 26, 27, 29, 30, 33, 36 }; - uint8_t maxEirp = eirpEncoding[maxEirpRaw]; - RADIOLIB_DEBUG_PRINTLN("TX timing: dlDwell = %d, dlDwell = %d, maxEirp = %d dBm", dlDwell, ulDwell, maxEirp); + this->txPowerMax = eirpEncoding[maxEirpRaw]; + RADIOLIB_DEBUG_PRINTLN("TX timing: dlDwell = %d, ulDwell = %d, maxEirp = %d dBm", dlDwell, ulDwell, this->txPowerMax); - // TODO implement this - (void)dlDwell; - (void)ulDwell; - (void)maxEirp; - return(1); + this->dwellTimeEnabledUp = ulDwell ? true : false; + this->dwellTimeUp = ulDwell ? 400 : 0; + + this->dwellTimeEnabledDn = dlDwell ? true : false; + this->dwellTimeDn = dlDwell ? 400 : 0; + +#if !defined(RADIOLIB_EEPROM_UNSUPPORTED) + if(saveToEeprom) { + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_TX_PARAM_SETUP_ID, cmd->payload[0]); + + } +#endif + + cmd->len = 0; + return(true); } break; - case(RADIOLIB_LORAWAN_MAC_CMD_REKEY): { + case(RADIOLIB_LORAWAN_MAC_REKEY): { // get the server version uint8_t srvVersion = cmd->payload[0]; RADIOLIB_DEBUG_PRINTLN("Server version: 1.%d", srvVersion); if((srvVersion > 0) && (srvVersion <= this->rev)) { // valid server version, stop sending the ReKey MAC command - deleteMacCommand(RADIOLIB_LORAWAN_MAC_CMD_REKEY, &this->commandsUp); + deleteMacCommand(RADIOLIB_LORAWAN_MAC_REKEY, &this->commandsUp); } - return(1); + return(false); } break; - case(RADIOLIB_LORAWAN_MAC_CMD_ADR_PARAM_SETUP): { + case(RADIOLIB_LORAWAN_MAC_ADR_PARAM_SETUP): { this->adrLimitExp = (cmd->payload[0] & 0xF0) >> 4; this->adrDelayExp = cmd->payload[0] & 0x0F; RADIOLIB_DEBUG_PRINTLN("ADR param setup: limitExp = %d, delayExp = %d", this->adrLimitExp, this->adrDelayExp); - return(1); +#if !defined(RADIOLIB_EEPROM_UNSUPPORTED) + if(saveToEeprom) { + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_ADR_PARAM_SETUP_ID, cmd->payload[0]); + + } +#endif + + cmd->len = 0; + return(true); } break; - case(RADIOLIB_LORAWAN_MAC_CMD_DEVICE_TIME): { - // TODO implement this - sent by gateway as reply to node request - uint32_t gpsEpoch = LoRaWANNode::ntoh(&cmd->payload[0]); - uint8_t fraction = cmd->payload[4]; - RADIOLIB_DEBUG_PRINTLN("Network time: gpsEpoch = %d s, delayExp = %f", gpsEpoch, (float)fraction/256.0f); - (void)gpsEpoch; - (void)fraction; - return(5); + case(RADIOLIB_LORAWAN_MAC_DEVICE_TIME): { + pushMacCommand(cmd, &this->commandsDown); + return(false); } break; - case(RADIOLIB_LORAWAN_MAC_CMD_FORCE_REJOIN): { + case(RADIOLIB_LORAWAN_MAC_FORCE_REJOIN): { // TODO implement this uint16_t rejoinReq = LoRaWANNode::ntoh(&cmd->payload[0]); uint8_t period = (rejoinReq & 0x3800) >> 11; @@ -2194,23 +2582,72 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { (void)maxRetries; (void)rejoinType; (void)dr; - return(2); + return(false); } break; - case(RADIOLIB_LORAWAN_MAC_CMD_REJOIN_PARAM_SETUP): { + case(RADIOLIB_LORAWAN_MAC_REJOIN_PARAM_SETUP): { // TODO implement this uint8_t maxTime = (cmd->payload[0] & 0xF0) >> 4; uint8_t maxCount = cmd->payload[0] & 0x0F; RADIOLIB_DEBUG_PRINTLN("Rejoin setup: maxTime = %d, maxCount = %d", maxTime, maxCount); + +#if !defined(RADIOLIB_EEPROM_UNSUPPORTED) + if(saveToEeprom) { + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_REJOIN_PARAM_SETUP_ID, cmd->payload[0]); + + } +#endif + + cmd->len = 0; + cmd->payload[0] = (1 << 1) | 1; + (void)maxTime; (void)maxCount; - return(0); + return(true); } break; } - return(0); + return(false); +} + +uint8_t LoRaWANNode::getMacPayloadLength(uint8_t cid) { + for (LoRaWANMacSpec_t entry : MacTable) { + if (entry.cid == cid) { + return entry.lenDn; + } + } + // no idea about the length + return 0; +} + +bool LoRaWANNode::getMacLinkCheckAns(uint8_t* margin, uint8_t* gwCnt) { + uint8_t payload[5]; + int16_t state = deleteMacCommand(RADIOLIB_LORAWAN_LINK_CHECK_REQ, &this->commandsDown, payload); + if(state != RADIOLIB_ERR_NONE) + return false; + + *margin = payload[0]; + *gwCnt = payload[1]; + RADIOLIB_DEBUG_PRINTLN("Link check: margin = %d dB, gwCnt = %d", margin, gwCnt); + return(true); } +bool LoRaWANNode::getMacDeviceTimeAns(uint32_t* gpsEpoch, uint8_t* fraction, bool returnUnix) { + uint8_t payload[5]; + int16_t state = deleteMacCommand(RADIOLIB_LORAWAN_MAC_DEVICE_TIME, &this->commandsDown, payload); + if(state != RADIOLIB_ERR_NONE) + return false; + + *gpsEpoch = LoRaWANNode::ntoh(&payload[0]); + *fraction = payload[4]; + RADIOLIB_DEBUG_PRINTLN("Network time: gpsEpoch = %d s, delayExp = %f", gpsEpoch, (float)(*fraction)/256.0f); + + uint32_t unixOffset = 315964800; + *gpsEpoch += unixOffset; + return(true); +} + + // The following function enables LMAC, a CSMA scheme for LoRa as specified // in the LoRa Alliance Technical Recommendation #13. // A user may enable CSMA to provide frames an additional layer of protection from interference. diff --git a/src/protocols/LoRaWAN/LoRaWAN.h b/src/protocols/LoRaWAN/LoRaWAN.h index 47be7cb35..81e87dbd6 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.h +++ b/src/protocols/LoRaWAN/LoRaWAN.h @@ -5,8 +5,15 @@ #include "../PhysicalLayer/PhysicalLayer.h" #include "../../utils/Cryptography.h" -// version of NVM table layout (NOT the LoRaWAN version) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_TABLE_VERSION (0x01) +// activation mode +#define RADIOLIB_LORAWAN_MODE_OTAA (0x01AA) +#define RADIOLIB_LORAWAN_MODE_ABP (0x0AB9) +#define RADIOLIB_LORAWAN_MODE_NONE (0x0000) + +// operation mode +#define RADIOLIB_LORAWAN_CLASS_A (0x00) +#define RADIOLIB_LORAWAN_CLASS_B (0x01) +#define RADIOLIB_LORAWAN_CLASS_C (0x02) // preamble format #define RADIOLIB_LORAWAN_LORA_SYNC_WORD (0x34) @@ -77,7 +84,6 @@ // recommended default settings #define RADIOLIB_LORAWAN_RECEIVE_DELAY_1_MS (1000) #define RADIOLIB_LORAWAN_RECEIVE_DELAY_2_MS ((RADIOLIB_LORAWAN_RECEIVE_DELAY_1_MS) + 1000) -#define RADIOLIB_LORAWAN_RX_WINDOW_LEN_MS (500) #define RADIOLIB_LORAWAN_RX1_DR_OFFSET (0) #define RADIOLIB_LORAWAN_JOIN_ACCEPT_DELAY_1_MS (5000) #define RADIOLIB_LORAWAN_JOIN_ACCEPT_DELAY_2_MS (6000) @@ -87,6 +93,8 @@ #define RADIOLIB_LORAWAN_RETRANSMIT_TIMEOUT_MIN_MS (1000) #define RADIOLIB_LORAWAN_RETRANSMIT_TIMEOUT_MAX_MS (3000) #define RADIOLIB_LORAWAN_POWER_STEP_SIZE_DBM (-2) +#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 // join request message layout #define RADIOLIB_LORAWAN_JOIN_REQUEST_LEN (23) @@ -155,32 +163,61 @@ #define RADIOLIB_LORAWAN_MAGIC (0x39EA) // MAC commands -#define RADIOLIB_LORAWAN_MAC_CMD_RESET (0x01) -#define RADIOLIB_LORAWAN_MAC_CMD_LINK_CHECK (0x02) -#define RADIOLIB_LORAWAN_MAC_CMD_LINK_ADR (0x03) -#define RADIOLIB_LORAWAN_MAC_CMD_DUTY_CYCLE (0x04) -#define RADIOLIB_LORAWAN_MAC_CMD_RX_PARAM_SETUP (0x05) -#define RADIOLIB_LORAWAN_MAC_CMD_DEV_STATUS (0x06) -#define RADIOLIB_LORAWAN_MAC_CMD_NEW_CHANNEL (0x07) -#define RADIOLIB_LORAWAN_MAC_CMD_RX_TIMING_SETUP (0x08) -#define RADIOLIB_LORAWAN_MAC_CMD_TX_PARAM_SETUP (0x09) -#define RADIOLIB_LORAWAN_MAC_CMD_DL_CHANNEL (0x0A) -#define RADIOLIB_LORAWAN_MAC_CMD_REKEY (0x0B) -#define RADIOLIB_LORAWAN_MAC_CMD_ADR_PARAM_SETUP (0x0C) -#define RADIOLIB_LORAWAN_MAC_CMD_DEVICE_TIME (0x0D) -#define RADIOLIB_LORAWAN_MAC_CMD_FORCE_REJOIN (0x0E) -#define RADIOLIB_LORAWAN_MAC_CMD_REJOIN_PARAM_SETUP (0x0F) -#define RADIOLIB_LORAWAN_MAC_CMD_PROPRIETARY (0x80) +#define RADIOLIB_LORAWAN_NUM_MAC_COMMANDS (16) + +#define RADIOLIB_LORAWAN_MAC_RESET (0x01) +#define RADIOLIB_LORAWAN_MAC_LINK_CHECK (0x02) +#define RADIOLIB_LORAWAN_MAC_LINK_ADR (0x03) +#define RADIOLIB_LORAWAN_MAC_DUTY_CYCLE (0x04) +#define RADIOLIB_LORAWAN_MAC_RX_PARAM_SETUP (0x05) +#define RADIOLIB_LORAWAN_MAC_DEV_STATUS (0x06) +#define RADIOLIB_LORAWAN_MAC_NEW_CHANNEL (0x07) +#define RADIOLIB_LORAWAN_MAC_RX_TIMING_SETUP (0x08) +#define RADIOLIB_LORAWAN_MAC_TX_PARAM_SETUP (0x09) +#define RADIOLIB_LORAWAN_MAC_DL_CHANNEL (0x0A) +#define RADIOLIB_LORAWAN_MAC_REKEY (0x0B) +#define RADIOLIB_LORAWAN_MAC_ADR_PARAM_SETUP (0x0C) +#define RADIOLIB_LORAWAN_MAC_DEVICE_TIME (0x0D) +#define RADIOLIB_LORAWAN_MAC_FORCE_REJOIN (0x0E) +#define RADIOLIB_LORAWAN_MAC_REJOIN_PARAM_SETUP (0x0F) +#define RADIOLIB_LORAWAN_MAC_PROPRIETARY (0x80) // unused frame counter value #define RADIOLIB_LORAWAN_FCNT_NONE (0xFFFFFFFF) // the length of internal MAC command queue - hopefully this is enough for most use cases -#define RADIOLIB_LORAWAN_MAC_COMMAND_QUEUE_SIZE (8) +#define RADIOLIB_LORAWAN_MAC_COMMAND_QUEUE_SIZE (9) // the maximum number of simultaneously available channels #define RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS (16) +struct LoRaWANMacSpec_t { + const uint8_t cid; + const uint8_t lenDn; + const uint8_t lenUp; + const bool user; // whether this MAC command can be issued by a user or not +}; + +const LoRaWANMacSpec_t MacTable[RADIOLIB_LORAWAN_NUM_MAC_COMMANDS + 1] = { + { 0x00, 0, 0, false }, // not an actual MAC command, exists for offsetting + { 0x01, 1, 1, false }, // RADIOLIB_LORAWAN_MAC_RESET + { 0x02, 2, 0, true }, // RADIOLIB_LORAWAN_MAC_LINK_CHECK + { 0x03, 4, 1, false }, // RADIOLIB_LORAWAN_MAC_LINK_ADR + { 0x04, 1, 0, false }, // RADIOLIB_LORAWAN_MAC_DUTY_CYCLE + { 0x05, 4, 1, false }, // RADIOLIB_LORAWAN_MAC_RX_PARAM_SETUP + { 0x06, 0, 2, false }, // RADIOLIB_LORAWAN_MAC_DEV_STATUS + { 0x07, 5, 1, false }, // RADIOLIB_LORAWAN_MAC_NEW_CHANNEL + { 0x08, 1, 0, false }, // RADIOLIB_LORAWAN_MAC_RX_TIMING_SETUP + { 0x09, 1, 0, false }, // RADIOLIB_LORAWAN_MAC_TX_PARAM_SETUP + { 0x0A, 4, 1, false }, // RADIOLIB_LORAWAN_MAC_DL_CHANNEL + { 0x0B, 1, 1, false }, // RADIOLIB_LORAWAN_MAC_REKEY + { 0x0C, 1, 0, false }, // RADIOLIB_LORAWAN_MAC_ADR_PARAM_SETUP + { 0x0D, 5, 0, true }, // RADIOLIB_LORAWAN_MAC_DEVICE_TIME + { 0x0E, 2, 0, false }, // RADIOLIB_LORAWAN_MAC_FORCE_REJOIN + { 0x0F, 1, 1, false }, // RADIOLIB_LORAWAN_MAC_REJOIN_PARAM_SETUP + { 0x80, 5, 0, true } // RADIOLIB_LORAWAN_MAC_PROPRIETARY +}; + /*! \struct LoRaWANChannelSpan_t \brief Structure to save information about LoRaWAN channels. @@ -251,6 +288,13 @@ struct LoRaWANBand_t { /*! \brief Number of power steps in this band */ int8_t powerNumSteps; + /*! \brief Number of milliseconds per hour of allowed Time-on-Air */ + uint32_t dutyCycle; + + /*! \brief Maximum dwell time per message in milliseconds */ + uint32_t dwellTimeUp; + uint32_t dwellTimeDn; + /*! \brief A set of default uplink (TX) channels for frequency-type bands */ LoRaWANChannel_t txFreqs[3]; @@ -371,7 +415,8 @@ class LoRaWANNode { /*! \brief Restore session by loading information from persistent storage. - \returns \ref status_codes + \returns \ref status_codes in case of error, + else LoRaWAN session mode (0 = no active session, 0xAA / 170 = OTAA, 0xAB / 171 = ABP) */ int16_t restore(); #endif @@ -413,6 +458,15 @@ class LoRaWANNode { */ int16_t saveSession(); + /*! + \brief Add a MAC command to the uplink queue. + Only LinkCheck and DeviceTime are available to the user. + Other commands are ignored; duplicate MAC commands are discarded. + \param cid ID of the MAC command + \returns Whether or not the MAC command was added to the queue. + */ + bool sendMacCommandReq(uint8_t cid); + #if defined(RADIOLIB_BUILD_ARDUINO) /*! \brief Send a message to the server. @@ -533,6 +587,12 @@ class LoRaWANNode { /*! \brief Returns the last application downlink's frame counter */ uint32_t getAFcntDown(); + /*! \brief Reset the downlink frame counters (application and network) + This is unsafe and can possibly allow replay attacks using downlinks. + It mainly exists as part of the TS008 Specification Verification protocol. + */ + void resetFcntDown(); + /*! \brief Set uplink datarate. This should not be used when ADR is enabled. \param dr Datarate to use for uplinks. @@ -546,6 +606,41 @@ class LoRaWANNode { */ void setADR(bool enable = true); + /*! + \brief Toggle adherence to dutyCycle limits to on or off. + \param enable Whether to adhere to dutyCycle limits or not (default true). + \param msPerHour The maximum allowed Time-on-Air per hour in milliseconds + (default 0 = maximum allowed for configured band). + */ + void setDutyCycle(bool enable = true, uint32_t msPerHour = 0); + + /*! + \brief Calculate the minimum interval to adhere to a certain dutyCycle. + This interval is based on the ToA of one uplink and does not actually keep track of total airtime. + \param msPerHour The maximum allowed dutycyle (in milliseconds per hour). + \param airtime The airtime of the uplink. + \returns Required interval (delay) in milliseconds between consecutive uplinks. + */ + uint32_t dutyCycleInterval(uint32_t msPerHour, uint32_t airtime); + + /*! \brief Returns time in milliseconds until next uplink is available under dutyCycle limits */ + uint32_t timeUntilUplink(); + + /*! + \brief Toggle adherence to dwellTime limits to on or off. + \param enable Whether to adhere to dwellTime limits or not (default true). + \param msPerHour The maximum allowed Time-on-Air per uplink in milliseconds + (default 0 = maximum allowed for configured band). + */ + void setDwellTime(bool enable, uint32_t msPerUplink = 0); + + /*! + \brief Returns the maximum payload given the currently present dwelltime limits. + WARNING: the addition of MAC commands may cause uplink errors; + if you want to be sure that your payload fits within dwelltime limits, subtract 16 from the result! + */ + uint8_t maxPayloadDwellTime(); + /*! \brief Configure TX power of the radio module. \param txPower Output power during TX mode to be set in dBm. @@ -578,12 +673,35 @@ class LoRaWANNode { */ void setCSMA(uint8_t backoffMax, uint8_t difsSlots, bool enableCSMA = false); + /*! + \brief Returns the quality of connectivity after requesting a LinkCheck MAC command. + Returns 'true' if a network response was succesfully parsed. + Returns 'false' if there was no network response / parsing failed. + \param margin Link margin in dB of LinkCheckReq demodulation at gateway side. + \param gwCnt Number of gateways that received the LinkCheckReq. + \returns Whether the parameters where succesfully parsed. + */ + bool getMacLinkCheckAns(uint8_t* margin, uint8_t* gwCnt); + + /*! + \brief Returns the network time after requesting a DeviceTime MAC command. + Returns 'true' if a network response was succesfully parsed. + Returns 'false' if there was no network response / parsing failed. + \param gpsEpoch Number of seconds since GPS epoch (Jan. 6th 1980) + \param fraction Fractional-second, in 1/256-second steps + \param returnUnix If true, returns Unix timestamp instead of GPS (default true) + \returns Whether the parameters where succesfully parsed. + */ + bool getMacDeviceTimeAns(uint32_t* gpsEpoch, uint8_t* fraction, bool returnUnix = true); + #if !RADIOLIB_GODMODE private: #endif PhysicalLayer* phyLayer = NULL; const LoRaWANBand_t* band = NULL; + void beginCommon(); + LoRaWANMacCommandQueue_t commandsUp = { .numCommands = 0, .len = 0, @@ -613,7 +731,8 @@ class LoRaWANNode { uint8_t adrLimitExp = RADIOLIB_LORAWAN_ADR_ACK_LIMIT_EXP; uint8_t adrDelayExp = RADIOLIB_LORAWAN_ADR_ACK_DELAY_EXP; uint8_t nbTrans = 1; // Number of allowed frame retransmissions - uint8_t txPwrCur = 0; + uint8_t txPowerCur = 0; + uint8_t txPowerMax = 0; uint32_t fcntUp = 0; uint32_t aFcntDown = 0; uint32_t nFcntDown = 0; @@ -624,11 +743,21 @@ class LoRaWANNode { // whether the current configured channel is in FSK mode bool FSK = false; - // flag that shows whether the device is joined and there is an ongoing session - bool isJoinedFlag = false; + // flag that shows whether the device is joined and there is an ongoing session (none, ABP or OTAA) + uint16_t activeMode = 0; // ADR is enabled by default bool adrEnabled = true; + + // duty cycle is set upon initialization and activated in regions that impose this + bool dutyCycleEnabled = false; + uint32_t dutyCycle = 0; + + // dwell time is set upon initialization and activated in regions that impose this + bool dwellTimeEnabledUp = false; + uint16_t dwellTimeUp = 0; + bool dwellTimeEnabledDn = false; + uint16_t dwellTimeDn = 0; // enable/disable CSMA for LoRaWAN bool enableCSMA; @@ -653,6 +782,9 @@ class LoRaWANNode { // LoRaWAN revision (1.0 vs 1.1) uint8_t rev = 0; + // Time on Air of last uplink + uint32_t lastToA = 0; + // timestamp to measure the RX1/2 delay (from uplink end) uint32_t rxDelayStart = 0; @@ -714,9 +846,6 @@ class LoRaWANNode { // configure channel based on cached data rate ID and frequency int16_t configureChannel(uint8_t dir); - // save all available channels to persistent storage - int16_t saveChannels(); - // restore all available channels from persistent storage int16_t restoreChannels(); @@ -724,10 +853,14 @@ class LoRaWANNode { int16_t pushMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQueue_t* queue); // delete a specific MAC command from queue, indicated by the command ID - int16_t deleteMacCommand(uint8_t cid, LoRaWANMacCommandQueue_t* queue); + // if a payload pointer is supplied, this returns the payload of the MAC command + int16_t deleteMacCommand(uint8_t cid, LoRaWANMacCommandQueue_t* queue, uint8_t payload[5] = NULL); // execute mac command, return the number of processed bytes for sequential processing - size_t execMacCommand(LoRaWANMacCommand_t* cmd); + bool execMacCommand(LoRaWANMacCommand_t* cmd, bool saveToEeprom = true); + + // get the payload length for a specific MAC command + uint8_t getMacPayloadLength(uint8_t cid); // Performs CSMA as per LoRa Alliance Technical Reccomendation 13 (TR-013). void performCSMA(); diff --git a/src/protocols/LoRaWAN/LoRaWANBands.cpp b/src/protocols/LoRaWAN/LoRaWANBands.cpp index 7b0e04cf0..54580b0ee 100644 --- a/src/protocols/LoRaWAN/LoRaWANBands.cpp +++ b/src/protocols/LoRaWAN/LoRaWANBands.cpp @@ -7,6 +7,9 @@ const LoRaWANBand_t EU868 = { .payloadLenMax = { 59, 59, 59, 123, 230, 230, 230, 230, 0, 0, 0, 0, 0, 0, 0 }, .powerMax = 16, .powerNumSteps = 7, + .dutyCycle = 36000, + .dwellTimeUp = 0, + .dwellTimeDn = 0, .txFreqs = { { .enabled = true, .idx = 0, .freq = 868.100, .drMin = 0, .drMax = 5}, { .enabled = true, .idx = 1, .freq = 868.300, .drMin = 0, .drMax = 5}, @@ -49,6 +52,9 @@ const LoRaWANBand_t US915 = { .payloadLenMax = { 19, 61, 133, 250, 250, 0, 0, 0, 41, 117, 230, 230, 230, 230, 0 }, .powerMax = 30, .powerNumSteps = 10, + .dutyCycle = 0, + .dwellTimeUp = 400, + .dwellTimeDn = 0, .txFreqs = { RADIOLIB_LORAWAN_CHANNEL_NONE, RADIOLIB_LORAWAN_CHANNEL_NONE, @@ -112,6 +118,9 @@ const LoRaWANBand_t CN780 = { .payloadLenMax = { 59, 59, 59, 123, 230, 230, 250, 230, 0, 0, 0, 0, 0, 0, 0 }, .powerMax = 12, .powerNumSteps = 5, + .dutyCycle = 3600, + .dwellTimeUp = 0, + .dwellTimeDn = 0, .txFreqs = { { .enabled = true, .idx = 0, .freq = 779.500, .drMin = 0, .drMax = 5}, { .enabled = true, .idx = 1, .freq = 779.700, .drMin = 0, .drMax = 5}, @@ -154,6 +163,9 @@ const LoRaWANBand_t EU433 = { .payloadLenMax = { 59, 59, 59, 123, 230, 230, 230, 230, 0, 0, 0, 0, 0, 0, 0 }, .powerMax = 12, .powerNumSteps = 5, + .dutyCycle = 36000, + .dwellTimeUp = 0, + .dwellTimeDn = 0, .txFreqs = { { .enabled = true, .idx = 0, .freq = 433.175, .drMin = 0, .drMax = 5}, { .enabled = true, .idx = 1, .freq = 433.375, .drMin = 0, .drMax = 5}, @@ -196,6 +208,9 @@ const LoRaWANBand_t AU915 = { .payloadLenMax = { 59, 59, 59, 123, 230, 230, 230, 0, 41, 117, 230, 230, 230, 230, 0 }, .powerMax = 30, .powerNumSteps = 10, + .dutyCycle = 0, + .dwellTimeUp = 0, + .dwellTimeDn = 0, .txFreqs = { RADIOLIB_LORAWAN_CHANNEL_NONE, RADIOLIB_LORAWAN_CHANNEL_NONE, @@ -259,6 +274,9 @@ const LoRaWANBand_t CN500 = { .payloadLenMax = { 59, 59, 59, 123, 230, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, .powerMax = 19, .powerNumSteps = 7, + .dutyCycle = 0, + .dwellTimeUp = 0, + .dwellTimeDn = 0, .txFreqs = { RADIOLIB_LORAWAN_CHANNEL_NONE, RADIOLIB_LORAWAN_CHANNEL_NONE, @@ -315,6 +333,9 @@ const LoRaWANBand_t AS923 = { .payloadLenMax = { 59, 59, 59, 123, 230, 230, 230, 230, 0, 0, 0, 0, 0, 0, 0 }, .powerMax = 16, .powerNumSteps = 7, + .dutyCycle = 36000, + .dwellTimeUp = 400, + .dwellTimeDn = 400, .txFreqs = { { .enabled = true, .idx = 0, .freq = 923.200, .drMin = 0, .drMax = 5}, { .enabled = true, .idx = 1, .freq = 923.400, .drMin = 0, .drMax = 5}, @@ -357,6 +378,9 @@ const LoRaWANBand_t KR920 = { .payloadLenMax = { 59, 59, 59, 123, 230, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, .powerMax = 14, .powerNumSteps = 7, + .dutyCycle = 0, + .dwellTimeUp = 0, + .dwellTimeDn = 0, .txFreqs = { { .enabled = true, .idx = 0, .freq = 922.100, .drMin = 0, .drMax = 5}, { .enabled = true, .idx = 1, .freq = 922.300, .drMin = 0, .drMax = 5}, @@ -399,6 +423,9 @@ const LoRaWANBand_t IN865 = { .payloadLenMax = { 59, 59, 59, 123, 230, 230, 230, 230, 0, 0, 0, 0, 0, 0, 0 }, .powerMax = 30, .powerNumSteps = 10, + .dutyCycle = 0, + .dwellTimeUp = 0, + .dwellTimeDn = 0, .txFreqs = { { .enabled = true, .idx = 0, .freq = 865.0625, .drMin = 0, .drMax = 5}, { .enabled = true, .idx = 1, .freq = 865.4025, .drMin = 0, .drMax = 5}, From 853c1731ca241b7a9345e1ef3fbe4c7babc693aa Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Fri, 5 Jan 2024 11:36:01 +0100 Subject: [PATCH 2/9] [LoRaWAN] Fix warnings --- src/protocols/LoRaWAN/LoRaWAN.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index e7217f4e9..49c90a494 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -307,7 +307,7 @@ int16_t LoRaWANNode::restoreChannels() { #endif void LoRaWANNode::beginCommon() { - LoRaWANMacCommand_t cmd; + LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 }; cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR; cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; cmd.payload[0] = (this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] << 4); @@ -1449,11 +1449,9 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len, LoRaWANEvent_t* event) } RADIOLIB_DEBUG_PRINTLN("MAC response:"); - for (int x; x < this->commandsUp.numCommands; x++) { - LoRaWANMacCommand_t cmd = this->commandsUp.commands[x]; - RADIOLIB_DEBUG_PRINTLN("[%02x] %02x %02x %02x %02x %02x (%02x)", cmd.cid, - cmd.payload[0], cmd.payload[1], cmd.payload[2], cmd.payload[3], cmd.payload[4], cmd.len); - } + for (int i = 0; i < this->commandsUp.numCommands; i++) { + RADIOLIB_DEBUG_HEXDUMP(&this->commandsUp[i].cid, sizeof(LoRaWANMacCommand_t)); + } // if FOptsLen for the next uplink is larger than can be piggybacked onto an uplink, send separate uplink if(this->commandsUp.len > 15) { @@ -1972,8 +1970,10 @@ uint32_t LoRaWANNode::dutyCycleInterval(uint32_t msPerHour, uint32_t airtime) { uint32_t LoRaWANNode::timeUntilUplink() { Module* mod = this->phyLayer->getMod(); uint32_t nextUplink = this->rxDelayStart + dutyCycleInterval(this->dutyCycle, this->lastToA); - uint32_t timeRemaining = RADIOLIB_MAX(0, nextUplink - mod->hal->millis() + 1); - return(timeRemaining); + if(mod->hal->millis() > nextUplink){ + return(0); + } + return(nextUplink - mod->hal->millis() + 1); } void LoRaWANNode::setDwellTime(bool enable, uint32_t msPerUplink) { From 00264f48a90ad82a37887b129d63154e3f631a17 Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Fri, 5 Jan 2024 11:49:59 +0100 Subject: [PATCH 3/9] [HAL] Fix a comment --- src/Hal.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Hal.h b/src/Hal.h index 20d858ff7..fbc8d5f75 100644 --- a/src/Hal.h +++ b/src/Hal.h @@ -71,7 +71,7 @@ static const uint32_t RadioLibPersistentParamTable[] = { 0x70, // RADIOLIB_EEPROM_LORAWAN_RJ_COUNT0_ID 0x72, // RADIOLIB_EEPROM_LORAWAN_RJ_COUNT1_ID 0x74, // RADIOLIB_EEPROM_LORAWAN_FCNT_UP_ID - 0xA0, // RADIOLIB_EEPROM_LORAWAN_LINK_ADR_ID - NOTE change upon ADR Ack Req + 0xA0, // RADIOLIB_EEPROM_LORAWAN_LINK_ADR_ID 0xA4, // RADIOLIB_EEPROM_LORAWAN_DUTY_CYCLE_ID 0xA5, // RADIOLIB_EEPROM_LORAWAN_RX_PARAM_SETUP_ID 0xA9, // RADIOLIB_EEPROM_LORAWAN_RX_TIMING_SETUP_ID From f7730463bd40d03547751875a02feed4229e93c0 Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Fri, 5 Jan 2024 13:41:00 +0100 Subject: [PATCH 4/9] [LoRaWAN] Fix ADR bug if not enabled --- src/protocols/LoRaWAN/LoRaWAN.cpp | 78 ++++++++++++++++--------------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index 49c90a494..43fe0514a 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -912,52 +912,54 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf // increase frame counter by one this->fcntUp += 1; - // check if we need to do ADR stuff - uint32_t adrLimit = 0x01 << this->adrLimitExp; - uint32_t adrDelay = 0x01 << this->adrDelayExp; bool adrAckReq = false; - if((this->fcntUp - this->adrFcnt) >= adrLimit) { - adrAckReq = true; - } - // if we hit the Limit + Delay, try one of three, in order: - // set TxPower to max, set DR to min, enable all defined channels - if ((this->fcntUp - this->adrFcnt) == (adrLimit + adrDelay)) { - - // if the TxPower field has some offset, remove it and switch to maximum power - if(this->txPowerCur > 0) { - this->txPowerCur = 0; - // set the maximum power supported by both the module and the band - state = this->setTxPower(this->txPowerMax); - RADIOLIB_ASSERT(state); - - } else { - // failed to increase Tx power, so try to decrease the datarate - if(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] > this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK].drMin) { - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]--; - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK]--; + if(this->adrEnabled) { + // check if we need to do ADR stuff + uint32_t adrLimit = 0x01 << this->adrLimitExp; + uint32_t adrDelay = 0x01 << this->adrDelayExp; + if((this->fcntUp - this->adrFcnt) >= adrLimit) { + adrAckReq = true; + } + // if we hit the Limit + Delay, try one of three, in order: + // set TxPower to max, set DR to min, enable all defined channels + if ((this->fcntUp - this->adrFcnt) == (adrLimit + adrDelay)) { + + // if the TxPower field has some offset, remove it and switch to maximum power + if(this->txPowerCur > 0) { + this->txPowerCur = 0; + // set the maximum power supported by both the module and the band + state = this->setTxPower(this->txPowerMax); + RADIOLIB_ASSERT(state); + } else { + // failed to increase Tx power, so try to decrease the datarate + if(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] > this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK].drMin) { + this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]--; + this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK]--; + } else { - // failed to decrease datarate, so enable all available channels - for(size_t i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { - if(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx != RADIOLIB_LORAWAN_CHANNEL_INDEX_NONE) { - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = true; - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].enabled = true; + // failed to decrease datarate, so enable all available channels + for(size_t i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { + if(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx != RADIOLIB_LORAWAN_CHANNEL_INDEX_NONE) { + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = true; + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].enabled = true; + } } } - } - } + } - LoRaWANMacCommand_t cmd; - cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR; - cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; - cmd.payload[0] = (this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] << 4); - cmd.payload[0] |= 0; // default to max Tx Power - cmd.payload[3] |= (1 << 7); // set the RFU bit, which means that the channel mask gets ignored - (void)execMacCommand(&cmd); + LoRaWANMacCommand_t cmd; + cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; + cmd.payload[0] = (this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] << 4); + cmd.payload[0] |= 0; // default to max Tx Power + cmd.payload[3] |= (1 << 7); // set the RFU bit, which means that the channel mask gets ignored + (void)execMacCommand(&cmd); - // we tried something to improve the range, so increase the ADR frame counter by 'ADR delay' - this->adrFcnt += adrDelay; + // we tried something to improve the range, so increase the ADR frame counter by 'ADR delay' + this->adrFcnt += adrDelay; + } } // configure for uplink From 2da09b5adcd67297dacd33db94e40506902bda2c Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Sat, 6 Jan 2024 15:03:55 +0100 Subject: [PATCH 5/9] [LoRaWAN] Convert setDatarate() and setTxPower() to internal MAC; improve ADR --- .../LoRaWAN_End_Device_Reference.ino | 4 + src/protocols/LoRaWAN/LoRaWAN.cpp | 171 ++++++++++++------ src/protocols/LoRaWAN/LoRaWAN.h | 9 +- 3 files changed, 122 insertions(+), 62 deletions(-) diff --git a/examples/LoRaWAN/LoRaWAN_End_Device_Reference/LoRaWAN_End_Device_Reference.ino b/examples/LoRaWAN/LoRaWAN_End_Device_Reference/LoRaWAN_End_Device_Reference.ino index 52d5904f2..2d8c84cf1 100644 --- a/examples/LoRaWAN/LoRaWAN_End_Device_Reference/LoRaWAN_End_Device_Reference.ino +++ b/examples/LoRaWAN/LoRaWAN_End_Device_Reference/LoRaWAN_End_Device_Reference.ino @@ -129,6 +129,10 @@ void setup() { // set a fixed datarate node.setDatarate(5); + // in order to save the datarate persistent across reboot/deepsleep, use the following: + /* + node.setDatarate(5, true); + */ // enable CSMA // this tries to minimize packet loss by searching for a free channel diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index 43fe0514a..56fd5b639 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -151,7 +151,7 @@ int16_t LoRaWANNode::restore() { RADIOLIB_ASSERT(state); // get MAC state - LoRaWANMacCommand_t cmd; + LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 }; cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR; cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_LINK_ADR_ID), cmd.payload, cmd.len); @@ -260,7 +260,7 @@ int16_t LoRaWANNode::restoreChannels() { uint8_t bufferUp[numBytesUp] = { 0 }; mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_UL_CHANNELS_ID), bufferUp, numBytesUp); - LoRaWANMacCommand_t cmd; + LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 }; cmd.cid = RADIOLIB_LORAWAN_MAC_NEW_CHANNEL; for(int i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { @@ -276,9 +276,9 @@ int16_t LoRaWANNode::restoreChannels() { mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_DL_CHANNELS_ID), bufferDn, numBytesDn); cmd.cid = RADIOLIB_LORAWAN_MAC_DL_CHANNEL; - cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_DL_CHANNEL].lenDn; for(int i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_DL_CHANNEL].lenDn; memcpy(cmd.payload, &bufferDn[i * cmd.len], cmd.len); if(memcmp(cmd.payload, bufferZeroes, cmd.len) != 0) { // only execute if it is not all zeroes (void)execMacCommand(&cmd, false); @@ -290,7 +290,7 @@ int16_t LoRaWANNode::restoreChannels() { uint8_t buffer[numBytes] = { 0 }; mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_UL_CHANNELS_ID), buffer, numBytes); - LoRaWANMacCommand_t cmd; + LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 }; cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR; cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; @@ -552,7 +552,7 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe } - LoRaWANMacCommand_t cmd; + LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 }; cmd.cid = RADIOLIB_LORAWAN_MAC_RX_PARAM_SETUP; cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_RX_PARAM_SETUP].lenDn; cmd.payload[0] = dlSettings & 0x7F; @@ -921,42 +921,54 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf adrAckReq = true; } // if we hit the Limit + Delay, try one of three, in order: - // set TxPower to max, set DR to min, enable all defined channels + // set TxPower to max, set DR to min, enable all default channels if ((this->fcntUp - this->adrFcnt) == (adrLimit + adrDelay)) { - - // if the TxPower field has some offset, remove it and switch to maximum power - if(this->txPowerCur > 0) { - this->txPowerCur = 0; - // set the maximum power supported by both the module and the band - state = this->setTxPower(this->txPowerMax); - RADIOLIB_ASSERT(state); - - } else { - // failed to increase Tx power, so try to decrease the datarate - if(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] > this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK].drMin) { - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]--; - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK]--; - } else { - - // failed to decrease datarate, so enable all available channels - for(size_t i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { - if(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx != RADIOLIB_LORAWAN_CHANNEL_INDEX_NONE) { - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = true; - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].enabled = true; + uint8_t adrStage = 1; + while(adrStage != 0) { + switch(adrStage) { + case(1): { + // if the TxPower field has some offset, remove it and switch to maximum power + if(this->txPowerCur > 0) { + // set the maximum power supported by both the module and the band + state = this->setTxPower(this->txPowerMax, true); + if(state == RADIOLIB_ERR_NONE) { + this->txPowerCur = 0; + adrStage = 0; // successfully did some ADR stuff + } + } + if(adrStage == 1) { // if nothing succeeded, proceed to stage 2 + adrStage = 2; + } } - } + break; + case(2): { + // try to decrease the datarate + if(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] > 0) { + if(this->setDatarate(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] - 1, true) == RADIOLIB_ERR_NONE) { + adrStage = 0; // successfully did some ADR stuff + } + } + if(adrStage == 2) { // if nothing succeeded, proceed to stage 3 + adrStage = 3; + } + } + break; + case(3): { + if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) { + this->setupChannels(nullptr); // revert to default frequencies + } else { + // if a subband was selected by user, go back to its default state + // hopefully it'll help something, but probably not; at least we tried.. + if(this->selectedSubband >= 0) { + this->selectSubband(this->selectedSubband); + } + } + adrStage = 0; // nothing else to do, so end the cycle + } + break; } - } - LoRaWANMacCommand_t cmd; - cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR; - cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; - cmd.payload[0] = (this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] << 4); - cmd.payload[0] |= 0; // default to max Tx Power - cmd.payload[3] |= (1 << 7); // set the RFU bit, which means that the channel mask gets ignored - (void)execMacCommand(&cmd); - // we tried something to improve the range, so increase the ADR frame counter by 'ADR delay' this->adrFcnt += adrDelay; } @@ -1452,7 +1464,7 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len, LoRaWANEvent_t* event) RADIOLIB_DEBUG_PRINTLN("MAC response:"); for (int i = 0; i < this->commandsUp.numCommands; i++) { - RADIOLIB_DEBUG_HEXDUMP(&this->commandsUp[i].cid, sizeof(LoRaWANMacCommand_t)); + RADIOLIB_DEBUG_HEXDUMP(&(this->commandsUp.commands[i].cid), sizeof(LoRaWANMacCommand_t)); } // if FOptsLen for the next uplink is larger than can be piggybacked onto an uplink, send separate uplink @@ -1638,7 +1650,11 @@ bool LoRaWANNode::verifyMIC(uint8_t* msg, size_t len, uint8_t* key) { int16_t LoRaWANNode::setPhyProperties() { // set the physical layer configuration int8_t pwr = this->txPowerMax - this->txPowerCur * 2; - int16_t state = this->setTxPower(pwr); + int16_t state = RADIOLIB_ERR_INVALID_OUTPUT_POWER; + while(state == RADIOLIB_ERR_INVALID_OUTPUT_POWER) { + // go from the highest power and lower it until we hit one supported by the module + state = this->phyLayer->setOutputPower(pwr--); + } RADIOLIB_ASSERT(state); uint8_t syncWord[3] = { 0 }; @@ -1681,7 +1697,7 @@ int16_t LoRaWANNode::setupChannels(uint8_t* cfList) { // if there is a cflist present, parse its frequencies into the next five slots, with datarate range copied from default channel 0 if(cfList != nullptr) { RADIOLIB_DEBUG_PRINTLN("CFList present"); - LoRaWANMacCommand_t cmd; + LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 }; cmd.cid = RADIOLIB_LORAWAN_MAC_NEW_CHANNEL; cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_NEW_CHANNEL].lenDn; // datarate range for all new channels is equal to the default channels @@ -1700,7 +1716,7 @@ int16_t LoRaWANNode::setupChannels(uint8_t* cfList) { } else { // RADIOLIB_LORAWAN_BAND_FIXED if(cfList != nullptr) { RADIOLIB_DEBUG_PRINTLN("CFList present"); - LoRaWANMacCommand_t cmd; + LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 }; cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR; cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; cmd.payload[0] = 0xFF; // same datarate and payload @@ -1746,6 +1762,7 @@ int16_t LoRaWANNode::selectSubband(uint8_t startChannel, uint8_t endChannel) { RADIOLIB_DEBUG_PRINTLN("This is a dynamic band plan which does not support subbands"); return(RADIOLIB_ERR_INVALID_CHANNEL); } + this->selectedSubband = startChannel % 8; // save selected subband - assumed a block of 8 channels uint8_t numChannels = endChannel - startChannel + 1; if(startChannel > this->band->txSpans[0].numChannels) { @@ -1923,24 +1940,37 @@ int16_t LoRaWANNode::selectChannels() { return(RADIOLIB_ERR_NONE); } -int16_t LoRaWANNode::setDatarate(uint8_t drUp) { - // find the minimum and maximum available datarates by checking the enabled uplink channels - uint8_t drMin = RADIOLIB_LORAWAN_CHANNEL_NUM_DATARATES; - uint8_t drMax = 0; +int16_t LoRaWANNode::setDatarate(uint8_t drUp, bool saveToEeprom) { + // scan through all enabled channels and check if the requested datarate is available + bool isValidDR = false; for(size_t i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { - if(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled) { - drMin = RADIOLIB_MIN(drMin, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].drMin); - drMax = RADIOLIB_MAX(drMax, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].drMax); + LoRaWANChannel_t *chnl = &(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i]); + if(chnl->enabled) { + if(drUp > chnl->drMin && drUp < chnl->drMax) { + isValidDR = true; + break; + } } } - if((drUp < drMin) || (drUp > drMax)) { - RADIOLIB_DEBUG_PRINTLN("Cannot configure DR %d (min: %d, max: %d)", drUp, drMin, drMax); + if(!isValidDR) { + RADIOLIB_DEBUG_PRINTLN("No defined channel allows datarate %d", drUp); return(RADIOLIB_ERR_DATA_RATE_INVALID); } - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = drUp; - - RADIOLIB_DEBUG_PRINTLN("Configured DR up = %d", drUp); + LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 }; + cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; + cmd.payload[0] = (drUp << 4); + cmd.payload[0] |= 0x0F; // keep Tx Power the same + cmd.payload[3] |= (1 << 7); // set the RFU bit, which means that the channel mask gets ignored + cmd.payload[3] |= 0; // keep NbTrans the same + (void)execMacCommand(&cmd, saveToEeprom); + + // check if ACK is set for Tx Power + if((cmd.payload[0] >> 1) != 1) { + return(RADIOLIB_ERR_DATA_RATE_INVALID); + } + return(RADIOLIB_ERR_NONE); } @@ -2007,13 +2037,31 @@ uint8_t LoRaWANNode::maxPayloadDwellTime() { return(payLen - 13); // fixed 13-byte header } -int16_t LoRaWANNode::setTxPower(int8_t txPower) { - int16_t state = RADIOLIB_ERR_INVALID_OUTPUT_POWER; - while(state == RADIOLIB_ERR_INVALID_OUTPUT_POWER) { - // go from the highest power and lower it until we hit one supported by the module - state = this->phyLayer->setOutputPower(txPower--); +int16_t LoRaWANNode::setTxPower(int8_t txPower, bool saveToEeprom) { + // only allow values within the band's (or MAC state) maximum + if(txPower > this->txPowerMax) { + return(RADIOLIB_ERR_INVALID_OUTPUT_POWER); } - return(state); + // Tx Power is set in steps of two + // the selected value is rounded down to nearest multiple of two away from txPowerMax + // e.g. on EU868, max is 16; if 13 is selected then we set to 12 + uint8_t txPowerNew = (this->txPowerMax - txPower) / 2 + 1; + + LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 }; + cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; + cmd.payload[0] = 0xF0; // keep datarate the same + cmd.payload[0] |= txPowerNew; // set the Tx Power + cmd.payload[3] |= (1 << 7); // set the RFU bit, which means that the channel mask gets ignored + cmd.payload[3] |= 0; // keep NbTrans the same + (void)execMacCommand(&cmd, saveToEeprom); + + // check if ACK is set for Tx Power + if((cmd.payload[0] >> 2) != 1) { + return(RADIOLIB_ERR_INVALID_OUTPUT_POWER); + } + + return(RADIOLIB_ERR_NONE); } int16_t LoRaWANNode::findDataRate(uint8_t dr, DataRate_t* dataRate) { @@ -2197,13 +2245,16 @@ bool LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd, bool saveToEeprom) { } else { int8_t pwr = this->txPowerMax - 2*txPower; - int16_t state = this->setTxPower(pwr); + int16_t state = RADIOLIB_ERR_INVALID_OUTPUT_POWER; + while(state == RADIOLIB_ERR_INVALID_OUTPUT_POWER) { + // go from the highest power and lower it until we hit one supported by the module + state = this->phyLayer->setOutputPower(pwr--); + } // only acknowledge if the requested datarate was succesfully configured if(state == RADIOLIB_ERR_NONE) { pwrAck = 1; this->txPowerCur = txPower; } - } bool isSuccessive = false; diff --git a/src/protocols/LoRaWAN/LoRaWAN.h b/src/protocols/LoRaWAN/LoRaWAN.h index 81e87dbd6..4387070d0 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.h +++ b/src/protocols/LoRaWAN/LoRaWAN.h @@ -596,9 +596,10 @@ class LoRaWANNode { /*! \brief Set uplink datarate. This should not be used when ADR is enabled. \param dr Datarate to use for uplinks. + \param saveToEeprom Whether to save this setting to EEPROM or not (default false). \returns \ref status_codes */ - int16_t setDatarate(uint8_t drUp); + int16_t setDatarate(uint8_t drUp, bool saveToEeprom = false); /*! \brief Toggle ADR to on or off. @@ -644,9 +645,10 @@ class LoRaWANNode { /*! \brief Configure TX power of the radio module. \param txPower Output power during TX mode to be set in dBm. + \param saveToEeprom Whether to save this setting to EEPROM or not (default false). \returns \ref status_codes */ - int16_t setTxPower(int8_t txPower); + int16_t setTxPower(int8_t txPower, bool saveToEeprom = false); /*! \brief Select a single subband (8 channels) for fixed bands such as US915. @@ -800,6 +802,9 @@ class LoRaWANNode { // indicates whether an uplink has MAC commands as payload bool isMACPayload = false; + // save the selected subband in case this must be restored in ADR control + int8_t selectedSubband = -1; + #if !defined(RADIOLIB_EEPROM_UNSUPPORTED) /*! \brief Save the current uplink frame counter. From 7c676f939373c5c8505c5707897fc15a6cf9ccb5 Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Mon, 8 Jan 2024 22:33:34 +0100 Subject: [PATCH 6/9] [LoRaWAN] Implement requested changes --- .../LoRaWAN_End_Device/LoRaWAN_End_Device.ino | 2 +- .../LoRaWAN_End_Device_ABP.ino | 2 +- .../LoRaWAN_End_Device_Persistent.ino | 4 +- src/protocols/LoRaWAN/LoRaWAN.cpp | 100 +++++++----------- src/protocols/LoRaWAN/LoRaWAN.h | 51 ++++----- src/protocols/LoRaWAN/LoRaWANBands.cpp | 6 +- 6 files changed, 72 insertions(+), 93 deletions(-) diff --git a/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino b/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino index c2ea337fc..732da594a 100644 --- a/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino +++ b/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino @@ -157,7 +157,7 @@ void loop() { // wait before sending another packet uint32_t minimumDelay = 60000; // try to send once every minute uint32_t interval = node.timeUntilUplink(); // calculate minimum duty cycle delay (per law!) - uint32_t delayMs = max(interval, minimumDelay); // cannot send faster than duty cycle allows + uint32_t delayMs = max(interval, minimumDelay); // cannot send faster than duty cycle allows delay(delayMs); } diff --git a/examples/LoRaWAN/LoRaWAN_End_Device_ABP/LoRaWAN_End_Device_ABP.ino b/examples/LoRaWAN/LoRaWAN_End_Device_ABP/LoRaWAN_End_Device_ABP.ino index c2bd5ff13..5ac9261f1 100644 --- a/examples/LoRaWAN/LoRaWAN_End_Device_ABP/LoRaWAN_End_Device_ABP.ino +++ b/examples/LoRaWAN/LoRaWAN_End_Device_ABP/LoRaWAN_End_Device_ABP.ino @@ -165,7 +165,7 @@ void loop() { // wait before sending another packet uint32_t minimumDelay = 60000; // try to send once every minute uint32_t interval = node.timeUntilUplink(); // calculate minimum duty cycle delay (per law!) - uint32_t delayMs = max(interval, minimumDelay); // cannot send faster than duty cycle allows + uint32_t delayMs = max(interval, minimumDelay); // cannot send faster than duty cycle allows delay(delayMs); } diff --git a/examples/LoRaWAN/LoRaWAN_End_Device_Persistent/LoRaWAN_End_Device_Persistent.ino b/examples/LoRaWAN/LoRaWAN_End_Device_Persistent/LoRaWAN_End_Device_Persistent.ino index 983a82a08..672506c6b 100644 --- a/examples/LoRaWAN/LoRaWAN_End_Device_Persistent/LoRaWAN_End_Device_Persistent.ino +++ b/examples/LoRaWAN/LoRaWAN_End_Device_Persistent/LoRaWAN_End_Device_Persistent.ino @@ -77,7 +77,7 @@ void setup() { if(state >= RADIOLIB_ERR_NONE) { Serial.println(F("success!")); Serial.print(F("Restored an ")); - if(state == RADIOLIB_LORAWAN_MODE_OTAA) + if(state == RADIOLIB_LORAWAN_MODE_OTAA) Serial.println(F("OTAA session.")); else { Serial.println(F("ABP session.")); @@ -142,7 +142,7 @@ void loop() { // make sure to send the radio to sleep as well using radio.sleep() uint32_t minimumDelay = 60000; // try to send once every minute uint32_t interval = node.timeUntilUplink(); // calculate minimum duty cycle delay (per law!) - uint32_t delayMs = max(interval, minimumDelay); // cannot send faster than duty cycle allows + uint32_t delayMs = max(interval, minimumDelay); // cannot send faster than duty cycle allows delay(delayMs); } diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index 56fd5b639..2af2ae8b2 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -28,39 +28,6 @@ uint8_t getDownlinkDataRate(uint8_t uplink, uint8_t offset, uint8_t base, uint8_ return(dr); } -uint16_t checkSum16(uint32_t key) { - uint8_t bufLen = 2; - uint16_t buf16[bufLen]; - memcpy(buf16, &key, bufLen); - uint16_t checkSum = 0; - for(int i = 0; i < bufLen; i++) { - checkSum ^= buf16[i]; - } - return(checkSum); -} - -uint16_t checkSum16(uint64_t key) { - uint8_t bufLen = 4; - uint16_t buf16[bufLen]; - memcpy(buf16, &key, bufLen); - uint16_t checkSum = 0; - for(int i = 0; i < bufLen; i++) { - checkSum ^= buf16[i]; - } - return(checkSum); -} - -uint16_t checkSum16(uint8_t *key, uint8_t keyLen) { - uint8_t bufLen = keyLen / 2; - uint16_t buf16[bufLen]; - memcpy(buf16, key, bufLen); - uint16_t checkSum = 0; - for(int i = 0; i < bufLen; i++) { - checkSum ^= buf16[i]; - } - return(checkSum); -} - LoRaWANNode::LoRaWANNode(PhysicalLayer* phy, const LoRaWANBand_t* band) { this->phyLayer = phy; this->band = band; @@ -395,8 +362,8 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe #if !defined(RADIOLIB_EEPROM_UNSUPPORTED) uint16_t checkSum = 0; - checkSum ^= checkSum16(joinEUI); - checkSum ^= checkSum16(devEUI); + checkSum ^= checkSum16((uint8_t*)joinEUI, 8); + checkSum ^= checkSum16((uint8_t*)devEUI, 8); checkSum ^= checkSum16(nwkKey, 16); checkSum ^= checkSum16(appKey, 16); @@ -666,13 +633,11 @@ int16_t LoRaWANNode::beginABP(uint32_t addr, uint8_t* nwkSKey, uint8_t* appSKey, #if !defined(RADIOLIB_EEPROM_UNSUPPORTED) // check if we actually need to restart from a clean session uint16_t checkSum = 0; - checkSum ^= checkSum16(addr); + checkSum ^= checkSum16((uint8_t*)addr, 4); checkSum ^= checkSum16(nwkSKey, 16); checkSum ^= checkSum16(appSKey, 16); - if(fNwkSIntKey) - checkSum ^= checkSum16(fNwkSIntKey, 16); - if(sNwkSIntKey) - checkSum ^= checkSum16(sNwkSIntKey, 16); + if(fNwkSIntKey) { checkSum ^= checkSum16(fNwkSIntKey, 16); } + if(sNwkSIntKey) { checkSum ^= checkSum16(sNwkSIntKey, 16); } bool validCheckSum = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_CHECKSUM_ID) == checkSum; bool validMode = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_MODE_ID) == RADIOLIB_LORAWAN_MODE_ABP; @@ -904,10 +869,9 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf // check maximum payload len as defined in phy if(len > this->band->payloadLenMax[this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]]) { - len = this->band->payloadLenMax[this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]]; - // return(RADIOLIB_ERR_PACKET_TOO_LONG); + // len = this->band->payloadLenMax[this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]]; + return(RADIOLIB_ERR_PACKET_TOO_LONG); } - if(RADIOLIB_LORAWAN_FRAME_LEN(len, foptsLen)) // increase frame counter by one this->fcntUp += 1; @@ -2575,10 +2539,10 @@ bool LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd, bool saveToEeprom) { RADIOLIB_DEBUG_PRINTLN("TX timing: dlDwell = %d, ulDwell = %d, maxEirp = %d dBm", dlDwell, ulDwell, this->txPowerMax); this->dwellTimeEnabledUp = ulDwell ? true : false; - this->dwellTimeUp = ulDwell ? 400 : 0; + this->dwellTimeUp = ulDwell ? RADIOLIB_LORAWAN_DWELL_TIME : 0; this->dwellTimeEnabledDn = dlDwell ? true : false; - this->dwellTimeDn = dlDwell ? 400 : 0; + this->dwellTimeDn = dlDwell ? RADIOLIB_LORAWAN_DWELL_TIME : 0; #if !defined(RADIOLIB_EEPROM_UNSUPPORTED) if(saveToEeprom) { @@ -2673,31 +2637,32 @@ uint8_t LoRaWANNode::getMacPayloadLength(uint8_t cid) { return 0; } -bool LoRaWANNode::getMacLinkCheckAns(uint8_t* margin, uint8_t* gwCnt) { +int16_t LoRaWANNode::getMacLinkCheckAns(uint8_t* margin, uint8_t* gwCnt) { uint8_t payload[5]; int16_t state = deleteMacCommand(RADIOLIB_LORAWAN_LINK_CHECK_REQ, &this->commandsDown, payload); - if(state != RADIOLIB_ERR_NONE) - return false; + RADIOLIB_ASSERT(state); - *margin = payload[0]; - *gwCnt = payload[1]; - RADIOLIB_DEBUG_PRINTLN("Link check: margin = %d dB, gwCnt = %d", margin, gwCnt); - return(true); + if(margin) { *margin = payload[0]; } + if(gwCnt) { *gwCnt = payload[1]; } + // RADIOLIB_DEBUG_PRINTLN("Link check: margin = %d dB, gwCnt = %d", margin, gwCnt); + return(RADIOLIB_ERR_NONE); } -bool LoRaWANNode::getMacDeviceTimeAns(uint32_t* gpsEpoch, uint8_t* fraction, bool returnUnix) { +int16_t LoRaWANNode::getMacDeviceTimeAns(uint32_t* gpsEpoch, uint8_t* fraction, bool returnUnix) { uint8_t payload[5]; int16_t state = deleteMacCommand(RADIOLIB_LORAWAN_MAC_DEVICE_TIME, &this->commandsDown, payload); - if(state != RADIOLIB_ERR_NONE) - return false; - - *gpsEpoch = LoRaWANNode::ntoh(&payload[0]); - *fraction = payload[4]; - RADIOLIB_DEBUG_PRINTLN("Network time: gpsEpoch = %d s, delayExp = %f", gpsEpoch, (float)(*fraction)/256.0f); + RADIOLIB_ASSERT(state); - uint32_t unixOffset = 315964800; - *gpsEpoch += unixOffset; - return(true); + if(gpsEpoch) { + *gpsEpoch = LoRaWANNode::ntoh(&payload[0]); + if(returnUnix) { + uint32_t unixOffset = 315964800; + *gpsEpoch += unixOffset; + } + } + if(fraction) { *fraction = payload[4]; } + // RADIOLIB_DEBUG_PRINTLN("Network time: gpsEpoch = %d s, delayExp = %f", gpsEpoch, (float)(*fraction)/256.0f); + return(RADIOLIB_ERR_NONE); } @@ -2786,6 +2751,17 @@ void LoRaWANNode::processAES(uint8_t* in, size_t len, uint8_t* key, uint8_t* out } } +uint16_t LoRaWANNode::checkSum16(uint8_t *key, uint8_t keyLen) { + uint16_t buf16[RADIOLIB_AES128_KEY_SIZE/2]; + uint8_t bufLen = keyLen / 2; + memcpy(buf16, key, bufLen); + uint16_t checkSum = 0; + for(int i = 0; i < bufLen; i++) { + checkSum ^= buf16[i]; + } + return(checkSum); +} + template T LoRaWANNode::ntoh(uint8_t* buff, size_t size) { uint8_t* buffPtr = buff; diff --git a/src/protocols/LoRaWAN/LoRaWAN.h b/src/protocols/LoRaWAN/LoRaWAN.h index 4387070d0..de4b3ea0a 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.h +++ b/src/protocols/LoRaWAN/LoRaWAN.h @@ -159,9 +159,6 @@ #define RADIOLIB_LORAWAN_MIC_DATA_RATE_POS (3) #define RADIOLIB_LORAWAN_MIC_CH_INDEX_POS (4) -// magic word saved in persistent memory upon activation -#define RADIOLIB_LORAWAN_MAGIC (0x39EA) - // MAC commands #define RADIOLIB_LORAWAN_NUM_MAC_COMMANDS (16) @@ -191,6 +188,9 @@ // the maximum number of simultaneously available channels #define RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS (16) +// maximum allowed dwell time on bands that implement dwell time limitations +#define RADIOLIB_LORAWAN_DWELL_TIME (400) + struct LoRaWANMacSpec_t { const uint8_t cid; const uint8_t lenDn; @@ -199,23 +199,23 @@ struct LoRaWANMacSpec_t { }; const LoRaWANMacSpec_t MacTable[RADIOLIB_LORAWAN_NUM_MAC_COMMANDS + 1] = { - { 0x00, 0, 0, false }, // not an actual MAC command, exists for offsetting - { 0x01, 1, 1, false }, // RADIOLIB_LORAWAN_MAC_RESET - { 0x02, 2, 0, true }, // RADIOLIB_LORAWAN_MAC_LINK_CHECK - { 0x03, 4, 1, false }, // RADIOLIB_LORAWAN_MAC_LINK_ADR - { 0x04, 1, 0, false }, // RADIOLIB_LORAWAN_MAC_DUTY_CYCLE - { 0x05, 4, 1, false }, // RADIOLIB_LORAWAN_MAC_RX_PARAM_SETUP - { 0x06, 0, 2, false }, // RADIOLIB_LORAWAN_MAC_DEV_STATUS - { 0x07, 5, 1, false }, // RADIOLIB_LORAWAN_MAC_NEW_CHANNEL - { 0x08, 1, 0, false }, // RADIOLIB_LORAWAN_MAC_RX_TIMING_SETUP - { 0x09, 1, 0, false }, // RADIOLIB_LORAWAN_MAC_TX_PARAM_SETUP - { 0x0A, 4, 1, false }, // RADIOLIB_LORAWAN_MAC_DL_CHANNEL - { 0x0B, 1, 1, false }, // RADIOLIB_LORAWAN_MAC_REKEY - { 0x0C, 1, 0, false }, // RADIOLIB_LORAWAN_MAC_ADR_PARAM_SETUP - { 0x0D, 5, 0, true }, // RADIOLIB_LORAWAN_MAC_DEVICE_TIME - { 0x0E, 2, 0, false }, // RADIOLIB_LORAWAN_MAC_FORCE_REJOIN - { 0x0F, 1, 1, false }, // RADIOLIB_LORAWAN_MAC_REJOIN_PARAM_SETUP - { 0x80, 5, 0, true } // RADIOLIB_LORAWAN_MAC_PROPRIETARY + { 0x00, 0, 0, false }, // not an actual MAC command, exists for index offsetting + { RADIOLIB_LORAWAN_MAC_RESET, 1, 1, false }, + { RADIOLIB_LORAWAN_MAC_LINK_CHECK, 2, 0, true }, + { RADIOLIB_LORAWAN_MAC_LINK_ADR, 4, 1, false }, + { RADIOLIB_LORAWAN_MAC_DUTY_CYCLE, 1, 0, false }, + { RADIOLIB_LORAWAN_MAC_RX_PARAM_SETUP, 4, 1, false }, + { RADIOLIB_LORAWAN_MAC_DEV_STATUS, 0, 2, false }, + { RADIOLIB_LORAWAN_MAC_NEW_CHANNEL, 5, 1, false }, + { RADIOLIB_LORAWAN_MAC_RX_TIMING_SETUP, 1, 0, false }, + { RADIOLIB_LORAWAN_MAC_TX_PARAM_SETUP, 1, 0, false }, + { RADIOLIB_LORAWAN_MAC_DL_CHANNEL, 4, 1, false }, + { RADIOLIB_LORAWAN_MAC_REKEY, 1, 1, false }, + { RADIOLIB_LORAWAN_MAC_ADR_PARAM_SETUP, 1, 0, false }, + { RADIOLIB_LORAWAN_MAC_DEVICE_TIME, 5, 0, true }, + { RADIOLIB_LORAWAN_MAC_FORCE_REJOIN, 2, 0, false }, + { RADIOLIB_LORAWAN_MAC_REJOIN_PARAM_SETUP, 1, 1, false }, + { RADIOLIB_LORAWAN_MAC_PROPRIETARY, 5, 0, true } }; /*! @@ -681,9 +681,9 @@ class LoRaWANNode { Returns 'false' if there was no network response / parsing failed. \param margin Link margin in dB of LinkCheckReq demodulation at gateway side. \param gwCnt Number of gateways that received the LinkCheckReq. - \returns Whether the parameters where succesfully parsed. + \returns \ref status_codes */ - bool getMacLinkCheckAns(uint8_t* margin, uint8_t* gwCnt); + int16_t getMacLinkCheckAns(uint8_t* margin, uint8_t* gwCnt); /*! \brief Returns the network time after requesting a DeviceTime MAC command. @@ -692,9 +692,9 @@ class LoRaWANNode { \param gpsEpoch Number of seconds since GPS epoch (Jan. 6th 1980) \param fraction Fractional-second, in 1/256-second steps \param returnUnix If true, returns Unix timestamp instead of GPS (default true) - \returns Whether the parameters where succesfully parsed. + \returns \ref status_codes */ - bool getMacDeviceTimeAns(uint32_t* gpsEpoch, uint8_t* fraction, bool returnUnix = true); + int16_t getMacDeviceTimeAns(uint32_t* gpsEpoch, uint8_t* fraction, bool returnUnix = true); #if !RADIOLIB_GODMODE private: @@ -876,6 +876,9 @@ class LoRaWANNode { // function to encrypt and decrypt payloads void processAES(uint8_t* in, size_t len, uint8_t* key, uint8_t* out, uint32_t fcnt, uint8_t dir, uint8_t ctrId, bool counter); + // 16-bit checksum method that takes a uint8_t array of even length and calculates the checksum + static uint16_t checkSum16(uint8_t *key, uint8_t keyLen); + // network-to-host conversion method - takes data from network packet and converts it to the host endians template static T ntoh(uint8_t* buff, size_t size = 0); diff --git a/src/protocols/LoRaWAN/LoRaWANBands.cpp b/src/protocols/LoRaWAN/LoRaWANBands.cpp index 54580b0ee..e8b244bd9 100644 --- a/src/protocols/LoRaWAN/LoRaWANBands.cpp +++ b/src/protocols/LoRaWAN/LoRaWANBands.cpp @@ -53,7 +53,7 @@ const LoRaWANBand_t US915 = { .powerMax = 30, .powerNumSteps = 10, .dutyCycle = 0, - .dwellTimeUp = 400, + .dwellTimeUp = RADIOLIB_LORAWAN_DWELL_TIME, .dwellTimeDn = 0, .txFreqs = { RADIOLIB_LORAWAN_CHANNEL_NONE, @@ -334,8 +334,8 @@ const LoRaWANBand_t AS923 = { .powerMax = 16, .powerNumSteps = 7, .dutyCycle = 36000, - .dwellTimeUp = 400, - .dwellTimeDn = 400, + .dwellTimeUp = RADIOLIB_LORAWAN_DWELL_TIME, + .dwellTimeDn = RADIOLIB_LORAWAN_DWELL_TIME, .txFreqs = { { .enabled = true, .idx = 0, .freq = 923.200, .drMin = 0, .drMax = 5}, { .enabled = true, .idx = 1, .freq = 923.400, .drMin = 0, .drMax = 5}, From d0979ce853302af89f6438e99812e14afd58014d Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Mon, 8 Jan 2024 23:36:17 +0100 Subject: [PATCH 7/9] [LoRaWAN] Fix checksum calculation --- src/protocols/LoRaWAN/LoRaWAN.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index 2af2ae8b2..6e7e0d6c7 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -362,10 +362,10 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe #if !defined(RADIOLIB_EEPROM_UNSUPPORTED) uint16_t checkSum = 0; - checkSum ^= checkSum16((uint8_t*)joinEUI, 8); - checkSum ^= checkSum16((uint8_t*)devEUI, 8); - checkSum ^= checkSum16(nwkKey, 16); - checkSum ^= checkSum16(appKey, 16); + checkSum ^= LoRaWANNode::checkSum16(reinterpret_cast(&joinEUI), 8); + checkSum ^= LoRaWANNode::checkSum16(reinterpret_cast(&devEUI), 8); + checkSum ^= LoRaWANNode::checkSum16(nwkKey, 16); + checkSum ^= LoRaWANNode::checkSum16(appKey, 16); bool validCheckSum = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_CHECKSUM_ID) == checkSum; bool validMode = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_MODE_ID) == RADIOLIB_LORAWAN_MODE_OTAA; @@ -633,11 +633,11 @@ int16_t LoRaWANNode::beginABP(uint32_t addr, uint8_t* nwkSKey, uint8_t* appSKey, #if !defined(RADIOLIB_EEPROM_UNSUPPORTED) // check if we actually need to restart from a clean session uint16_t checkSum = 0; - checkSum ^= checkSum16((uint8_t*)addr, 4); - checkSum ^= checkSum16(nwkSKey, 16); - checkSum ^= checkSum16(appSKey, 16); - if(fNwkSIntKey) { checkSum ^= checkSum16(fNwkSIntKey, 16); } - if(sNwkSIntKey) { checkSum ^= checkSum16(sNwkSIntKey, 16); } + checkSum ^= LoRaWANNode::checkSum16(reinterpret_cast(&addr), 4); + checkSum ^= LoRaWANNode::checkSum16(nwkSKey, 16); + checkSum ^= LoRaWANNode::checkSum16(appSKey, 16); + if(fNwkSIntKey) { checkSum ^= LoRaWANNode::checkSum16(fNwkSIntKey, 16); } + if(sNwkSIntKey) { checkSum ^= LoRaWANNode::checkSum16(sNwkSIntKey, 16); } bool validCheckSum = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_CHECKSUM_ID) == checkSum; bool validMode = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_MODE_ID) == RADIOLIB_LORAWAN_MODE_ABP; @@ -2752,9 +2752,9 @@ void LoRaWANNode::processAES(uint8_t* in, size_t len, uint8_t* key, uint8_t* out } uint16_t LoRaWANNode::checkSum16(uint8_t *key, uint8_t keyLen) { - uint16_t buf16[RADIOLIB_AES128_KEY_SIZE/2]; + uint16_t buf16[RADIOLIB_AES128_KEY_SIZE/2] = { 0 }; uint8_t bufLen = keyLen / 2; - memcpy(buf16, key, bufLen); + memcpy(buf16, key, keyLen); uint16_t checkSum = 0; for(int i = 0; i < bufLen; i++) { checkSum ^= buf16[i]; From 0bba68f3ae9321e8e2f3bd9ac97946ec79126bf6 Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Sat, 13 Jan 2024 00:05:25 +0100 Subject: [PATCH 8/9] [LoRaWAN] Rework channel logic --- keywords.txt | 2 +- src/TypeDef.h | 5 - src/protocols/LoRaWAN/LoRaWAN.cpp | 701 +++++++++++++++++------------- src/protocols/LoRaWAN/LoRaWAN.h | 47 +- 4 files changed, 423 insertions(+), 332 deletions(-) diff --git a/keywords.txt b/keywords.txt index 3f2da44a5..ad416f4e2 100644 --- a/keywords.txt +++ b/keywords.txt @@ -294,6 +294,7 @@ wipe KEYWORD2 restore KEYWORD2 beginOTAA KEYWORD2 beginABP KEYWORD2 +isJoined KEYWORD2 saveSession KEYWORD2 sendMacCommandReq KEYWORD2 uplink KEYWORD2 @@ -312,7 +313,6 @@ timeUntilUplink KEYWORD2 setDwellTime KEYWORD2 maxPayloadDwellTime KEYWORD2 setTxPower KEYWORD2 -selectSubband KEYWORD2 setCSMA KEYWORD2 getMacLinkCheckAns KEYWORD2 getMacDeviceTimeAns KEYWORD2 diff --git a/src/TypeDef.h b/src/TypeDef.h index 64d1a1a29..f82e787b1 100644 --- a/src/TypeDef.h +++ b/src/TypeDef.h @@ -553,11 +553,6 @@ */ #define RADIOLIB_ERR_A_FCNT_DOWN_INVALID (-1114) -/*! - \brief Datarate requested by user is invalid. -*/ -#define RADIOLIB_ERR_DATA_RATE_INVALID (-1115) - /*! \} */ diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index 6e7e0d6c7..ba2c6be02 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -28,24 +28,12 @@ uint8_t getDownlinkDataRate(uint8_t uplink, uint8_t offset, uint8_t base, uint8_ return(dr); } -LoRaWANNode::LoRaWANNode(PhysicalLayer* phy, const LoRaWANBand_t* band) { +LoRaWANNode::LoRaWANNode(PhysicalLayer* phy, const LoRaWANBand_t* band, uint8_t subBand) { this->phyLayer = phy; this->band = band; this->rx2 = this->band->rx2; - this->dutyCycle = this->band->dutyCycle; - if(this->dutyCycle > 0) { - this->dutyCycleEnabled = true; - } - this->dwellTimeUp = this->band->dwellTimeUp; - if(this->dwellTimeUp > 0) { - this->dwellTimeEnabledUp = true; - } - this->dwellTimeDn = this->band->dwellTimeDn; - if(this->dwellTimeDn) { - this->dwellTimeEnabledDn = true; - } this->txPowerMax = this->band->powerMax; - this->txPowerCur = 0; // start at 0 offset = full power + this->subBand = subBand; this->difsSlots = 2; this->backoffMax = 6; this->enableCSMA = false; @@ -63,7 +51,6 @@ void LoRaWANNode::wipe() { mod->hal->wipePersistentStorage(); } -// TODO do not return status code, but return LoRaWAN mode (OTAA, ABP, none) int16_t LoRaWANNode::restore() { // if already joined, ignore if(this->activeMode != RADIOLIB_LORAWAN_MODE_NONE) { @@ -218,7 +205,11 @@ int16_t LoRaWANNode::restoreFcntUp() { int16_t LoRaWANNode::restoreChannels() { // first do the default channels - this->setupChannels(nullptr); + if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) { + this->setupChannelsDyn(false); + } else { // RADIOLIB_LORAWAN_BAND_FIXED + this->setupChannelsFix(this->subBand); + } Module* mod = this->phyLayer->getMod(); uint8_t bufferZeroes[5] = { 0 }; @@ -252,16 +243,17 @@ int16_t LoRaWANNode::restoreChannels() { } } - } else { - uint8_t numBytes = 8 * MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; + } else { // RADIOLIB_LORAWAN_BAND_FIXED + uint8_t numADRCommands = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_NUM_ADR_MASKS_ID); + uint8_t numBytes = numADRCommands * MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; uint8_t buffer[numBytes] = { 0 }; mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_UL_CHANNELS_ID), buffer, numBytes); LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 }; cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR; - cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; - for(int i = 0; i < 8; i++) { + for(int i = 0; i < numADRCommands; i++) { + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; memcpy(cmd.payload, &buffer[i * cmd.len], cmd.len); // there COULD, according to spec, be an all zeroes ADR command - meh if(memcmp(cmd.payload, bufferZeroes, cmd.len) != 0) { @@ -273,13 +265,32 @@ int16_t LoRaWANNode::restoreChannels() { } #endif -void LoRaWANNode::beginCommon() { +void LoRaWANNode::beginCommon(uint8_t joinDr) { LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 }; cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR; cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; - cmd.payload[0] = (this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] << 4); + if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) { + uint8_t drUp = 0; + // if join datarate is user-specified and valid, select that value; otherwise use + if(joinDr != RADIOLIB_LORAWAN_DATA_RATE_UNUSED) { + if(joinDr >= this->band->txFreqs[0].drMin && joinDr <= this->band->txFreqs[0].drMax) { + drUp = joinDr; + } else { + RADIOLIB_DEBUG_PRINTLN("Datarate %d is not valid (min: %d, max %d) - using default", + joinDr, this->band->txFreqs[0].drMin, this->band->txFreqs[0].drMax); + joinDr = RADIOLIB_LORAWAN_DATA_RATE_UNUSED; + } + } + if(joinDr == RADIOLIB_LORAWAN_DATA_RATE_UNUSED) { + drUp = (this->band->txFreqs[0].drMin + this->band->txFreqs[0].drMax) / 2; + } + cmd.payload[0] = (drUp << 4); + } else { + uint8_t drJr = this->band->txSpans[0].joinRequestDataRate; + cmd.payload[0] = (drJr << 4); + } cmd.payload[0] |= 0; // default to max Tx Power - cmd.payload[3] |= (1 << 7); // set the RFU bit, which means that the channel mask gets ignored + cmd.payload[3] = (1 << 7); // set the RFU bit, which means that the channel mask gets ignored (void)execMacCommand(&cmd); cmd.cid = RADIOLIB_LORAWAN_MAC_DUTY_CYCLE; @@ -393,17 +404,25 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe int16_t state = RADIOLIB_ERR_NONE; - // setup uplink/downlink frequencies and datarates - state = this->selectChannelsJR(this->devNonce, joinDr); + // setup join-request uplink/downlink frequencies and datarates + if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) { + state = this->setupChannelsDyn(true); + } else { + state = this->setupChannelsFix(this->subBand); + } RADIOLIB_ASSERT(state); // setup all MAC properties to default values - this->beginCommon(); + this->beginCommon(joinDr); // set the physical layer configuration state = this->setPhyProperties(); RADIOLIB_ASSERT(state); + // select a random pair of Tx/Rx channels + state = this->selectChannels(); + RADIOLIB_ASSERT(state); + // configure for uplink with default configuration state = this->configureChannel(RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK); RADIOLIB_ASSERT(state); @@ -532,14 +551,17 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe cmd.payload[0] = joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_RX_DELAY_POS]; (void)execMacCommand(&cmd); + // in case of dynamic band, setup the default channels first + if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) { + this->setupChannelsDyn(false); + } // process CFlist if present if(lenRx == RADIOLIB_LORAWAN_JOIN_ACCEPT_MAX_LEN) { uint8_t cfList[RADIOLIB_LORAWAN_JOIN_ACCEPT_CFLIST_LEN] = { 0 }; memcpy(&cfList[0], &joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_CFLIST_POS], RADIOLIB_LORAWAN_JOIN_ACCEPT_CFLIST_LEN); - this->setupChannels(cfList); - } else { - this->setupChannels(nullptr); - } + this->processCFList(cfList); + } + // if no CFList was received, default or subband are already setup so don't need to do anything else // prepare buffer for key derivation uint8_t keyDerivationBuff[RADIOLIB_AES128_BLOCK_SIZE] = { 0 }; @@ -678,9 +700,12 @@ int16_t LoRaWANNode::beginABP(uint32_t addr, uint8_t* nwkSKey, uint8_t* appSKey, int16_t state = RADIOLIB_ERR_NONE; - // calculate initial datarate - in case of fixed bands, this requires a subband to be selected - // downlink datarate is calculated using a specific uplink channel, so don't care here - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = (this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][0].drMax + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][0].drMin) / 2; + // setup the uplink/downlink channels and initial datarate + if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) { + this->setupChannelsDyn(); + } else { + this->setupChannelsFix(this->subBand); + } // setup all MAC properties to default values this->beginCommon(); @@ -869,8 +894,9 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf // check maximum payload len as defined in phy if(len > this->band->payloadLenMax[this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]]) { - // len = this->band->payloadLenMax[this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]]; return(RADIOLIB_ERR_PACKET_TOO_LONG); + // if testing with TS008 specification verification protocol, don't throw error but clip the message + // len = this->band->payloadLenMax[this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]]; } // increase frame counter by one @@ -919,13 +945,11 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf break; case(3): { if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) { - this->setupChannels(nullptr); // revert to default frequencies + this->setupChannelsDyn(false); // revert to default frequencies } else { - // if a subband was selected by user, go back to its default state + // go back to default selected subband // hopefully it'll help something, but probably not; at least we tried.. - if(this->selectedSubband >= 0) { - this->selectSubband(this->selectedSubband); - } + this->setupChannelsFix(this->subBand); } adrStage = 0; // nothing else to do, so end the cycle } @@ -1645,216 +1669,135 @@ int16_t LoRaWANNode::setPhyProperties() { return(state); } -int16_t LoRaWANNode::setupChannels(uint8_t* cfList) { - RADIOLIB_DEBUG_PRINTLN("Setting up channels"); +int16_t LoRaWANNode::setupChannelsDyn(bool joinRequest) { + RADIOLIB_DEBUG_PRINTLN("Setting up dynamic channels"); - // in case of frequency list-type band, copy the default TX channels into the available channels, with RX1 = TX - if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) { - RADIOLIB_DEBUG_PRINTLN("Dynamic band"); - size_t num = 0; - // copy the default defined channels into the first slots - for(; num < 3 && this->band->txFreqs[num].enabled; num++) { + size_t num = 0; + // copy the default defined channels into the first slots (where Tx = Rx) + for(; num < 3 && this->band->txFreqs[num].enabled; num++) { + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num] = this->band->txFreqs[num]; + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][num] = this->band->txFreqs[num]; + RADIOLIB_DEBUG_PRINTLN("Channel UL/DL %d frequency = %f MHz", this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].idx, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].freq); + } + + // if we're about to send a join-request, copy the join-request channels to the next slots + if(joinRequest) { + size_t numJR = 0; + for(; numJR < 3 && this->band->txJoinReq[num].enabled; numJR++, num++) { this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num] = this->band->txFreqs[num]; this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][num] = this->band->txFreqs[num]; RADIOLIB_DEBUG_PRINTLN("Channel UL/DL %d frequency = %f MHz", this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].idx, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].freq); } - // if there is a cflist present, parse its frequencies into the next five slots, with datarate range copied from default channel 0 - if(cfList != nullptr) { - RADIOLIB_DEBUG_PRINTLN("CFList present"); - LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 }; - cmd.cid = RADIOLIB_LORAWAN_MAC_NEW_CHANNEL; - cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_NEW_CHANNEL].lenDn; - // datarate range for all new channels is equal to the default channels - cmd.payload[4] = (this->band->txFreqs[0].drMax << 4) | this->band->txFreqs[0].drMin; - for(uint8_t i = 0; i < 5; i++, num++) { - cmd.payload[0] = num; - memcpy(&cmd.payload[1], &cfList[i*3], 3); - (void)execMacCommand(&cmd); - } - } - for(; num < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; num++) { - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num] = RADIOLIB_LORAWAN_CHANNEL_NONE; - } - - - } else { // RADIOLIB_LORAWAN_BAND_FIXED - if(cfList != nullptr) { - RADIOLIB_DEBUG_PRINTLN("CFList present"); - LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 }; - cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR; - cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; - cmd.payload[0] = 0xFF; // same datarate and payload - - // in case of mask-type bands, copy those frequencies that are masked true into the available TX channels - size_t numChMasks = 3 + this->band->numTxSpans; // 4 masks for bands with 2 spans, 5 spans for bands with 1 span - for(size_t chMaskCntl = 0; chMaskCntl < numChMasks; chMaskCntl++) { - cmd.payload[3] = chMaskCntl << 4; // NbTrans = 0 -> keep the same - memcpy(&cmd.payload[1], &cfList[chMaskCntl*2], 2); - (void)execMacCommand(&cmd); - } - } - } - for (int i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { - RADIOLIB_DEBUG_PRINTLN("UL: %d %d %5.2f (%d - %d) | DL: %d %d %5.2f (%d - %d)", - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx, - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled, - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].freq, - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].drMin, - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].drMax, - - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].idx, - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].enabled, - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].freq, - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].drMin, - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].drMax - ); } + return(RADIOLIB_ERR_NONE); } -int16_t LoRaWANNode::selectSubband(uint8_t idx) { - int16_t state = this->selectSubband((idx - 1) * 8, idx * 8 - 1); - return(state); -} - -int16_t LoRaWANNode::selectSubband(uint8_t startChannel, uint8_t endChannel) { - if(this->activeMode != RADIOLIB_LORAWAN_MODE_NONE) { - RADIOLIB_DEBUG_PRINTLN("There is already an active session - cannot change subband"); - return(RADIOLIB_ERR_INVALID_CHANNEL); - } - if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) { - RADIOLIB_DEBUG_PRINTLN("This is a dynamic band plan which does not support subbands"); - return(RADIOLIB_ERR_INVALID_CHANNEL); +// setup a subband and its corresponding join-request datarate +// WARNING: subBand starts at 1 (corresponds to all populair schemes) +int16_t LoRaWANNode::setupChannelsFix(uint8_t subBand) { + RADIOLIB_DEBUG_PRINTLN("Setting up fixed channels"); + // randomly select one of 8 or 9 channels and find corresponding datarate + uint8_t numChannels = this->band->numTxSpans == 1 ? 8 : 9; + uint8_t rand = this->phyLayer->random(numChannels) + 1; // range 1-8 or 1-9 + uint8_t drJR = RADIOLIB_LORAWAN_DATA_RATE_UNUSED; + if(rand <= 8) { + drJR = this->band->txSpans[0].joinRequestDataRate; // if one of the first 8 channels, select datarate of span 0 + } else { + drJR = this->band->txSpans[1].joinRequestDataRate; // if ninth channel, select datarate of span 1 } - this->selectedSubband = startChannel % 8; // save selected subband - assumed a block of 8 channels - uint8_t numChannels = endChannel - startChannel + 1; - if(startChannel > this->band->txSpans[0].numChannels) { - RADIOLIB_DEBUG_PRINTLN("There are only %d channels available in this band", this->band->txSpans[0].numChannels); - return(RADIOLIB_ERR_INVALID_CHANNEL); - } - if(startChannel + numChannels > this->band->txSpans[0].numChannels) { - numChannels = this->band->txSpans[0].numChannels - startChannel; - RADIOLIB_DEBUG_PRINTLN("Could only select %d channels due to end of band", numChannels); - } - if(numChannels > RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS) { - numChannels = RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; - RADIOLIB_DEBUG_PRINTLN("Could only select %d channels due to specified limit", numChannels); + // if no subband is selected by user, cycle through banks of 8 using devNonce value + if(subBand == 0) { + uint8_t numBanks8 = this->band->txSpans[0].numChannels / 8; + subBand = this->devNonce % numBanks8; } - LoRaWANChannel_t chnl; - for(size_t chNum = 0; chNum < numChannels; chNum++) { - chnl.enabled = true; - chnl.idx = startChannel + chNum; - chnl.freq = this->band->txSpans[0].freqStart + chnl.idx*this->band->txSpans[0].freqStep; - chnl.drMin = this->band->txSpans[0].drMin; - chnl.drMax = this->band->txSpans[0].drMax; - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chNum] = chnl; - // downlink channel is dynamically calculated on each uplink in selectChannels() - RADIOLIB_DEBUG_PRINTLN("Channel UL %d frequency = %f MHz", chNum, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chNum].freq); + // chMask is set for 16 channels at once, so widen the Cntl value + uint8_t chMaskCntl = (subBand - 1) / 2; // compensate the 1 offset + + LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 }; + cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR; + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; + + // if there are two channel spans, first set the channel from second span + if(this->band->numTxSpans == 2) { + cmd.payload[0] = (drJR << 4); // set join-request datarate + cmd.payload[0] |= 0; // set Tx power to maximum + // enable channel that belongs to this subband + cmd.payload[1] = (1 << (subBand - 1)); // set channel mask + cmd.payload[2] = 0; + cmd.payload[3] = (7 << 4); // set the chMaskCntl value to all channels off + cmd.payload[3] |= 0; // keep NbTrans the same + (void)execMacCommand(&cmd, false); + } + + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; + cmd.payload[0] = (drJR << 4); // set join-request datarate + cmd.payload[0] |= 0; // set Tx power to maximum + // now select the correct bank of 8 channels + // 0x00 0xFF channel mask for subband = 2, 4.. (even) + // 0xFF 0x00 channel mask for subband = 1, 3.. (odd) + if(subBand % 2 == 0) { + cmd.payload[1] = 0x00; + cmd.payload[2] = 0xFF; + } else { + cmd.payload[1] = 0xFF; + cmd.payload[2] = 0x00; } + cmd.payload[3] = (chMaskCntl << 4); // set the chMaskCntl value + cmd.payload[3] |= 0; // keep NbTrans the same + (void)execMacCommand(&cmd, false); + return(RADIOLIB_ERR_NONE); } -int16_t LoRaWANNode::selectChannelsJR(uint16_t devNonce, uint8_t joinDr) { - LoRaWANChannel_t channelUp; - LoRaWANChannel_t channelDown; - uint8_t drUp; - uint8_t drDown; +int16_t LoRaWANNode::processCFList(uint8_t* cfList) { + RADIOLIB_DEBUG_PRINTLN("Processing CFList"); + if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) { - // count the number of available channels for a join-request (default channels + join-request channels) - uint8_t numJRChannels = 0; - for(size_t i = 0; i < 3; i++) { - if(this->band->txFreqs[i].enabled) { - numJRChannels++; - } - if(this->band->txJoinReq[i].enabled) { - numJRChannels++; - } - } - - // cycle through the available channels (seed with devNonce) - uint8_t channelId = devNonce % numJRChannels; - - // find the channel whose index is selected - for(size_t i = 0; i < 3; i++) { - if(this->band->txFreqs[i].idx == channelId) { - channelUp = this->band->txFreqs[i]; + // retrieve number of existing (default) channels + size_t num = 0; + for(int i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { + if(!this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled) { break; } - if(this->band->txJoinReq[i].idx == channelId) { - channelUp = this->band->txJoinReq[i]; - } + num++; } - // if join datarate is user-specified and valid, select that value; otherwise use - if(joinDr != RADIOLIB_LORAWAN_DATA_RATE_UNUSED) { - if(joinDr >= channelUp.drMin && joinDr <= channelUp.drMax) { - drUp = joinDr; - } else { - RADIOLIB_DEBUG_PRINTLN("Datarate %d is not valid (min: %d, max %d) - using default", joinDr, channelUp.drMin, channelUp.drMax); - joinDr = RADIOLIB_LORAWAN_DATA_RATE_UNUSED; - } - } - if(joinDr == RADIOLIB_LORAWAN_DATA_RATE_UNUSED) { - drUp = int((channelUp.drMax + channelUp.drMin) / 2); + LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 }; + cmd.cid = RADIOLIB_LORAWAN_MAC_NEW_CHANNEL; + // datarate range for all new channels is equal to the default channels + cmd.payload[4] = (this->band->txFreqs[0].drMax << 4) | this->band->txFreqs[0].drMin; + for(uint8_t i = 0; i < 5; i++, num++) { + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_NEW_CHANNEL].lenDn; + cmd.payload[0] = num; + memcpy(&cmd.payload[1], &cfList[i*3], 3); + (void)execMacCommand(&cmd); } - - // derive the downlink channel and datarate from the uplink channel and datarate - channelDown = channelUp; - drDown = getDownlinkDataRate(drUp, this->rx1DrOffset, this->band->rx1DataRateBase, channelDown.drMin, channelDown.drMax); - - } else { // RADIOLIB_LORAWAN_BAND_FIXED - uint8_t spanID = 0; - uint8_t channelID = 0; - uint8_t numEnabledChannels = 0; - // if there are any predefined channels because user selected a subband, select one of these channels - for(; numEnabledChannels < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; numEnabledChannels++) { - if(this->availableChannels[numEnabledChannels][RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK].enabled == false) { - break; - } + } else { // RADIOLIB_LORAWAN_BAND_FIXED + // complete channel mask received, so clear all existing channels + for(int i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i] = RADIOLIB_LORAWAN_CHANNEL_NONE; } - if(numEnabledChannels > 0) { - uint8_t channelID = this->phyLayer->random(numEnabledChannels); - channelUp = this->availableChannels[channelID][RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]; - spanID = channelUp.idx / this->band->txSpans[0].numChannels; - channelID = channelUp.idx; - - } else { // no pre-selected subband, cycle through size-8 (or size-9) blocks - channelUp.enabled = true; - uint8_t numBlocks = this->band->txSpans[0].numChannels / 8; // calculate number of 8-channel blocks - uint8_t numBlockChannels = 8 + (this->band->numTxSpans == 2 ? 1 : 0); // add a 9th channel if there's a second span - uint8_t blockID = devNonce % numBlocks; // currently selected block (seed with devNonce) - channelID = this->phyLayer->random(numBlockChannels); // select randomly from these 8 or 9 channels - RADIOLIB_DEBUG_PRINTLN("blocks: %d, channels/block: %d, blockID: %d, channelID: %d", numBlocks, numBlockChannels, blockID, channelID); - - // if channel 0-7 is selected, retrieve this channel from span 0; otherwise span 1 - if(channelID < 8) { - spanID = 0; - channelUp.idx = blockID * 8 + channelID; - } else { - spanID = 1; - channelUp.idx = blockID; - } - channelUp.freq = this->band->txSpans[spanID].freqStart + channelUp.idx*this->band->txSpans[spanID].freqStep; + + LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 }; + cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR; + cmd.payload[0] = 0xFF; // same datarate and payload + + // in case of mask-type bands, copy those frequencies that are masked true into the available TX channels + size_t numChMasks = 3 + this->band->numTxSpans; // 4 masks for bands with 2 spans, 5 spans for bands with 1 span + for(size_t chMaskCntl = 0; chMaskCntl < numChMasks; chMaskCntl++) { + cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; + cmd.payload[3] = chMaskCntl << 4; // NbTrans = 0 -> keep the same + memcpy(&cmd.payload[1], &cfList[chMaskCntl*2], 2); + (void)execMacCommand(&cmd); + // save the response as a MAC answer, as this signals execMacCommand() to store the masks contiguously + pushMacCommand(&cmd, &this->commandsUp); } - - // for fixed channel plans, the user-specified datarate is ignored and span-specific value must be used - drUp = this->band->txSpans[spanID].joinRequestDataRate; - - // derive the downlink channel and datarate from the uplink channel and datarate - channelDown.enabled = true; - channelDown.idx = channelID % this->band->rx1Span.numChannels; - channelDown.freq = this->band->rx1Span.freqStart + channelDown.idx*this->band->rx1Span.freqStep; - channelDown.drMin = this->band->rx1Span.drMin; - channelDown.drMax = this->band->rx1Span.drMax; - drDown = getDownlinkDataRate(drUp, this->rx1DrOffset, this->band->rx1DataRateBase, channelDown.drMin, channelDown.drMax); - + // delete the ADR response + (void)deleteMacCommand(RADIOLIB_LORAWAN_MAC_LINK_ADR, &this->commandsUp); } - this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = channelUp; - this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = channelDown; - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = drUp; - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = drDown; return(RADIOLIB_ERR_NONE); } @@ -1869,9 +1812,7 @@ int16_t LoRaWANNode::selectChannels() { && this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] <= this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].drMax) { channelsEnabled[numChannels] = i; numChannels++; - } - } else { - break; + } } } if(numChannels == 0) { @@ -1886,7 +1827,7 @@ int16_t LoRaWANNode::selectChannels() { // for dynamic bands, the downlink channel is the one matched to the uplink channel this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][channelID]; - } else { // RADIOLIB_LORAWAN_BAND_FIXED + } else { // RADIOLIB_LORAWAN_BAND_FIXED // for fixed bands, the downlink channel is the uplink channel ID `modulo` number of downlink channels LoRaWANChannel_t channelDn; channelDn.enabled = true; @@ -1918,7 +1859,7 @@ int16_t LoRaWANNode::setDatarate(uint8_t drUp, bool saveToEeprom) { } if(!isValidDR) { RADIOLIB_DEBUG_PRINTLN("No defined channel allows datarate %d", drUp); - return(RADIOLIB_ERR_DATA_RATE_INVALID); + return(RADIOLIB_ERR_INVALID_DATA_RATE); } LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 }; @@ -1926,13 +1867,13 @@ int16_t LoRaWANNode::setDatarate(uint8_t drUp, bool saveToEeprom) { cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; cmd.payload[0] = (drUp << 4); cmd.payload[0] |= 0x0F; // keep Tx Power the same - cmd.payload[3] |= (1 << 7); // set the RFU bit, which means that the channel mask gets ignored + cmd.payload[3] = (1 << 7); // set the RFU bit, which means that the channel mask gets ignored cmd.payload[3] |= 0; // keep NbTrans the same (void)execMacCommand(&cmd, saveToEeprom); // check if ACK is set for Tx Power if((cmd.payload[0] >> 1) != 1) { - return(RADIOLIB_ERR_DATA_RATE_INVALID); + return(RADIOLIB_ERR_INVALID_DATA_RATE); } return(RADIOLIB_ERR_NONE); @@ -2016,7 +1957,7 @@ int16_t LoRaWANNode::setTxPower(int8_t txPower, bool saveToEeprom) { cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; cmd.payload[0] = 0xF0; // keep datarate the same cmd.payload[0] |= txPowerNew; // set the Tx Power - cmd.payload[3] |= (1 << 7); // set the RFU bit, which means that the channel mask gets ignored + cmd.payload[3] = (1 << 7); // set the RFU bit, which means that the channel mask gets ignored cmd.payload[3] |= 0; // keep NbTrans the same (void)execMacCommand(&cmd, saveToEeprom); @@ -2227,65 +2168,17 @@ bool LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd, bool saveToEeprom) { // (which is set on the internal MAC command when creating new session) if((cmd->payload[3] >> 7) == 0) { if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) { - for(size_t i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { - if(chMaskCntl == 0) { - // if chMaskCntl == 0, apply the mask by looking at each channel bit - RADIOLIB_DEBUG_PRINTLN("ADR channel %d: %d --> %d", this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled, (chMask >> i) & 0x01); - if(chMask & (1UL << i)) { - // if it should be enabled but is not currently defined, stop immediately - if(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx == RADIOLIB_LORAWAN_CHANNEL_INDEX_NONE) { - chMaskAck = 0; - break; - } - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = true; - } else { - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = false; - } + chMaskAck = (uint8_t)this->applyChannelMaskDyn(chMaskCntl, chMask); - } else if(chMaskCntl == 6) { - // if chMaskCntl == 6, enable all defined channels - if(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx != RADIOLIB_LORAWAN_CHANNEL_INDEX_NONE) { - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = true; - } - } - - } - } else { // RADIOLIB_LORAWAN_BAND_FIXED - // delete any prior ADR responses from the uplink queue, but do not care if none is present yet + } else { // RADIOLIB_LORAWAN_BAND_FIXED + // if there was already an ADR response in the uplink MAC queue, + // this is a consecutive ADR command, so we delete the prior response int16_t state = deleteMacCommand(RADIOLIB_LORAWAN_MAC_LINK_ADR, &this->commandsUp); if(state == RADIOLIB_ERR_NONE) { - isSuccessive = true; // if we found an ADR Ans in the uplink MAC queue, this is a successive ADR MAC request + isSuccessive = true; } - RADIOLIB_DEBUG_PRINTLN("mask[%d] = 0x%04x", chMaskCntl, chMask); - uint8_t num = 0; - uint8_t chNum = chMaskCntl*16; - uint8_t chSpan = 0; - for(size_t i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { - RADIOLIB_DEBUG_PRINTLN("chNum: %d, chSpan: %d, i: %d, mask: %d", chNum, chSpan, i, chMask & (1UL << i)); - // if we must roll over to next span, reset chNum and move to next channel span - if(chNum >= this->band->txSpans[chSpan].numChannels) { - chNum = 0; - chSpan++; - } + chMaskAck = (uint8_t)this->applyChannelMaskFix(chMaskCntl, chMask, !isSuccessive); - if(chMask & (1UL << i)) { - if(chSpan >= this->band->numTxSpans) { - RADIOLIB_DEBUG_PRINTLN("channel bitmask overrun!"); - return(RADIOLIB_ERR_UNKNOWN); - } - LoRaWANChannel_t chnl; - chnl.enabled = true; - chnl.idx = chMaskCntl*16 + i; - chnl.freq = this->band->txSpans[chSpan].freqStart + chNum*this->band->txSpans[chSpan].freqStep; - chnl.drMin = this->band->txSpans[chSpan].drMin; - chnl.drMax = this->band->txSpans[chSpan].drMax; - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num] = chnl; - // downlink channels are dynamically calculated on each uplink in selectChannels() - RADIOLIB_DEBUG_PRINTLN("Channel UL %d (%d) frequency = %f MHz", num, chnl.idx, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].freq); - num++; - } - chNum++; - } } } @@ -2299,7 +2192,6 @@ bool LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd, bool saveToEeprom) { if(saveToEeprom) { uint8_t payLen = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn; if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) { - // if RFU bit is set, this is just a change in Datarate or TxPower, so read ADR command and overwrite first byte if((cmd->payload[3] >> 7) == 1) { mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_LINK_ADR_ID) + 1, &(cmd->payload[1]), 3); @@ -2310,23 +2202,31 @@ bool LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd, bool saveToEeprom) { } else { // read how many ADR masks are already stored - uint8_t macNumADR = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_NUM_ADR_MASKS_ID); - - // if RFU bit is set, this is just a change in Datarate or TxPower, so read ADR command and overwrite first byte + uint8_t numMacADR = mod->hal->getPersistentParameter(RADIOLIB_EEPROM_LORAWAN_NUM_ADR_MASKS_ID); + RADIOLIB_DEBUG_PRINTLN("[1] Successive: %d, numMacADR: %d, RFU: %d, payload: %02X %02X %02X %02X", + isSuccessive, numMacADR, (cmd->payload[3] >> 7), + cmd->payload[0], cmd->payload[1], cmd->payload[2], cmd->payload[3]); + // if RFU bit is set, this is just a change in Datarate or TxPower + // so read bytes 1..3 from last stored ADR command into the current MAC payload and re-store it if((cmd->payload[3] >> 7) == 1) { - mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_UL_CHANNELS_ID) + macNumADR * payLen + 1, &(cmd->payload[1]), 3); + if(numMacADR > 0) { + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_UL_CHANNELS_ID) + (numMacADR - 1) * payLen + 1, &(cmd->payload[1]), 3); + mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_UL_CHANNELS_ID) + (numMacADR - 1) * payLen, &(cmd->payload[0]), payLen); + } + } else { - if(isSuccessive) { - // saved another ADR mask, so increase counter - mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_NUM_ADR_MASKS_ID, macNumADR + 1); - } else { - // this is the first ADR mask in this downlink, so (re)set counter to 1 - mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_NUM_ADR_MASKS_ID, 1); + // if no previous mask was processed, reset counter to 0 + if(!isSuccessive) { + numMacADR = 0; } + // save to the uplink channel location, to the numMacADR-th slot of 4 bytes + mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_UL_CHANNELS_ID) + numMacADR * payLen, &(cmd->payload[0]), payLen); + // saved an ADR mask, so increase counter + mod->hal->setPersistentParameter(RADIOLIB_EEPROM_LORAWAN_NUM_ADR_MASKS_ID, numMacADR + 1); } - - // save to the uplink channel location, to the macNumADR-th slot of 4 bytes - mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_UL_CHANNELS_ID) + macNumADR * payLen, &(cmd->payload[0]), payLen); + RADIOLIB_DEBUG_PRINTLN("[2] Successive: %d, numMacADR: %d, RFU: %d, payload: %02X %02X %02X %02X", + isSuccessive, numMacADR, (cmd->payload[3] >> 7), + cmd->payload[0], cmd->payload[1], cmd->payload[2], cmd->payload[3]); } } #endif @@ -2627,6 +2527,204 @@ bool LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd, bool saveToEeprom) { return(false); } +bool LoRaWANNode::applyChannelMaskDyn(uint8_t chMaskCntl, uint16_t chMask) { + for(size_t i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { + if(chMaskCntl == 0) { + // apply the mask by looking at each channel bit + RADIOLIB_DEBUG_PRINTLN("ADR channel %d: %d --> %d", this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled, (chMask >> i) & 0x01); + if(chMask & (1UL << i)) { + // if it should be enabled but is not currently defined, stop immediately + if(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx == RADIOLIB_LORAWAN_CHANNEL_INDEX_NONE) { + return(false); + } + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = true; + } else { + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = false; + } + + } else if(chMaskCntl == 6) { + // enable all defined channels + if(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx != RADIOLIB_LORAWAN_CHANNEL_INDEX_NONE) { + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = true; + } + } + + } + + for (int i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { + RADIOLIB_DEBUG_PRINTLN("UL: %d %d %5.2f (%d - %d) | DL: %d %d %5.2f (%d - %d)", + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx, + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled, + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].freq, + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].drMin, + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].drMax, + + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].idx, + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].enabled, + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].freq, + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].drMin, + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].drMax + ); + } + + return(true); +} + +bool LoRaWANNode::applyChannelMaskFix(uint8_t chMaskCntl, uint16_t chMask, bool clear) { + RADIOLIB_DEBUG_PRINTLN("mask[%d] = 0x%04x", chMaskCntl, chMask); + if(clear) { + for(size_t i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i] = RADIOLIB_LORAWAN_CHANNEL_NONE; + } + } + // find out how many channels have already been configured + uint8_t idx = 0; + for(size_t i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { + if(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].freq > 0) { + idx++; + } + } + + if((this->band->numTxSpans == 1 && chMaskCntl <= 5) || (this->band->numTxSpans == 2 && chMaskCntl <= 3)) { + // select channels from first span + LoRaWANChannel_t chnl; + for(uint8_t i = 0; i < 16; i++) { + uint16_t mask = 1 << i; + if(mask & chMask) { + uint8_t chNum = chMaskCntl * 16 + i; // 0 through 63 or 95 + this->subBand = chNum % 8; // keep track of configured subband in case we must reset the channels + chnl.enabled = true; + chnl.idx = chNum; + chnl.freq = this->band->txSpans[0].freqStart + chNum*this->band->txSpans[0].freqStep; + chnl.drMin = this->band->txSpans[0].drMin; + chnl.drMax = this->band->txSpans[0].drMax; + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][idx++] = chnl; + RADIOLIB_DEBUG_PRINTLN("Channel UL %d (%d) frequency = %f MHz", chnl.idx, idx, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][idx-1].freq); + } + } + + } + if(this->band->numTxSpans == 1 && chMaskCntl == 6) { + // all channels on (but we revert to user-selected subband) + this->setupChannelsFix(this->subBand); + + } + if(this->band->numTxSpans == 2 && chMaskCntl == 4) { + // select channels from second span + LoRaWANChannel_t chnl; + for(uint8_t i = 0; i < 8; i++) { + uint16_t mask = 1 << i; + if(mask & chMask) { + uint8_t chNum = chMaskCntl * 16 + i; // 64 through 71 + chnl.enabled = true; + chnl.idx = chNum; + chnl.freq = this->band->txSpans[1].freqStart + i*this->band->txSpans[1].freqStep; + chnl.drMin = this->band->txSpans[1].drMin; + chnl.drMax = this->band->txSpans[1].drMax; + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][idx++] = chnl; + RADIOLIB_DEBUG_PRINTLN("Channel UL %d (%d) frequency = %f MHz", chnl.idx, idx-1, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][idx-1].freq); + } + } + + } + if(this->band->numTxSpans == 2 && chMaskCntl == 5) { + // a '1' enables a bank of 8 + 1 channels from 1st and 2nd span respectively + LoRaWANChannel_t chnl; + for(uint8_t i = 0; i < 8; i++) { + uint16_t mask = 1 << i; + if(mask & chMask) { + // enable bank of 8 channels from first span + for(uint8_t j = 0; j < 8; i++) { + uint8_t chNum = i * 8 + j; + chnl.enabled = true; + chnl.idx = chNum; + chnl.freq = this->band->txSpans[0].freqStart + chNum*this->band->txSpans[0].freqStep; + chnl.drMin = this->band->txSpans[0].drMin; + chnl.drMax = this->band->txSpans[0].drMax; + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][idx++] = chnl; + RADIOLIB_DEBUG_PRINTLN("Channel UL %d (%d) frequency = %f MHz", chnl.idx, idx, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][idx-1].freq); + } + // enable single channel from second span + uint8_t chNum = 64 + i; + chnl.enabled = true; + chnl.idx = chNum; + chnl.freq = this->band->txSpans[1].freqStart + i*this->band->txSpans[1].freqStep; + chnl.drMin = this->band->txSpans[1].drMin; + chnl.drMax = this->band->txSpans[1].drMax; + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][idx++] = chnl; + RADIOLIB_DEBUG_PRINTLN("Channel UL %d (%d) frequency = %f MHz", chnl.idx, idx, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][idx-1].freq); + } + } + + } + if(this->band->numTxSpans == 2 && chMaskCntl == 6) { + // all channels on (but we revert to selected subband) + if(this->subBand >= 0) { + this->setupChannelsFix(this->subBand); + } + // a '1' enables a single channel from second span + LoRaWANChannel_t chnl; + for(uint8_t i = 0; i < 8; i++) { + uint16_t mask = 1 << i; + if(mask & chMask) { + // enable single channel from second span + uint8_t chNum = 64 + i; + chnl.enabled = true; + chnl.idx = chNum; + chnl.freq = this->band->txSpans[1].freqStart + i*this->band->txSpans[1].freqStep; + chnl.drMin = this->band->txSpans[1].drMin; + chnl.drMax = this->band->txSpans[1].drMax; + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][idx++] = chnl; + RADIOLIB_DEBUG_PRINTLN("Channel UL %d (%d) frequency = %f MHz", chnl.idx, idx, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][idx-1].freq); + } + } + + } + if(this->band->numTxSpans == 2 && chMaskCntl == 7) { + // all channels off (clear all channels) + LoRaWANChannel_t chnl = RADIOLIB_LORAWAN_CHANNEL_NONE; + for(int i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i] = chnl; + // downlink channels are not defined so don't need to reset + } + idx = 0; + // a '1' enables a single channel from second span + for(uint8_t i = 0; i < 8; i++) { + uint16_t mask = 1 << i; + if(mask & chMask) { + // enable single channel from second span + uint8_t chNum = 64 + i; + chnl.enabled = true; + chnl.idx = chNum; + chnl.freq = this->band->txSpans[1].freqStart + i*this->band->txSpans[1].freqStep; + chnl.drMin = this->band->txSpans[1].drMin; + chnl.drMax = this->band->txSpans[1].drMax; + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][idx++] = chnl; + RADIOLIB_DEBUG_PRINTLN("Channel UL %d (%d) frequency = %f MHz", chnl.idx, idx, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][idx-1].freq); + } + } + + } + + for (int i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { + RADIOLIB_DEBUG_PRINTLN("UL: %d %d %5.2f (%d - %d) | DL: %d %d %5.2f (%d - %d)", + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx, + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled, + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].freq, + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].drMin, + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].drMax, + + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].idx, + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].enabled, + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].freq, + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].drMin, + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].drMax + ); + } + + return(true); +} + uint8_t LoRaWANNode::getMacPayloadLength(uint8_t cid) { for (LoRaWANMacSpec_t entry : MacTable) { if (entry.cid == cid) { @@ -2752,7 +2850,10 @@ void LoRaWANNode::processAES(uint8_t* in, size_t len, uint8_t* key, uint8_t* out } uint16_t LoRaWANNode::checkSum16(uint8_t *key, uint8_t keyLen) { - uint16_t buf16[RADIOLIB_AES128_KEY_SIZE/2] = { 0 }; + if(keyLen > RADIOLIB_AES128_KEY_SIZE / 2) { + keyLen = RADIOLIB_AES128_KEY_SIZE / 2; + } + uint16_t buf16[RADIOLIB_AES128_KEY_SIZE / 2] = { 0 }; uint8_t bufLen = keyLen / 2; memcpy(buf16, key, keyLen); uint16_t checkSum = 0; diff --git a/src/protocols/LoRaWAN/LoRaWAN.h b/src/protocols/LoRaWAN/LoRaWAN.h index de4b3ea0a..e62ddb24c 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.h +++ b/src/protocols/LoRaWAN/LoRaWAN.h @@ -79,7 +79,7 @@ #define RADIOLIB_LORAWAN_BAND_DYNAMIC (0) #define RADIOLIB_LORAWAN_BAND_FIXED (1) #define RADIOLIB_LORAWAN_CHANNEL_NUM_DATARATES (15) -#define RADIOLIB_LORAWAN_CHANNEL_INDEX_NONE (0xFF >> 1) // reserve first bit for enable-flag +#define RADIOLIB_LORAWAN_CHANNEL_INDEX_NONE (0xFF >> 0) // recommended default settings #define RADIOLIB_LORAWAN_RECEIVE_DELAY_1_MS (1000) @@ -403,8 +403,9 @@ class LoRaWANNode { \brief Default constructor. \param phy Pointer to the PhysicalLayer radio module. \param band Pointer to the LoRaWAN band to use. + \param subBand The subband to be used (starting from 1!) */ - LoRaWANNode(PhysicalLayer* phy, const LoRaWANBand_t* band); + LoRaWANNode(PhysicalLayer* phy, const LoRaWANBand_t* band, uint8_t subBand = 0); #if !defined(RADIOLIB_EEPROM_UNSUPPORTED) /*! @@ -650,23 +651,6 @@ class LoRaWANNode { */ int16_t setTxPower(int8_t txPower, bool saveToEeprom = false); - /*! - \brief Select a single subband (8 channels) for fixed bands such as US915. - Only available before joining a network. - \param idx The subband to be used (starting from 1!) - \returns \ref status_codes - */ - int16_t selectSubband(uint8_t idx); - - /*! - \brief Select a set of channels for fixed bands such as US915. - Only available before joining a network. - \param startChannel The first channel of the band to be used (inclusive) - \param endChannel The last channel of the band to be used (inclusive) - \returns \ref status_codes - */ - int16_t selectSubband(uint8_t startChannel, uint8_t endChannel); - /*! \brief Configures CSMA for LoRaWAN as per TR-13, LoRa Alliance. \param backoffMax Num of BO slots to be decremented after DIFS phase. 0 to disable BO. @@ -702,7 +686,7 @@ class LoRaWANNode { PhysicalLayer* phyLayer = NULL; const LoRaWANBand_t* band = NULL; - void beginCommon(); + void beginCommon(uint8_t joinDr = RADIOLIB_LORAWAN_DATA_RATE_UNUSED); LoRaWANMacCommandQueue_t commandsUp = { .numCommands = 0, @@ -803,7 +787,7 @@ class LoRaWANNode { bool isMACPayload = false; // save the selected subband in case this must be restored in ADR control - int8_t selectedSubband = -1; + int8_t subBand = -1; #if !defined(RADIOLIB_EEPROM_UNSUPPORTED) /*! @@ -832,15 +816,20 @@ class LoRaWANNode { bool verifyMIC(uint8_t* msg, size_t len, uint8_t* key); // configure the common physical layer properties (preamble, sync word etc.) - // channels must be configured separately by setupChannels()! + // channels must be configured separately by setupChannelsDyn()! int16_t setPhyProperties(); // setup uplink/downlink channel data rates and frequencies - // will attempt to randomly select based on currently used band plan - int16_t setupChannels(uint8_t* cfList); + // for dynamic channels, there is a small set of predefined channels + // in case of JoinRequest, add some optional extra frequencies + int16_t setupChannelsDyn(bool joinRequest = false); + + // setup uplink/downlink channel data rates and frequencies + // for fixed bands, we only allow one subband at a time to be selected + int16_t setupChannelsFix(uint8_t subBand); - // select a set of semi-random TX/RX channels for the join-request and -accept message - int16_t selectChannelsJR(uint16_t devNonce, uint8_t drJoinSubband); + // a join-accept can piggy-back a set of channels or channel masks + int16_t processCFList(uint8_t* cfList); // select a set of random TX/RX channels for up- and downlink int16_t selectChannels(); @@ -864,6 +853,12 @@ class LoRaWANNode { // execute mac command, return the number of processed bytes for sequential processing bool execMacCommand(LoRaWANMacCommand_t* cmd, bool saveToEeprom = true); + // apply a channel mask to a set of readily defined channels (dynamic bands only) + bool applyChannelMaskDyn(uint8_t chMaskCntl, uint16_t chMask); + + // define or delete channels from a fixed set of channels (fixed bands only) + bool applyChannelMaskFix(uint8_t chMaskCntl, uint16_t chMask, bool clear); + // get the payload length for a specific MAC command uint8_t getMacPayloadLength(uint8_t cid); From 3338034ac7966b7fe2cf45b5f8501c54651f5d45 Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Sat, 13 Jan 2024 00:15:52 +0100 Subject: [PATCH 9/9] [LoRaWAN] Update examples --- .../LoRaWAN_End_Device/LoRaWAN_End_Device.ino | 15 ++++++++------- .../LoRaWAN_End_Device_ABP.ino | 15 ++++++++------- .../LoRaWAN_End_Device_Persistent.ino | 8 ++++++++ .../LoRaWAN_End_Device_Reference.ino | 15 ++++++++------- 4 files changed, 32 insertions(+), 21 deletions(-) diff --git a/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino b/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino index 732da594a..c755a8bd0 100644 --- a/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino +++ b/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino @@ -38,6 +38,14 @@ SX1278 radio = new Module(10, 2, 9, 3); // based on your geographical location! LoRaWANNode node(&radio, &EU868); +// for fixed bands with subband selection +// such as US915 and AU915, you must specify +// the subband that matches the Frequency Plan +// that you selected on your LoRaWAN console +/* + LoRaWANNode node(&radio, &US915, 2); +*/ + void setup() { Serial.begin(9600); @@ -78,13 +86,6 @@ void setup() { // when connecting to LoRaWAN 1.0 network, "appKey" will be disregarded // and can be set to NULL - // some frequency bands only use a subset of the available channels - // you can select the specific band or set the first channel and last channel - // for example, either of the following corresponds to US915 FSB2 in TTN - /* - node.selectSubband(2); - node.selectSubband(8, 15); - */ // on EEPROM-enabled boards, after the device has been activated, // the session can be restored without rejoining after device power cycle diff --git a/examples/LoRaWAN/LoRaWAN_End_Device_ABP/LoRaWAN_End_Device_ABP.ino b/examples/LoRaWAN/LoRaWAN_End_Device_ABP/LoRaWAN_End_Device_ABP.ino index 5ac9261f1..ec1252e9e 100644 --- a/examples/LoRaWAN/LoRaWAN_End_Device_ABP/LoRaWAN_End_Device_ABP.ino +++ b/examples/LoRaWAN/LoRaWAN_End_Device_ABP/LoRaWAN_End_Device_ABP.ino @@ -39,6 +39,14 @@ SX1278 radio = new Module(10, 2, 9, 3); // based on your geographical location! LoRaWANNode node(&radio, &EU868); +// for fixed bands with subband selection +// such as US915 and AU915, you must specify +// the subband that matches the Frequency Plan +// that you selected on your LoRaWAN console +/* + LoRaWANNode node(&radio, &US915, 2); +*/ + void setup() { Serial.begin(9600); @@ -82,13 +90,6 @@ void setup() { // when connecting to LoRaWAN 1.0 network, "appKey" will be disregarded // and can be set to NULL - // some frequency bands only use a subset of the available channels - // you can select the specific band or set the first channel and last channel - // for example, either of the following corresponds to US915 FSB2 in TTN - /* - node.selectSubband(2); - node.selectSubband(8, 15); - */ // if using EU868 on ABP in TTN, you need to set the SF for RX2 window manually /* diff --git a/examples/LoRaWAN/LoRaWAN_End_Device_Persistent/LoRaWAN_End_Device_Persistent.ino b/examples/LoRaWAN/LoRaWAN_End_Device_Persistent/LoRaWAN_End_Device_Persistent.ino index 672506c6b..a475e7255 100644 --- a/examples/LoRaWAN/LoRaWAN_End_Device_Persistent/LoRaWAN_End_Device_Persistent.ino +++ b/examples/LoRaWAN/LoRaWAN_End_Device_Persistent/LoRaWAN_End_Device_Persistent.ino @@ -43,6 +43,14 @@ SX1278 radio = new Module(10, 2, 9, 3); // based on your geographical location! LoRaWANNode node(&radio, &EU868); +// for fixed bands with subband selection +// such as US915 and AU915, you must specify +// the subband that matches the Frequency Plan +// that you selected on your LoRaWAN console +/* + LoRaWANNode node(&radio, &US915, 2); +*/ + void setup() { Serial.begin(9600); diff --git a/examples/LoRaWAN/LoRaWAN_End_Device_Reference/LoRaWAN_End_Device_Reference.ino b/examples/LoRaWAN/LoRaWAN_End_Device_Reference/LoRaWAN_End_Device_Reference.ino index 2d8c84cf1..0507d5f2d 100644 --- a/examples/LoRaWAN/LoRaWAN_End_Device_Reference/LoRaWAN_End_Device_Reference.ino +++ b/examples/LoRaWAN/LoRaWAN_End_Device_Reference/LoRaWAN_End_Device_Reference.ino @@ -41,6 +41,14 @@ SX1278 radio = new Module(10, 2, 9, 3); // based on your geographical location! LoRaWANNode node(&radio, &EU868); +// for fixed bands with subband selection +// such as US915 and AU915, you must specify +// the subband that matches the Frequency Plan +// that you selected on your LoRaWAN console +/* + LoRaWANNode node(&radio, &US915, 2); +*/ + void setup() { Serial.begin(9600); @@ -81,13 +89,6 @@ void setup() { // when connecting to LoRaWAN 1.0 network, "appKey" will be disregarded // and can be set to NULL - // some frequency bands only use a subset of the available channels - // you can select the specific band or set the first channel and last channel - // for example, either of the following corresponds to US915 FSB2 in TTN - /* - node.selectSubband(2); - node.selectSubband(8, 15); - */ // now we can start the activation // this can take up to 10 seconds, and requires a LoRaWAN gateway in range