From 3df866e23dfa52031b5a8f10dbc95cc45507a149 Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Wed, 8 Nov 2023 17:42:05 +0100 Subject: [PATCH] [LoRaWAN] improve examples, add unified sendReceive, bugfixes, add FSK --- .../LoRaWAN_End_Device/LoRaWAN_End_Device.ino | 72 +---- .../LoRaWAN_End_Device_ABP.ino | 66 +---- .../LoRaWAN_End_Device_persistent.ino | 153 ++++++++++ .../LoRaWAN_End_Device_reference.ino | 216 ++++++++++++++ src/ArduinoHal.cpp | 8 +- src/modules/SX126x/SX126x.cpp | 5 +- src/modules/SX126x/SX126x.h | 5 +- src/modules/SX127x/SX127x.cpp | 18 +- src/modules/SX127x/SX127x.h | 7 +- src/protocols/LoRaWAN/LoRaWAN.cpp | 274 ++++++++++++------ src/protocols/LoRaWAN/LoRaWAN.h | 115 +++++--- src/protocols/PhysicalLayer/PhysicalLayer.cpp | 3 +- src/protocols/PhysicalLayer/PhysicalLayer.h | 5 +- 13 files changed, 683 insertions(+), 264 deletions(-) create mode 100644 examples/LoRaWAN/LoRaWAN_End_Device_persistent/LoRaWAN_End_Device_persistent.ino create mode 100644 examples/LoRaWAN/LoRaWAN_End_Device_reference/LoRaWAN_End_Device_reference.ino diff --git a/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino b/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino index 50badb08..b5eaaf8c 100644 --- a/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino +++ b/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino @@ -7,14 +7,12 @@ After your device is registered, you can run this example. The device will join the network and start uploading data. - NOTE: LoRaWAN requires storing some parameters persistently! - RadioLib does this by using EEPROM, by default - starting at address 0 and using 32 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 - RADIOLIB_HAL_PERSISTENT_STORAGE_BASE macro, either - during build or in src/BuildOpt.h. + LoRaWAN v1.1 requires the use of EEPROM (persistent storage). + Please refer to the 'persistent' example once you are familiar + with LoRaWAN. + Running this examples REQUIRES you to check "Resets DevNonces" + on your LoRaWAN dashboard. Refer to the network's + documentation on how to do this. For default module settings, see the wiki page https://github.com/jgromes/RadioLib/wiki/Default-configuration @@ -53,13 +51,6 @@ 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! - //node.wipe(); - // application identifier - pre-LoRaWAN 1.1.0, this was called appEUI // when adding new end device in TTN, you will have to enter this number // you can pick any number you want, but it has to be unique @@ -91,21 +82,16 @@ void setup() { // for example, either of the following corresponds to US915 FSB2 in TTN /* node.selectSubband(2); - node.selectSubband(8, 16); + node.selectSubband(8, 15); */ // now we can start the activation - // this can take up to 20 seconds, and requires a LoRaWAN gateway in range + // 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): /* - uint8_t joinDr = 8; + uint8_t joinDr = 4; state = node.beginOTAA(joinEUI, devEUI, nwkKey, appKey, joinDr); */ - // a specific band can be selected for joining in fixed bands (e.g. US915): - /* - uint8_t subband = 2; - state = node.beginOTAA(joinEUI, devEUI, nwkKey, appKey, subband); - */ Serial.print(F("[LoRaWAN] Attempting over-the-air activation ... ")); state = node.beginOTAA(joinEUI, devEUI, nwkKey, appKey); @@ -117,21 +103,6 @@ void setup() { while(true); } - // after the device has been activated, - // the session can be restored without rejoining after device power cycle - // on EEPROM-enabled boards by calling "restore" - /* - Serial.print(F("[LoRaWAN] Resuming previous session ... ")); - state = node.restore(); - if(state == RADIOLIB_ERR_NONE) { - Serial.println(F("success!")); - } else { - Serial.print(F("failed, code ")); - Serial.println(state); - while(true); - } - */ - delay(5000); } // counter to keep track of transmitted packets @@ -141,23 +112,10 @@ void loop() { // send uplink to port 10 Serial.print(F("[LoRaWAN] Sending uplink packet ... ")); String strUp = "Hello World! #" + String(count++); - int state = node.uplink(strUp, 10); - if(state == RADIOLIB_ERR_NONE) { - Serial.println(F("success!")); - } else { - Serial.print(F("failed, code ")); - Serial.println(state); - } - - // after uplink, you can call downlink(), - // to receive any possible reply from the server - // this function must be called within a few seconds - // after uplink to receive the downlink! - Serial.print(F("[LoRaWAN] Waiting for downlink ... ")); String strDown; - state = node.downlink(strDown); + int state = node.sendReceive(strUp, 10, strDown); if(state == RADIOLIB_ERR_NONE) { - Serial.println(F("success!")); + Serial.println(F("received a downlink!")); // print data of the packet (if there are any) Serial.print(F("[LoRaWAN] Data:\t\t")); @@ -183,19 +141,13 @@ void loop() { Serial.println(F(" Hz")); } else if(state == RADIOLIB_ERR_RX_TIMEOUT) { - Serial.println(F("timeout!")); + Serial.println(F("no downlink!")); } else { Serial.print(F("failed, code ")); Serial.println(state); } - // on EEPROM enabled boards, you can save the current session - // by calling "saveSession" which allows retrieving the session after reboot or deepsleep - /* - node.saveSession(); - */ - // wait before sending another packet delay(30000); } 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 91b49d71..05198f5f 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 @@ -8,15 +8,13 @@ The device will start uploading data directly, without having to join the network. - NOTE: LoRaWAN requires storing some parameters persistently! - RadioLib does this by using EEPROM, by default - starting at address 0 and using 32 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 - RADIOLIB_HAL_PERSISTENT_STORAGE_BASE macro, either - during build or in src/BuildOpt.h. - + LoRaWAN v1.1 requires the use of EEPROM (persistent storage). + Please refer to the 'persistent' example once you are familiar + with LoRaWAN. + Running this examples REQUIRES you to check "Resets DevNonces" + on your LoRaWAN dashboard. Refer to the network's + documentation on how to do this. + For default module settings, see the wiki page https://github.com/jgromes/RadioLib/wiki/Default-configuration @@ -54,13 +52,6 @@ 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! - //node.wipe(); - // device address - this number can be anything // when adding new end device in TTN, you can generate this number, // or you can set any value you want, provided it is unique @@ -87,7 +78,7 @@ void setup() { // for example, either of the following corresponds to US915 FSB2 in TTN /* node.selectSubband(2); - node.selectSubband(8, 16); + node.selectSubband(8, 15); */ // if using EU868 on ABP in TTN, you need to set the SF for RX2 window manually @@ -112,20 +103,6 @@ void setup() { while(true); } - // after the device has been activated, - // the session can be restored without rejoining after device power cycle - // on EEPROM-enabled boards by calling "restore" - /* - Serial.print(F("[LoRaWAN] Resuming previous session ... ")); - state = node.restore(); - if(state == RADIOLIB_ERR_NONE) { - Serial.println(F("success!")); - } else { - Serial.print(F("failed, code ")); - Serial.println(state); - while(true); - } - */ } // counter to keep track of transmitted packets @@ -135,23 +112,10 @@ void loop() { // send uplink to port 10 Serial.print(F("[LoRaWAN] Sending uplink packet ... ")); String strUp = "Hello World! #" + String(count++); - int state = node.uplink(strUp, 10); - if(state == RADIOLIB_ERR_NONE) { - Serial.println(F("success!")); - } else { - Serial.print(F("failed, code ")); - Serial.println(state); - } - - // after uplink, you can call downlink(), - // to receive any possible reply from the server - // this function must be called within a few seconds - // after uplink to receive the downlink! - Serial.print(F("[LoRaWAN] Waiting for downlink ... ")); String strDown; - state = node.downlink(strDown); + int state = node.sendReceive(strUp, 10, strDown); if(state == RADIOLIB_ERR_NONE) { - Serial.println(F("success!")); + Serial.println(F("received a downlink!")); // print data of the packet (if there are any) Serial.print(F("[LoRaWAN] Data:\t\t")); @@ -177,19 +141,13 @@ void loop() { Serial.println(F(" Hz")); } else if(state == RADIOLIB_ERR_RX_TIMEOUT) { - Serial.println(F("timeout!")); + Serial.println(F("no downlink!")); } else { Serial.print(F("failed, code ")); Serial.println(state); } - // on EEPROM enabled boards, you can save the current session - // by calling "saveSession" which allows retrieving the session after reboot or deepsleep - /* - node.saveSession(); - */ - // wait before sending another packet - delay(10000); + delay(30000); } 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 new file mode 100644 index 00000000..6ccd484f --- /dev/null +++ b/examples/LoRaWAN/LoRaWAN_End_Device_persistent/LoRaWAN_End_Device_persistent.ino @@ -0,0 +1,153 @@ +/* + RadioLib LoRaWAN End Device Example + + This example assumes you have tried one of the OTAA or ABP + examples and are familiar with the required keys and procedures. + This example restores and saves a session such that you can use + deepsleep or survive power cycles. Before you start, you will + have to register your device at https://www.thethingsnetwork.org/ + and join the network using either OTAA or ABP. + Please refer to one of the other examples for more + information regarding joining a network. + + NOTE: LoRaWAN requires storing some parameters persistently! + RadioLib does this by using EEPROM, by default + starting at address 0 and using 384 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 + RADIOLIB_HAL_PERSISTENT_STORAGE_BASE macro, either + during build or in src/BuildOpt.h. + + For default module settings, see the wiki page + https://github.com/jgromes/RadioLib/wiki/Default-configuration + + For full API reference, see the GitHub Pages + https://jgromes.github.io/RadioLib/ +*/ + +// include the library +#include + +// SX1278 has the following connections: +// NSS pin: 10 +// DIO0 pin: 2 +// RESET pin: 9 +// DIO1 pin: 3 +SX1278 radio = new Module(10, 2, 9, 3); + +// create the node instance on the EU-868 band +// using the radio module and the encryption key +// make sure you are using the correct band +// based on your geographical location! +LoRaWANNode node(&radio, &EU868); + +void setup() { + Serial.begin(9600); + + // initialize SX1278 with default settings + Serial.print(F("[SX1278] Initializing ... ")); + int state = radio.begin(); + if(state == RADIOLIB_ERR_NONE) { + Serial.println(F("success!")); + } else { + Serial.print(F("failed, code ")); + Serial.println(state); + 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 + // Serial.print(F("[LoRaWAN] Attempting over-the-air activation ... ")); + // uint64_t joinEUI = 0x12AD1011B0C0FFEE; + // 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, + // the session can be restored without rejoining after device power cycle + // on EEPROM-enabled boards by calling "restore" + Serial.print(F("[LoRaWAN] Resuming previous session ... ")); + state = node.restore(); + if(state == RADIOLIB_ERR_NONE) { + Serial.println(F("success!")); + } else { + Serial.print(F("failed, code ")); + Serial.println(state); + while(true); + } + + if(state == RADIOLIB_ERR_NONE) { + Serial.println(F("success!")); + } else { + Serial.print(F("failed, code ")); + Serial.println(state); + while(true); + } + +} + +// counter to keep track of transmitted packets +int count = 0; + +void loop() { + // send uplink to port 10 + Serial.print(F("[LoRaWAN] Sending uplink packet ... ")); + String strUp = "Hello World! #" + String(count++); + String strDown; + int state = node.sendReceive(strUp, 10, strDown); + if(state == RADIOLIB_ERR_NONE) { + Serial.println(F("received a downlink!")); + + // print data of the packet (if there are any) + Serial.print(F("[LoRaWAN] Data:\t\t")); + if(strDown.length() > 0) { + Serial.println(strDown); + } else { + Serial.println(F("")); + } + + // print RSSI (Received Signal Strength Indicator) + Serial.print(F("[LoRaWAN] RSSI:\t\t")); + Serial.print(radio.getRSSI()); + Serial.println(F(" dBm")); + + // print SNR (Signal-to-Noise Ratio) + Serial.print(F("[LoRaWAN] SNR:\t\t")); + Serial.print(radio.getSNR()); + Serial.println(F(" dB")); + + // print frequency error + Serial.print(F("[LoRaWAN] Frequency error:\t")); + Serial.print(radio.getFrequencyError()); + Serial.println(F(" Hz")); + + } else if(state == RADIOLIB_ERR_RX_TIMEOUT) { + Serial.println(F("no downlink!")); + + } else { + Serial.print(F("failed, code ")); + Serial.println(state); + } + + // on EEPROM enabled boards, you can save the current session + // by calling "saveSession" which allows retrieving the session after reboot or deepsleep + node.saveSession(); + + // 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); +} 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 new file mode 100644 index 00000000..29ec64fb --- /dev/null +++ b/examples/LoRaWAN/LoRaWAN_End_Device_reference/LoRaWAN_End_Device_reference.ino @@ -0,0 +1,216 @@ +/* + RadioLib LoRaWAN End Device Example + + This example joins a LoRaWAN network and will send + uplink packets. Before you start, you will have to + register your device at https://www.thethingsnetwork.org/ + After your device is registered, you can run this example. + The device will join the network and start uploading data. + + Also, most of the possible and available functions are + shown here for reference. + + LoRaWAN v1.1 requires the use of EEPROM (persistent storage). + Please refer to the 'persistent' example once you are familiar + with LoRaWAN. + Running this examples REQUIRES you to check "Resets DevNonces" + on your LoRaWAN dashboard. Refer to the network's + documentation on how to do this. + + For default module settings, see the wiki page + https://github.com/jgromes/RadioLib/wiki/Default-configuration + + For full API reference, see the GitHub Pages + https://jgromes.github.io/RadioLib/ +*/ + +// include the library +#include + +// SX1278 has the following connections: +// NSS pin: 10 +// DIO0 pin: 2 +// RESET pin: 9 +// DIO1 pin: 3 +SX1278 radio = new Module(10, 2, 9, 3); + +// create the node instance on the EU-868 band +// using the radio module and the encryption key +// make sure you are using the correct band +// based on your geographical location! +LoRaWANNode node(&radio, &EU868); + +void setup() { + Serial.begin(9600); + + // initialize SX1278 with default settings + Serial.print(F("[SX1278] Initializing ... ")); + int state = radio.begin(); + if(state == RADIOLIB_ERR_NONE) { + Serial.println(F("success!")); + } else { + Serial.print(F("failed, code ")); + Serial.println(state); + while(true); + } + + // application identifier - pre-LoRaWAN 1.1.0, this was called appEUI + // when adding new end device in TTN, you will have to enter this number + // you can pick any number you want, but it has to be unique + uint64_t joinEUI = 0x12AD1011B0C0FFEE; + + // device identifier - this number can be anything + // when adding new end device in TTN, you can generate this number, + // or you can set any value you want, provided it is also unique + uint64_t devEUI = 0x70B3D57ED005E120; + + // select some encryption keys which will be used to secure the communication + // there are two of them - network key and application key + // because LoRaWAN uses AES-128, the key MUST be 16 bytes (or characters) long + + // network key is the ASCII string "topSecretKey1234" + uint8_t nwkKey[] = { 0x74, 0x6F, 0x70, 0x53, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x4B, 0x65, 0x79, 0x31, 0x32, 0x33, 0x34 }; + + // application key is the ASCII string "aDifferentKeyABC" + uint8_t appKey[] = { 0x61, 0x44, 0x69, 0x66, 0x66, 0x65, 0x72, 0x65, + 0x6E, 0x74, 0x4B, 0x65, 0x79, 0x41, 0x42, 0x43 }; + + // 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 + + // 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 + // a specific starting-datarate can be selected in dynamic bands (e.g. EU868): + /* + uint8_t joinDr = 4; + state = node.beginOTAA(joinEUI, devEUI, nwkKey, appKey, joinDr); + */ + Serial.print(F("[LoRaWAN] Attempting over-the-air activation ... ")); + state = node.beginOTAA(joinEUI, devEUI, nwkKey, appKey); + + if(state == RADIOLIB_ERR_NONE) { + Serial.println(F("success!")); + } else { + Serial.print(F("failed, code ")); + Serial.println(state); + while(true); + } + + // after the device has been activated, + // the session can be restored without rejoining after device power cycle + // on EEPROM-enabled boards by calling "restore" + /* + Serial.print(F("[LoRaWAN] Resuming previous session ... ")); + state = node.restore(); + if(state == RADIOLIB_ERR_NONE) { + Serial.println(F("success!")); + } else { + Serial.print(F("failed, code ")); + Serial.println(state); + while(true); + } + */ + + // disable the ADR algorithm + node.setADR(false); + + // set a fixed datarate + node.setDatarate(5); + + // enable CSMA + // this tries to minimize packet loss by searching for a free channel + // before actually sending an uplink + node.setCSMA(6, 2, true); + +} + +void loop() { + int state = RADIOLIB_ERR_NONE; + + // set battery fill level, + // 0 = external power source + // 1 = lowest (empty battery) + // 254 = highest (full battery) + // 255 = unable to measure + uint8_t battLevel = 146; + node.setDeviceStatus(battLevel); + + // retrieve the last uplink frame counter + uint32_t fcntUp = node.getFcntUp(); + + Serial.print(F("[LoRaWAN] Sending uplink packet ... ")); + String strUp = "Hello World! #" + String(fcntUp); + + // send a confirmed uplink to port 10 every 64th frame + if(fcntUp / 64 == 0) { + state = node.uplink(strUp, 10, true); + } else { + state = node.uplink(strUp, 10); + } + if(state == RADIOLIB_ERR_NONE) { + Serial.println(F("success!")); + } else { + Serial.print(F("failed, code ")); + Serial.println(state); + } + + // after uplink, you can call downlink(), + // to receive any possible reply from the server + // this function must be called within a few seconds + // after uplink to receive the downlink! + Serial.print(F("[LoRaWAN] Waiting for downlink ... ")); + String strDown; + state = node.downlink(strDown); + if(state == RADIOLIB_ERR_NONE) { + Serial.println(F("success!")); + + // print data of the packet (if there are any) + Serial.print(F("[LoRaWAN] Data:\t\t")); + if(strDown.length() > 0) { + Serial.println(strDown); + } else { + Serial.println(F("")); + } + + // print RSSI (Received Signal Strength Indicator) + Serial.print(F("[LoRaWAN] RSSI:\t\t")); + Serial.print(radio.getRSSI()); + Serial.println(F(" dBm")); + + // print SNR (Signal-to-Noise Ratio) + Serial.print(F("[LoRaWAN] SNR:\t\t")); + Serial.print(radio.getSNR()); + Serial.println(F(" dB")); + + // print frequency error + Serial.print(F("[LoRaWAN] Frequency error:\t")); + Serial.print(radio.getFrequencyError()); + Serial.println(F(" Hz")); + + } else if(state == RADIOLIB_ERR_RX_TIMEOUT) { + Serial.println(F("timeout!")); + + } else { + Serial.print(F("failed, code ")); + Serial.println(state); + } + + // on EEPROM enabled boards, you can save the current session + // by calling "saveSession" which allows retrieving the session after reboot or deepsleep + /* + node.saveSession(); + */ + + // wait before sending another packet + delay(30000); +} diff --git a/src/ArduinoHal.cpp b/src/ArduinoHal.cpp index a044e111..0485fad8 100644 --- a/src/ArduinoHal.cpp +++ b/src/ArduinoHal.cpp @@ -120,7 +120,7 @@ void inline ArduinoHal::spiEnd() { void ArduinoHal::readPersistentStorage(uint32_t addr, uint8_t* buff, size_t len) { #if !defined(RADIOLIB_EEPROM_UNSUPPORTED) - #if defined(RADIOLIB_ESP32) + #if defined(RADIOLIB_ESP32) || defined(ARDUINO_ARCH_RP2040) EEPROM.begin(RADIOLIB_HAL_PERSISTENT_STORAGE_SIZE); #elif defined(ARDUINO_ARCH_APOLLO3) EEPROM.init(); @@ -128,7 +128,7 @@ void ArduinoHal::readPersistentStorage(uint32_t addr, uint8_t* buff, size_t len) for(size_t i = 0; i < len; i++) { buff[i] = EEPROM.read(addr + i); } - #if defined(RADIOLIB_ESP32) + #if defined(RADIOLIB_ESP32) || defined(ARDUINO_ARCH_RP2040) EEPROM.end(); #endif #endif @@ -136,7 +136,7 @@ void ArduinoHal::readPersistentStorage(uint32_t addr, uint8_t* buff, size_t len) void ArduinoHal::writePersistentStorage(uint32_t addr, uint8_t* buff, size_t len) { #if !defined(RADIOLIB_EEPROM_UNSUPPORTED) - #if defined(RADIOLIB_ESP32) + #if defined(RADIOLIB_ESP32) || defined(ARDUINO_ARCH_RP2040) EEPROM.begin(RADIOLIB_HAL_PERSISTENT_STORAGE_SIZE); #elif defined(ARDUINO_ARCH_APOLLO3) EEPROM.init(); @@ -144,7 +144,7 @@ void ArduinoHal::writePersistentStorage(uint32_t addr, uint8_t* buff, size_t len for(size_t i = 0; i < len; i++) { EEPROM.write(addr + i, buff[i]); } - #if defined(RADIOLIB_ESP32) + #if defined(RADIOLIB_ESP32) || defined(ARDUINO_ARCH_RP2040) EEPROM.commit(); EEPROM.end(); #endif diff --git a/src/modules/SX126x/SX126x.cpp b/src/modules/SX126x/SX126x.cpp index 412db354..28e55b79 100644 --- a/src/modules/SX126x/SX126x.cpp +++ b/src/modules/SX126x/SX126x.cpp @@ -1436,8 +1436,9 @@ uint32_t SX126x::getTimeOnAir(size_t len) { } } -uint32_t SX126x::calculateRxTimeout(uint8_t numSymbols, uint32_t timeoutUs) { - (void)numSymbols; // not used for these modules +uint32_t SX126x::calculateRxTimeout(uint32_t timeoutUs) { + // the timeout value is given in units of 15.625 microseconds + // the calling function should provide some extra width, as this number of units is truncated to integer uint32_t timeout = timeoutUs / 15.625; return(timeout); } diff --git a/src/modules/SX126x/SX126x.h b/src/modules/SX126x/SX126x.h index 976e90c5..0c2f9e20 100644 --- a/src/modules/SX126x/SX126x.h +++ b/src/modules/SX126x/SX126x.h @@ -950,12 +950,11 @@ class SX126x: public PhysicalLayer { uint32_t getTimeOnAir(size_t len) override; /*! - \brief Calculate the timeout value for this specific module / series based on number of symbols or time - \param numSymbols Number of payload symbols to listen for + \brief Calculate the timeout value for this specific module / series (in number of symbols or units of time) \param timeoutUs Timeout in microseconds to listen for \returns Timeout value in a unit that is specific for the used module */ - uint32_t calculateRxTimeout(uint8_t numSymbols, uint32_t timeoutUs); + uint32_t calculateRxTimeout(uint32_t timeoutUs); /*! \brief Create the flags that make up RxDone and RxTimeout used for receiving downlinks diff --git a/src/modules/SX127x/SX127x.cpp b/src/modules/SX127x/SX127x.cpp index e0dffe0c..a00d6be7 100644 --- a/src/modules/SX127x/SX127x.cpp +++ b/src/modules/SX127x/SX127x.cpp @@ -411,7 +411,7 @@ int16_t SX127x::startReceive(uint8_t len, uint8_t mode) { // timeout is only used in RxSingle, so when a packet length is defined, force mode to RxSingle // and set the timeout value to the expected number of symbols (usually preamble + header) - if(len > 0) { + if((len > 0) && (this->spreadingFactor > 6)) { mode = RADIOLIB_SX127X_RXSINGLE; state = this->mod->SPIsetRegValue(RADIOLIB_SX127X_REG_SYMB_TIMEOUT_LSB, len); } @@ -1274,17 +1274,19 @@ uint32_t SX127x::getTimeOnAir(size_t len) { } -uint32_t SX127x::calculateRxTimeout(uint8_t numSymbols, uint32_t timeoutUs) { - (void)timeoutUs; - numSymbols = 20; +uint32_t SX127x::calculateRxTimeout(uint32_t timeoutUs) { + // the timeout is given as the number of symbols + // the calling function should provide some extra width, as this number of symbols is truncated to integer + // the order of operators is swapped here to decrease the effects of this truncation error + float symbolLength = (float) (uint32_t(1) << this->spreadingFactor) / (float) this->bandwidth; + uint32_t numSymbols = (timeoutUs / symbolLength) / 1000; return(numSymbols); } int16_t SX127x::irqRxDoneRxTimeout(uint16_t &irqFlags, uint16_t &irqMask) { - irqFlags = RADIOLIB_SX127X_MASK_IRQ_FLAG_RX_DONE; - irqMask = RADIOLIB_SX127X_MASK_IRQ_FLAG_RX_DONE; - irqFlags &= RADIOLIB_SX127X_MASK_IRQ_FLAG_RX_TIMEOUT; - irqMask &= RADIOLIB_SX127X_MASK_IRQ_FLAG_RX_TIMEOUT; + // IRQ flags/masks are inverted to what seems logical for SX127x (0 being activated, 1 being deactivated) + irqFlags = RADIOLIB_SX127X_MASK_IRQ_FLAG_RX_DEFAULT; + irqMask = RADIOLIB_SX127X_MASK_IRQ_FLAG_RX_DONE & RADIOLIB_SX127X_MASK_IRQ_FLAG_RX_TIMEOUT; return(RADIOLIB_ERR_NONE); } diff --git a/src/modules/SX127x/SX127x.h b/src/modules/SX127x/SX127x.h index fbc3d77f..4fb28d7b 100644 --- a/src/modules/SX127x/SX127x.h +++ b/src/modules/SX127x/SX127x.h @@ -160,6 +160,7 @@ #define RADIOLIB_SX127X_MASK_IRQ_FLAG_CAD_DONE 0b11111011 // 2 2 CAD complete #define RADIOLIB_SX127X_MASK_IRQ_FLAG_FHSS_CHANGE_CHANNEL 0b11111101 // 1 1 FHSS change channel #define RADIOLIB_SX127X_MASK_IRQ_FLAG_CAD_DETECTED 0b11111110 // 0 0 valid LoRa signal detected during CAD operation +#define RADIOLIB_SX127X_MASK_IRQ_FLAG_RX_DEFAULT 0b00011111 // 7 0 default for Rx (RX_TIMEOUT, RX_DONE, CRC_ERR) // RADIOLIB_SX127X_REG_FIFO_TX_BASE_ADDR #define RADIOLIB_SX127X_FIFO_TX_BASE_ADDR_MAX 0b00000000 // 7 0 allocate the entire FIFO buffer for TX only @@ -817,6 +818,7 @@ class SX127x: public PhysicalLayer { \brief Interrupt-driven receive method. DIO0 will be activated when full valid packet is received. \param len Expected length of packet to be received, or 0 when unused. Defaults to 0, non-zero required for LoRa spreading factor 6. + If non-zero for LoRa spreading factor > 6, RxSingle is used and value must be given in symbols. \param mode Receive mode to be used. Defaults to RxContinuous. \returns \ref status_codes */ @@ -1049,12 +1051,11 @@ class SX127x: public PhysicalLayer { uint32_t getTimeOnAir(size_t len) override; /*! - \brief Calculate the timeout value for this specific module / series based on number of symbols or time - \param numSymbols Number of payload symbols to listen for + \brief Calculate the timeout value for this specific module / series (in number of symbols or units of time) \param timeoutUs Timeout in microseconds to listen for \returns Timeout value in a unit that is specific for the used module */ - uint32_t calculateRxTimeout(uint8_t numSymbols, uint32_t timeoutUs); + uint32_t calculateRxTimeout(uint32_t timeoutUs); /*! \brief Create the flags that make up RxDone and RxTimeout used for receiving downlinks diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index 1ec6d570..8d61a861 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -31,7 +31,6 @@ uint8_t getDownlinkDataRate(uint8_t uplink, uint8_t offset, uint8_t base, uint8_ LoRaWANNode::LoRaWANNode(PhysicalLayer* phy, const LoRaWANBand_t* band) { this->phyLayer = phy; this->band = band; - this->FSK = false; this->rx2 = this->band->rx2; this->difsSlots = 2; this->backoffMax = 6; @@ -51,7 +50,11 @@ void LoRaWANNode::wipe() { } int16_t LoRaWANNode::restore() { - // check the magic value + // if already joined, ignore + if(this->isJoinedFlag) { + return(RADIOLIB_ERR_NONE); + } + Module* mod = this->phyLayer->getMod(); uint8_t nvm_table_version = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TABLE_VERSION_ID); @@ -60,8 +63,13 @@ int16_t LoRaWANNode::restore() { // } (void)nvm_table_version; + // check the magic value if(mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID) != RADIOLIB_LORAWAN_MAGIC) { - RADIOLIB_DEBUG_PRINTLN("magic id: %d", mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID)); + RADIOLIB_DEBUG_PRINTLN("magic id not set (no saved session)"); + 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); // the magic value is not set, user will have to do perform the join procedure return(RADIOLIB_ERR_NETWORK_NOT_JOINED); } @@ -126,6 +134,9 @@ int16_t LoRaWANNode::restore() { state = this->setPhyProperties(); RADIOLIB_ASSERT(state); + // full session is restored, so set joined flag + this->isJoinedFlag = true; + return(RADIOLIB_ERR_NONE); } @@ -179,21 +190,21 @@ int16_t LoRaWANNode::restoreChannels() { 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[5] = { 0 }; + 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[0] & 0xF0) >> 4; - this->availableChannels[dir][i].drMin = (chBuff[0] & 0x0F) >> 0; + this->availableChannels[dir][i].drMax = (chBuff[4] & 0xF0) >> 4; + this->availableChannels[dir][i].drMin = (chBuff[4] & 0x0F) >> 0; } } return(RADIOLIB_ERR_NONE); } #endif -int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKey, uint8_t* appKey, uint8_t drJoinSubband, bool force) { +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(); @@ -210,7 +221,7 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe RADIOLIB_ASSERT(state); // setup uplink/downlink frequencies and datarates - state = this->selectChannelsJR(this->devNonce, drJoinSubband); + state = this->selectChannelsJR(this->devNonce, joinDr); RADIOLIB_ASSERT(state); // configure for uplink with default configuration @@ -428,6 +439,8 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID, RADIOLIB_LORAWAN_MAGIC); #endif + this->isJoinedFlag = true; + return(RADIOLIB_ERR_NONE); } @@ -480,9 +493,15 @@ int16_t LoRaWANNode::beginABP(uint32_t addr, uint8_t* nwkSKey, uint8_t* appSKey, mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID, RADIOLIB_LORAWAN_MAGIC); #endif + this->isJoinedFlag = true; + return(RADIOLIB_ERR_NONE); } +bool LoRaWANNode::isJoined() { + return(this->isJoinedFlag); +} + #if !defined(RADIOLIB_EEPROM_UNSUPPORTED) int16_t LoRaWANNode::saveSession() { Module* mod = this->phyLayer->getMod(); @@ -618,8 +637,8 @@ int16_t LoRaWANNode::saveChannels() { uint8_t 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[5] = { 0 }; - chBuff[0] |= (uint8_t)this->availableChannels[dir][i].enabled << 7; + 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); @@ -648,9 +667,10 @@ int16_t LoRaWANNode::uplink(const char* str, uint8_t port, bool isConfirmed) { int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConfirmed) { Module* mod = this->phyLayer->getMod(); - // check if sufficient time has elapsed since the last uplink - if(mod->hal->millis() - this->rxDelayStart < this->rxDelays[1]) { - // not enough time elapsed since the last uplink, we may still be in an RX window + // check if the Rx windows were closed after sending the previous uplink + // this FORCES a user to call downlink() after an uplink() + if(this->rxDelayEnd < this->rxDelayStart) { + // not enough time elapsed since the last uplink, we may still be in an Rx window return(RADIOLIB_ERR_UPLINK_UNAVAILABLE); } @@ -761,9 +781,12 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf } // if the saved confirm-fcnt is set, set the ACK bit + bool isConfirmingDown; if(this->confFcntDown != RADIOLIB_LORAWAN_FCNT_NONE) { + isConfirmingDown = true; uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] |= RADIOLIB_LORAWAN_FCTRL_ACK; } + (void)isConfirmingDown; LoRaWANNode::hton(&uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCNT_POS], (uint16_t)this->fcntUp); @@ -870,16 +893,30 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf // the downlink confirmation was acknowledged, so clear the counter value this->confFcntDown = RADIOLIB_LORAWAN_FCNT_NONE; + // LoRaWANEvent: + // dir = RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK + // confirmed = isConfirmed + // confirming = isConfirmingDown + // power = this->txPwrCur + // fcnt = this->fcntUp + // port = port + return(RADIOLIB_ERR_NONE); } int16_t LoRaWANNode::downlinkCommon() { - // check if there are any upcoming Rx windows Module* mod = this->phyLayer->getMod(); const uint32_t scanGuard = 10; - if(mod->hal->millis() - this->rxDelayStart > (this->rxDelays[1] + scanGuard)) { - // time since last Tx is greater than RX2 delay + some guard period - // we have nothing to downlink + + // check if there are any upcoming Rx windows + // if the Rx1 window has already started, you're too late, because most downlinks happen in Rx1 + if(mod->hal->millis() - this->rxDelayStart > (this->rxDelays[0] - scanGuard)) { + // if between start of Rx1 and end of Rx2, wait until Rx2 closes + if(mod->hal->millis() - this->rxDelayStart < this->rxDelays[1]) { + mod->hal->delay(this->rxDelays[1] + this->rxDelayStart - mod->hal->millis()); + } + // update the end timestamp in case user got stuck between uplink and downlink + this->rxDelayEnd = mod->hal->millis(); return(RADIOLIB_ERR_NO_RX_WINDOW); } @@ -894,8 +931,8 @@ int16_t LoRaWANNode::downlinkCommon() { } // create the masks that are required for receiving downlinks - uint16_t irqFlags; - uint16_t irqMask; + uint16_t irqFlags = 0x0000; + uint16_t irqMask = 0x0000; this->phyLayer->irqRxDoneRxTimeout(irqFlags, irqMask); this->phyLayer->setPacketReceivedAction(LoRaWANNodeOnDownlinkAction); @@ -908,7 +945,7 @@ int16_t LoRaWANNode::downlinkCommon() { // according to the spec, this must be at least enough time to effectively detect a preamble // but pad it a bit on both sides (start and end) to make sure it is wide enough uint32_t timeoutHost = this->phyLayer->getTimeOnAir(0) + 2*scanGuard*1000; - uint32_t timeoutMod = this->phyLayer->calculateRxTimeout(0, timeoutHost); + uint32_t timeoutMod = this->phyLayer->calculateRxTimeout(timeoutHost); // wait for the start of the Rx window // the waiting duration is shortened a bit to cover any possible timing errors @@ -943,6 +980,8 @@ int16_t LoRaWANNode::downlinkCommon() { } } + // Rx windows are now closed + this->rxDelayEnd = mod->hal->millis(); // if we got here due to a timeout, stop ongoing activities if(this->phyLayer->isRxTimeout()) { @@ -1201,6 +1240,14 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { // a downlink was received, so reset the ADR counter to this uplink's fcnt this->adrFcnt = this->fcntUp; + // LoRaWANEvent: + // dir = RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK + // confirmed = isConfirmedDown + // confirming = isConfirmingUp + // power = this->txPwrCur + // fcnt = isAppDownlink ? this->aFcntDown : this->nFcntDown + // port = ... + // process payload (if there is any) if(payLen <= 0) { // no payload @@ -1226,10 +1273,46 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { return(RADIOLIB_ERR_NONE); } +#if defined(RADIOLIB_BUILD_ARDUINO) +int16_t LoRaWANNode::sendReceive(String& strUp, uint8_t port, String& strDown, bool isConfirmed) { + // send the uplink + int16_t state = this->uplink(strUp, port, isConfirmed); + RADIOLIB_ASSERT(state); + + // wait for the downlink + state = this->downlink(strDown); + return(state); +} +#endif + +int16_t LoRaWANNode::sendReceive(const char* strUp, uint8_t port, uint8_t* dataDown, size_t* lenDown, bool isConfirmed) { + // send the uplink + int16_t state = this->uplink(strUp, port, isConfirmed); + RADIOLIB_ASSERT(state); + + // wait for the downlink + state = this->downlink(dataDown, lenDown); + return(state); +} + +int16_t LoRaWANNode::sendReceive(uint8_t* dataUp, size_t lenUp, uint8_t port, uint8_t* dataDown, size_t* lenDown, bool isConfirmed) { + // send the uplink + int16_t state = this->uplink(dataUp, lenUp, port, isConfirmed); + RADIOLIB_ASSERT(state); + + // wait for the downlink + state = this->downlink(dataDown, lenDown); + return(state); +} + void LoRaWANNode::setDeviceStatus(uint8_t battLevel) { this->battLevel = battLevel; } +uint32_t LoRaWANNode::getFcntUp() { + return(this->fcntUp); +} + uint32_t LoRaWANNode::generateMIC(uint8_t* msg, size_t len, uint8_t* key) { if((msg == NULL) || (len == 0)) { return(0); @@ -1261,24 +1344,9 @@ bool LoRaWANNode::verifyMIC(uint8_t* msg, size_t len, uint8_t* key) { int16_t LoRaWANNode::setPhyProperties() { // set the physical layer configuration - int16_t state = RADIOLIB_ERR_NONE; - // if(this->FSK) { - // // for FSK, configure the channel - // state = this->phyLayer->setFrequency(this->band->fskFreq); - // RADIOLIB_ASSERT(state); - // DataRate_t dr; - // dr.fsk.bitRate = 50; - // dr.fsk.freqDev = 25; - // state = this->phyLayer->setDataRate(dr); - // RADIOLIB_ASSERT(state); - // state = this->phyLayer->setDataShaping(RADIOLIB_SHAPING_1_0); - // RADIOLIB_ASSERT(state); - // state = this->phyLayer->setEncoding(RADIOLIB_ENCODING_WHITENING); - // } - // RADIOLIB_ASSERT(state); - + // set the maximum power supported by both the module and the band - state = RADIOLIB_ERR_INVALID_OUTPUT_POWER; + int16_t state = RADIOLIB_ERR_INVALID_OUTPUT_POWER; while(state == RADIOLIB_ERR_INVALID_OUTPUT_POWER) { // go from the highest power in band and lower it until we hit one supported by the module state = this->phyLayer->setOutputPower(this->txPwrCur--); @@ -1383,17 +1451,21 @@ int16_t LoRaWANNode::setupChannels(uint8_t* cfList) { } int16_t LoRaWANNode::selectSubband(uint8_t idx) { - int16_t state = this->selectSubband((idx - 1) * 8, idx * 8); + 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->isJoinedFlag) { + 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); } - uint8_t numChannels = endChannel - startChannel; + 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); @@ -1421,7 +1493,7 @@ int16_t LoRaWANNode::selectSubband(uint8_t startChannel, uint8_t endChannel) { return(RADIOLIB_ERR_NONE); } -int16_t LoRaWANNode::selectChannelsJR(uint16_t devNonce, uint8_t drJoinSubband) { +int16_t LoRaWANNode::selectChannelsJR(uint16_t devNonce, uint8_t joinDr) { LoRaWANChannel_t channelUp; LoRaWANChannel_t channelDown; uint8_t drUp; @@ -1453,15 +1525,15 @@ int16_t LoRaWANNode::selectChannelsJR(uint16_t devNonce, uint8_t drJoinSubband) } // if join datarate is user-specified and valid, select that value; otherwise use - if(drJoinSubband != RADIOLIB_LORAWAN_DATA_RATE_UNUSED) { - if(drJoinSubband >= channelUp.drMin && drJoinSubband <= channelUp.drMax) { - drUp = drJoinSubband; + 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", drJoinSubband, channelUp.drMin, channelUp.drMax); - drJoinSubband = RADIOLIB_LORAWAN_DATA_RATE_UNUSED; + 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(drJoinSubband == RADIOLIB_LORAWAN_DATA_RATE_UNUSED) { + if(joinDr == RADIOLIB_LORAWAN_DATA_RATE_UNUSED) { drUp = int((channelUp.drMax + channelUp.drMin) / 2); } @@ -1470,27 +1542,39 @@ int16_t LoRaWANNode::selectChannelsJR(uint16_t devNonce, uint8_t drJoinSubband) drDown = getDownlinkDataRate(drUp, this->rx1DrOffset, this->band->rx1DataRateBase, channelDown.drMin, channelDown.drMax); } else { // RADIOLIB_LORAWAN_BAND_FIXED - 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) - // if the user defined a specific subband, use that - if(drJoinSubband == RADIOLIB_LORAWAN_DATA_RATE_UNUSED) { - blockID = (drJoinSubband - 1); + 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; + } } - uint8_t 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 - uint8_t spanID; - if(channelID < 8) { - spanID = 0; - channelUp.idx = blockID * 8 + channelID; - } else { - spanID = 1; - channelUp.idx = blockID; + 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; } - channelUp.freq = this->band->txSpans[spanID].freqStart + channelUp.idx*this->band->txSpans[spanID].freqStep; // for fixed channel plans, the user-specified datarate is ignored and span-specific value must be used drUp = this->band->txSpans[spanID].joinRequestDataRate; @@ -1548,26 +1632,32 @@ int16_t LoRaWANNode::selectChannels() { channelDn.drMin = this->band->rx1Span.drMin; channelDn.drMax = this->band->rx1Span.drMax; this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = channelDn; - uint8_t drDown = getDownlinkDataRate(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK], this->rx1DrOffset, - this->band->rx1DataRateBase, channelDn.drMin, channelDn.drMax); - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = drDown; } + uint8_t drDown = getDownlinkDataRate(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK], this->rx1DrOffset, this->band->rx1DataRateBase, + this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK].drMin, this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK].drMax); + this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = drDown; + return(RADIOLIB_ERR_NONE); } int16_t LoRaWANNode::setDatarate(uint8_t drUp) { - if(drUp < this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK].drMin) { + // 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; + 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); + } + } + if((drUp < drMin) || (drUp > drMax)) { + RADIOLIB_DEBUG_PRINTLN("Cannot configure DR %d (min: %d, max: %d)", drUp, drMin, drMax); return(RADIOLIB_ERR_DATA_RATE_INVALID); } - if(drUp > this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK].drMax) { - return(RADIOLIB_ERR_DATA_RATE_INVALID); - } - 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); - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = drUp; - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = drDown; + + RADIOLIB_DEBUG_PRINTLN("Configured DR up = %d", drUp); return(RADIOLIB_ERR_NONE); } @@ -1615,11 +1705,24 @@ int16_t LoRaWANNode::configureChannel(uint8_t dir) { int state = this->phyLayer->setFrequency(this->currentChannels[dir].freq); RADIOLIB_ASSERT(state); - // set the data rate - DataRate_t dataRate; - findDataRate(this->dataRates[dir], &dataRate); + // if this channel is an FSK channel, toggle the FSK switch + if(this->band->dataRates[this->dataRates[dir]] == RADIOLIB_LORAWAN_DATA_RATE_FSK_50_K) { + this->FSK = true; + } else { + this->FSK = false; + } + + DataRate_t dr; + findDataRate(this->dataRates[dir], &dr); + state = this->phyLayer->setDataRate(dr); + RADIOLIB_ASSERT(state); + + if(this->FSK) { + state = this->phyLayer->setDataShaping(RADIOLIB_SHAPING_1_0); + RADIOLIB_ASSERT(state); + state = this->phyLayer->setEncoding(RADIOLIB_ENCODING_WHITENING); + } - state = this->phyLayer->setDataRate(dataRate); return(state); } @@ -1695,7 +1798,7 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { uint16_t chMask = LoRaWANNode::ntoh(&cmd->payload[1]); uint8_t chMaskCntl = (cmd->payload[3] & 0x70) >> 4; uint8_t nbTrans = cmd->payload[3] & 0x0F; - RADIOLIB_DEBUG_PRINTLN("ADR REQ: dataRate = %d, txPower = %d, chMask = 0x%02x, chMaskCntl = %02x, nbTrans = %d", drUp, txPower, chMask, chMaskCntl, nbTrans); + RADIOLIB_DEBUG_PRINTLN("ADR REQ: dataRate = %d, txPower = %d, chMask = 0x%04x, chMaskCntl = %02x, nbTrans = %d", drUp, txPower, chMask, chMaskCntl, nbTrans); // apply the configuration uint8_t drAck = 0; @@ -1748,12 +1851,14 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { } } else { // RADIOLIB_LORAWAN_BAND_FIXED - // delete any prior ADR responses from the uplink queue, but do not care about if none is present yet + // 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; @@ -1771,9 +1876,10 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { 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][i] = chnl; + 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", i, availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].freq); + RADIOLIB_DEBUG_PRINTLN("Channel UL %d (%d) frequency = %f MHz", num, chnl.idx, availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].freq); + num++; } chNum++; } diff --git a/src/protocols/LoRaWAN/LoRaWAN.h b/src/protocols/LoRaWAN/LoRaWAN.h index 4d70b308..5d0eacc1 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.h +++ b/src/protocols/LoRaWAN/LoRaWAN.h @@ -72,7 +72,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 +#define RADIOLIB_LORAWAN_CHANNEL_INDEX_NONE (0xFF >> 1) // reserve first bit for enable-flag // recommended default settings #define RADIOLIB_LORAWAN_RECEIVE_DELAY_1_MS (1000) @@ -317,20 +317,6 @@ struct LoRaWANMacCommandQueue_t { */ class LoRaWANNode { public: - /*! \brief Set to true to force the node to only use FSK channels. Set to false by default. */ - bool FSK; - - /*! \brief Starting channel offset. - Some band plans only support a subset of available channels. - Set to a positive value to set the first channel that will be used (e.g. 8 for US915 FSB2 used by TTN). - By default -1 (no channel offset). */ - int8_t startChannel; - - /*! \brief Number of supported channels. - Some band plans only support a subset of available channels. - Set to a positive value to set the number of channels that will be used - (e.g. 8 for US915 FSB2 used by TTN). By default -1 (no channel offset). */ - int8_t numChannels; // Offset between TX and RX1 (such that RX1 has equal or lower DR) uint8_t rx1DrOffset; @@ -338,19 +324,6 @@ class LoRaWANNode { // RX2 channel properties - may be changed by MAC command LoRaWANChannel_t rx2; - /*! - \brief Num of Back Off(BO) slots to be decremented after DIFS phase. 0 to disable BO. - A random BO avoids collisions in the case where two or more nodes start the CSMA - process at the same time. - */ - uint8_t backoffMax; - - /*! \brief Num of CADs to estimate a clear CH. */ - uint8_t difsSlots; - - /*! \brief enable/disable CSMA for LoRaWAN. */ - bool enableCSMA; - /*! \brief Default constructor. \param phy Pointer to the PhysicalLayer radio module. @@ -379,12 +352,12 @@ class LoRaWANNode { \param devEUI 8-byte device identifier. \param nwkKey Pointer to the network AES-128 key. \param appKey Pointer to the application AES-128 key. - \param drJoinSubband (OTAA:) The datarate at which to send the join-request; (ABP:) the subband at which to send the join-request + \param joinDr (OTAA:) The datarate at which to send the join-request; (ABP:) ignored \param force Set to true to force joining even if previously joined. \returns \ref status_codes */ - int16_t beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKey, uint8_t* appKey, uint8_t joinDrSubband = RADIOLIB_LORAWAN_DATA_RATE_UNUSED, bool force = false); + int16_t beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKey, uint8_t* appKey, uint8_t joinDr = RADIOLIB_LORAWAN_DATA_RATE_UNUSED, bool force = false); /*! \brief Join network by performing activation by personalization. @@ -399,6 +372,9 @@ class LoRaWANNode { */ int16_t beginABP(uint32_t addr, uint8_t* nwkSKey, uint8_t* appSKey, uint8_t* fNwkSIntKey = NULL, uint8_t* sNwkSIntKey = NULL, bool force = false); + /*! \brief Whether there is an ongoing session active */ + bool isJoined(); + /*! \brief Save the current state of the session. All variables are compared to what is saved and only the differences are rewritten. @@ -436,12 +412,6 @@ class LoRaWANNode { */ int16_t uplink(uint8_t* data, size_t len, uint8_t port, bool isConfirmed = false); - /*! - \brief Wait for, open and listen during Rx1 and Rx2 windows; only performs listening - \returns \ref status_codes - */ - int16_t downlinkCommon(); - #if defined(RADIOLIB_BUILD_ARDUINO) /*! \brief Wait for downlink from the server in either RX1 or RX2 window. @@ -459,6 +429,41 @@ class LoRaWANNode { */ int16_t downlink(uint8_t* data, size_t* len); + #if defined(RADIOLIB_BUILD_ARDUINO) + /*! + \brief Send a message to the server and wait for a downlink during Rx1 and/or Rx2 window. + \param strUp Address of Arduino String that will be transmitted. + \param port Port number to send the message to. + \param strDown Address of Arduino String to save the received data. + \param isConfirmed Whether to send a confirmed uplink or not. + \returns \ref status_codes + */ + int16_t sendReceive(String& strUp, uint8_t port, String& strDown, bool isConfirmed = false); + #endif + + /*! + \brief Send a message to the server and wait for a downlink during Rx1 and/or Rx2 window. + \param strUp C-string that will be transmitted. + \param port Port number to send the message to. + \param dataDown Buffer to save received data into. + \param lenDown Pointer to variable that will be used to save the number of received bytes. + \param isConfirmed Whether to send a confirmed uplink or not. + \returns \ref status_codes + */ + int16_t sendReceive(const char* strUp, uint8_t port, uint8_t* dataDown, size_t* lenDown, bool isConfirmed = false); + + /*! + \brief Send a message to the server and wait for a downlink during Rx1 and/or Rx2 window. + \param dataUp Data to send. + \param lenUp Length of the data. + \param port Port number to send the message to. + \param dataDown Buffer to save received data into. + \param lenDown Pointer to variable that will be used to save the number of received bytes. + \param isConfirmed Whether to send a confirmed uplink or not. + \returns \ref status_codes + */ + int16_t sendReceive(uint8_t* dataUp, size_t lenUp, uint8_t port, uint8_t* dataDown, size_t* lenDown, bool isConfirmed = false); + /*! \brief Set device status. \param battLevel Battery level to set. 0 for external power source, 1 for lowest battery, @@ -466,9 +471,12 @@ class LoRaWANNode { */ void setDeviceStatus(uint8_t battLevel); + /*! \brief Returns the last uplink's frame counter */ + uint32_t getFcntUp(); + /*! - \brief Set uplink datarate. This should _not_ be used when ADR is enabled. - \param dr Datarate to use for uplinks + \brief Set uplink datarate. This should not be used when ADR is enabled. + \param dr Datarate to use for uplinks. \returns \ref status_codes */ int16_t setDatarate(uint8_t drUp); @@ -480,16 +488,18 @@ class LoRaWANNode { void setADR(bool enable = true); /*! - \brief Select a single subband (8 channels) for fixed bands such as US915 + \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 + \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 (exclusive) + \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); @@ -545,8 +555,25 @@ class LoRaWANNode { uint32_t confFcntDown = RADIOLIB_LORAWAN_FCNT_NONE; uint32_t adrFcnt = 0; + // whether the current configured channel is in FSK mode + bool FSK; + + // flag that shows whether the device is joined and there is an ongoing session + bool isJoinedFlag = false; + // ADR is enabled by default bool adrEnabled = true; + + // enable/disable CSMA for LoRaWAN + bool enableCSMA; + + // number of backoff slots to be decremented after DIFS phase. 0 to disable BO. + // A random BO avoids collisions in the case where two or more nodes start the CSMA + // process at the same time. + uint8_t backoffMax; + + // number of CADs to estimate a clear CH + uint8_t difsSlots; // available channel frequencies from list passed during OTA activation LoRaWANChannel_t availableChannels[2][RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS] = { { 0 }, { 0 } }; @@ -563,6 +590,9 @@ class LoRaWANNode { // timestamp to measure the RX1/2 delay (from uplink end) uint32_t rxDelayStart = 0; + // timestamp when the Rx1/2 windows were closed (timeout or uplink received) + uint32_t rxDelayEnd = 0; + // delays between the uplink and RX1/2 windows uint32_t rxDelays[2] = { RADIOLIB_LORAWAN_RECEIVE_DELAY_1_MS, RADIOLIB_LORAWAN_RECEIVE_DELAY_2_MS }; @@ -588,6 +618,9 @@ class LoRaWANNode { int16_t restoreFcntUp(); #endif + // wait for, open and listen during Rx1 and Rx2 windows; only performs listening + int16_t downlinkCommon(); + // method to generate message integrity code uint32_t generateMIC(uint8_t* msg, size_t len, uint8_t* key); diff --git a/src/protocols/PhysicalLayer/PhysicalLayer.cpp b/src/protocols/PhysicalLayer/PhysicalLayer.cpp index d6c107d5..0bb634f9 100644 --- a/src/protocols/PhysicalLayer/PhysicalLayer.cpp +++ b/src/protocols/PhysicalLayer/PhysicalLayer.cpp @@ -294,8 +294,7 @@ uint32_t PhysicalLayer::getTimeOnAir(size_t len) { return(0); } -uint32_t PhysicalLayer::calculateRxTimeout(uint8_t numSymbols, uint32_t timeoutUs) { - (void)numSymbols; +uint32_t PhysicalLayer::calculateRxTimeout(uint32_t timeoutUs) { (void)timeoutUs; return(0); } diff --git a/src/protocols/PhysicalLayer/PhysicalLayer.h b/src/protocols/PhysicalLayer/PhysicalLayer.h index d7eccc13..741cdb8c 100644 --- a/src/protocols/PhysicalLayer/PhysicalLayer.h +++ b/src/protocols/PhysicalLayer/PhysicalLayer.h @@ -311,12 +311,11 @@ class PhysicalLayer { virtual uint32_t getTimeOnAir(size_t len); /*! - \brief Calculate the timeout value for this specific module / series based on number of symbols or time - \param numSymbols Number of payload symbols to listen for + \brief Calculate the timeout value for this specific module / series (in number of symbols or units of time) \param timeoutUs Timeout in microseconds to listen for \returns Timeout value in a unit that is specific for the used module */ - virtual uint32_t calculateRxTimeout(uint8_t numSymbols, uint32_t timeoutUs); + virtual uint32_t calculateRxTimeout(uint32_t timeoutUs); /*! \brief Create the flags that make up RxDone and RxTimeout used for receiving downlinks