From dd79213461eb7ca4d60f67fea6c3fe6f51cc2c44 Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Thu, 5 Oct 2023 12:16:49 +0200 Subject: [PATCH 01/21] [LoRaWAN] rework bands, add ADR, partial MAC support Known problem: terribly bad at receiving downlinks Mask-list bands (e.g. US915) untested, likely a few bugs --- src/BuildOpt.h | 2 +- src/Hal.h | 60 +- src/TypeDef.h | 20 + src/protocols/LoRaWAN/LoRaWAN.cpp | 1130 +++++++++++++++--------- src/protocols/LoRaWAN/LoRaWAN.h | 167 ++-- src/protocols/LoRaWAN/LoRaWANBands.cpp | 986 ++++++++------------- 6 files changed, 1237 insertions(+), 1128 deletions(-) diff --git a/src/BuildOpt.h b/src/BuildOpt.h index 5fe2e0ba6..1f8d952c1 100644 --- a/src/BuildOpt.h +++ b/src/BuildOpt.h @@ -443,7 +443,7 @@ // the amount of space allocated to the persistent storage #if !defined(RADIOLIB_HAL_PERSISTENT_STORAGE_SIZE) - #define RADIOLIB_HAL_PERSISTENT_STORAGE_SIZE (0x60) + #define RADIOLIB_HAL_PERSISTENT_STORAGE_SIZE (0x0180) #endif // This only compiles on STM32 boards with SUBGHZ module, but also diff --git a/src/Hal.h b/src/Hal.h index 6fa800b14..415e53fb9 100644 --- a/src/Hal.h +++ b/src/Hal.h @@ -7,25 +7,59 @@ #include "BuildOpt.h" // list of persistent parameters -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_NONCE_ID (0) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_ADDR_ID (1) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID (2) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID (3) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_APP_S_KEY_ID (4) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_FNWK_SINT_KEY_ID (5) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_SNWK_SINT_KEY_ID (6) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_NWK_SENC_KEY_ID (7) +#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 (2) +#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXDR_RX2DR_ID (3) +#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXPWR_CUR_MAX_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) static const uint32_t RadioLibPersistentParamTable[] = { - 0x00, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_NONCE_ID - 0x04, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_ADDR_ID - 0x08, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID - 0x0C, // RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID + 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_MAX_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, // end + 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 }; /*! diff --git a/src/TypeDef.h b/src/TypeDef.h index 11b3f7f6c..0ebcbbb17 100644 --- a/src/TypeDef.h +++ b/src/TypeDef.h @@ -533,6 +533,26 @@ */ #define RADIOLIB_ERR_COMMAND_QUEUE_EMPTY (-1110) +/*! + \brief Unable to delete MAC command because it was not found in the queue. +*/ +#define RADIOLIB_ERR_COMMAND_QUEUE_ITEM_NOT_FOUND (-1111) + +/*! + \brief Unable to join network because JoinNonce is not higher than saved value. +*/ +#define RADIOLIB_ERR_JOIN_NONCE_INVALID (-1112) + +/*! + \brief Received downlink Network frame counter is invalid (lower than last heard value). +*/ +#define RADIOLIB_ERR_N_FCNT_DOWN_INVALID (-1113) + +/*! + \brief Received downlink Application frame counter is invalid (lower than last heard value). +*/ +#define RADIOLIB_ERR_A_FCNT_DOWN_INVALID (-1114) + /*! \} */ diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index d2a1860fb..db3b543ec 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -1,5 +1,5 @@ #include "LoRaWAN.h" - +#include "LoRaWANBands.cpp" #include #if !defined(RADIOLIB_EXCLUDE_LORAWAN) @@ -36,7 +36,7 @@ LoRaWANNode::LoRaWANNode(PhysicalLayer* phy, const LoRaWANBand_t* band) { this->FSK = false; this->startChannel = -1; this->numChannels = -1; - this->backupFreq = this->band->backupChannel.freqStart; + this->rx2 = this->band->rx2; } void LoRaWANNode::wipe() { @@ -44,46 +44,87 @@ void LoRaWANNode::wipe() { mod->hal->wipePersistentStorage(); } -int16_t LoRaWANNode::begin() { - int16_t state = this->setPhyProperties(); - RADIOLIB_ASSERT(state); - +int16_t LoRaWANNode::restore() { // check the magic value Module* mod = this->phyLayer->getMod(); - if(mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID) != RADIOLIB_LORAWAN_MAGIC) { + if(mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID) != RADIOLIB_LORAWAN_MAGIC) { // the magic value is not set, user will have to do perform the join procedure return(RADIOLIB_ERR_NETWORK_NOT_JOINED); } - // pull all needed information from persistent storage + uint16_t nvm_table_version = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TABLE_VERSION_ID); + // if (RADIOLIB_PERSISTENT_PARAM_LORAWAN_TABLE_VERSION > nvm_table_version) { + // // set default values for variables that are new or something + // } + (void)nvm_table_version; + + // 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->rev = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION); + RADIOLIB_DEBUG_PRINTLN("LoRaWAN session: v1.%d", this->rev); + uint8_t txDrRx2Dr = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXDR_RX2DR_ID); + uint8_t txPwrCurMax = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXPWR_CUR_MAX_ID); + uint8_t rx1DrOffDel = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_RX1_DROFF_DEL_ID); + 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); + uint8_t adrLimDel = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_LIM_DEL_ID); + this->nbTrans = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_NBTRANS_ID); + + this->rx2.drMax = (txDrRx2Dr & 0x0F) >> 0; + this->rx1DrOffset = (rx1DrOffDel & 0xF0) >> 4; + this->rxDelays[0] = ((rx1DrOffDel & 0x0F) >> 0) * 1000; + if(this->rxDelays[0] == 0) { + this->rxDelays[0] = RADIOLIB_LORAWAN_RECEIVE_DELAY_1_MS; + } + this->rxDelays[1] = this->rxDelays[0] + 1000; + this->rx2.freq = (float)rx2Freq / 10000.0; + this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = (txDrRx2Dr & 0xF0) >> 4; + this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] + this->band->rx1DataRateBase + this->rx1DrOffset; + + uint8_t queueBuff[sizeof(LoRaWANMacCommandQueue_t)] = { 0 }; + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FOPTS_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); + return(RADIOLIB_ERR_NONE); } int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKey, uint8_t* appKey, bool force) { // check if we actually need to send the join request Module* mod = this->phyLayer->getMod(); - if(!force && (mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID) == RADIOLIB_LORAWAN_MAGIC)) { + if(!force && (mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID) == RADIOLIB_LORAWAN_MAGIC)) { // the device has joined already, we can just pull the data from persistent storage - return(this->begin()); + return(this->restore()); } // set the physical layer configuration int16_t state = this->setPhyProperties(); RADIOLIB_ASSERT(state); - // setup uplink/downlink frequencies and datarates - state = this->setupChannels(); - RADIOLIB_ASSERT(state); - // get dev nonce from persistent storage and increment it uint16_t devNonce = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_NONCE_ID); mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_NONCE_ID, devNonce + 1); + // setup uplink/downlink frequencies and datarates + state = this->selectChannelsJR(devNonce); + RADIOLIB_ASSERT(state); + + // configure for uplink with default configuration + state = this->configureChannel(RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK); + RADIOLIB_ASSERT(state); + // build the join-request message uint8_t joinRequestMsg[RADIOLIB_LORAWAN_JOIN_REQUEST_LEN]; @@ -118,7 +159,7 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe // start receiving uint32_t start = mod->hal->millis(); downlinkReceived = false; - state = this->phyLayer->startReceive(); + state = this->phyLayer->startReceive(0x00, 0b0000001001100010, 0b0000000000000010, 0); RADIOLIB_ASSERT(state); // wait for the reply or timeout @@ -175,9 +216,25 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe RADIOLIB_DEBUG_PRINTLN("joinAcceptMsg:"); RADIOLIB_DEBUG_HEXDUMP(joinAcceptMsg, lenRx); + // get current JoinNonce from downlink and previous JoinNonce from NVM + uint32_t joinNonce = LoRaWANNode::ntoh(&joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_JOIN_NONCE_POS], 3); + uint32_t joinNoncePrev = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_JOIN_NONCE_ID); + RADIOLIB_DEBUG_PRINTLN("JoinNoncePrev: %d, JoinNonce: %d", joinNoncePrev, joinNonce); + + // JoinNonce received must be greater than the last JoinNonce heard, else error + if((joinNoncePrev > 0) && (joinNonce <= joinNoncePrev)) { + return(RADIOLIB_ERR_JOIN_NONCE_INVALID); + } + // check LoRaWAN revision (the MIC verification depends on this) uint8_t dlSettings = joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_DL_SETTINGS_POS]; - if(dlSettings & RADIOLIB_LORAWAN_JOIN_ACCEPT_R_1_1) { + 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) { // 1.1 version, first we need to derive the join accept integrity key uint8_t keyDerivationBuff[RADIOLIB_AES128_BLOCK_SIZE] = { 0 }; keyDerivationBuff[0] = RADIOLIB_LORAWAN_JOIN_ACCEPT_JS_INT_KEY; @@ -203,11 +260,12 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe } } - - // parse the contents - uint32_t joinNonce = LoRaWANNode::ntoh(&joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_JOIN_NONCE_POS], 3); + + // parse other contents uint32_t 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; @@ -216,57 +274,20 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe // process CFlist if present if(lenRx == RADIOLIB_LORAWAN_JOIN_ACCEPT_MAX_LEN) { - if(this->band->cfListType == RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES) { - // list of frequencies - for(uint8_t i = 0; i < 5; i++) { - uint32_t freq = LoRaWANNode::ntoh(&joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_CFLIST_POS + 3*i], 3); - availableChannelsFreq[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i] = (float)freq/10000.0; - availableChannelsFreq[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i] = availableChannelsFreq[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i]; - RADIOLIB_DEBUG_PRINTLN("Channel UL/DL %d frequency = %f MHz", i, availableChannelsFreq[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i]); - } - - } else { - // frequency mask, we need to find out which frequencies are actually being used - uint8_t channelId = 0; - uint8_t chSpan = 0; - uint8_t chNum = 0; - for(uint8_t i = 0; i < 5; i++) { - uint16_t mask = LoRaWANNode::ntoh(&joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_CFLIST_POS + 2*i]); - RADIOLIB_DEBUG_PRINTLN("mask[%d] = 0x%04x", i, mask); - for(uint8_t j = 0; j < 16; j++) { - if(chNum >= this->band->defaultChannels[chSpan].numChannels) { - chNum -= this->band->defaultChannels[chSpan].numChannels; - chSpan++; - - if(chSpan >= this->band->numChannelSpans) { - RADIOLIB_DEBUG_PRINTLN("channel bitmask overrun!"); - return(RADIOLIB_ERR_UNKNOWN); - } - } - - if(mask & (1UL << j)) { - RADIOLIB_DEBUG_PRINTLN("chNum = %d, chSpan = %d", chNum, chSpan); - uint8_t dir = this->band->defaultChannels[chSpan].direction; - float freq = this->band->defaultChannels[chSpan].freqStart + chNum*this->band->defaultChannels[chSpan].freqStep; - availableChannelsFreq[dir][channelId] = freq; - RADIOLIB_DEBUG_PRINTLN("Channel %cL %d frequency = %f MHz", dir ? 'U': 'D', channelId, availableChannelsFreq[dir][channelId]); - channelId++; - } - - chNum++; - } - } - - } - + 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->saveChannels(); // prepare buffer for key derivation uint8_t keyDerivationBuff[RADIOLIB_AES128_BLOCK_SIZE] = { 0 }; LoRaWANNode::hton(&keyDerivationBuff[RADIOLIB_LORAWAN_JOIN_ACCEPT_JOIN_NONCE_POS], joinNonce, 3); // check protocol version (1.0 vs 1.1) - if(dlSettings & RADIOLIB_LORAWAN_JOIN_ACCEPT_R_1_1) { + if(this->rev == 1) { // 1.1 version, derive the keys LoRaWANNode::hton(&keyDerivationBuff[RADIOLIB_LORAWAN_JOIN_ACCEPT_JOIN_EUI_POS], joinEUI); LoRaWANNode::hton(&keyDerivationBuff[RADIOLIB_LORAWAN_JOIN_ACCEPT_DEV_NONCE_POS], devNonce); @@ -288,19 +309,17 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe RadioLibAES128Instance.encryptECB(keyDerivationBuff, RADIOLIB_AES128_BLOCK_SIZE, this->nwkSEncKey); // enqueue the RekeyInd MAC command to be sent in the next uplink - this->rev = 1; LoRaWANMacCommand_t cmd = { .cid = RADIOLIB_LORAWAN_MAC_CMD_REKEY, .len = sizeof(uint8_t), .payload = { this->rev }, - .repeat = RADIOLIB_LORAWAN_ADR_ACK_LIMIT, + .repeat = 0x01 << RADIOLIB_LORAWAN_ADR_ACK_LIMIT_EXP, }; state = pushMacCommand(&cmd, &this->commandsUp); RADIOLIB_ASSERT(state); } else { // 1.0 version, just derive the keys - this->rev = 0; LoRaWANNode::hton(&keyDerivationBuff[RADIOLIB_LORAWAN_JOIN_ACCEPT_HOME_NET_ID_POS], homeNetId, 3); LoRaWANNode::hton(&keyDerivationBuff[RADIOLIB_LORAWAN_JOIN_ACCEPT_DEV_ADDR_POS], devNonce); keyDerivationBuff[0] = RADIOLIB_LORAWAN_JOIN_ACCEPT_APP_S_KEY; @@ -315,23 +334,56 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe memcpy(this->nwkSEncKey, this->fNwkSIntKey, RADIOLIB_AES128_KEY_SIZE); } - - // save the device address + + // store session configuration + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION, this->rev); + uint8_t txDrRx2Dr = (this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] << 4) | this->rx2.drMax; + uint8_t txPwrCurMax; + uint8_t rx1DrOffDel = (this->rx1DrOffset << 4) | (this->rxDelays[0] / 1000); + uint32_t rx2Freq = uint32_t(this->rx2.freq * 10000); + uint8_t rx2FreqBuf[3]; + LoRaWANNode::hton(&rx2FreqBuf[0], rx2Freq, 3); + uint8_t adrLimDel = (RADIOLIB_LORAWAN_ADR_ACK_LIMIT_EXP << 4) | (RADIOLIB_LORAWAN_ADR_ACK_DELAY_EXP << 0); + uint8_t nbTrans = (0x01 << 0); + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXDR_RX2DR_ID, txDrRx2Dr); + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXPWR_CUR_MAX_ID, txPwrCurMax); + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_RX1_DROFF_DEL_ID, rx1DrOffDel); + mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_RX2FREQ_ID), rx2FreqBuf, 3); + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_LIM_DEL_ID, adrLimDel); + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_NBTRANS_ID, nbTrans); + + // save the device address & keys mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_ADDR_ID, this->devAddr); - - // update the keys 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); - // all complete, reset device counters and set the magic number + // save uplink parameters + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_HOME_NET_ID, homeNetId); + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_JOIN_NONCE_ID, joinNonce); + + // all complete, reset all frame counters and set the magic number + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_A_FCNT_DOWN_ID, 0); + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_N_FCNT_DOWN_ID, 0); + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_CONF_FCNT_UP_ID, 0); + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_CONF_FCNT_DOWN_ID, 0); + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_FCNT_ID, 0); mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID, 0); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID, RADIOLIB_LORAWAN_MAGIC); + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID, RADIOLIB_LORAWAN_MAGIC); + + // everything written to NVM, write current table version to NVM + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TABLE_VERSION_ID, RADIOLIB_PERSISTENT_PARAM_LORAWAN_TABLE_VERSION); return(RADIOLIB_ERR_NONE); } -int16_t LoRaWANNode::beginAPB(uint32_t addr, uint8_t* nwkSKey, uint8_t* appSKey, uint8_t* fNwkSIntKey, uint8_t* sNwkSIntKey) { +int16_t LoRaWANNode::beginABP(uint32_t addr, uint8_t* nwkSKey, uint8_t* appSKey, uint8_t* fNwkSIntKey, uint8_t* sNwkSIntKey, bool force) { + // 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)) { + // the device has joined already, we can just pull the data from persistent storage + return(this->restore()); + } this->devAddr = addr; memcpy(this->appSKey, appSKey, RADIOLIB_AES128_KEY_SIZE); memcpy(this->nwkSEncKey, nwkSKey, RADIOLIB_AES128_KEY_SIZE); @@ -350,8 +402,13 @@ int16_t LoRaWANNode::beginAPB(uint32_t addr, uint8_t* nwkSKey, uint8_t* appSKey, RADIOLIB_ASSERT(state); // setup uplink/downlink frequencies and datarates - state = this->setupChannels(); - return(state); + state = this->setupChannels(nullptr); + RADIOLIB_ASSERT(state); + + // everything written to NVM, write current version to NVM + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TABLE_VERSION_ID, RADIOLIB_PERSISTENT_PARAM_LORAWAN_TABLE_VERSION); + + return(RADIOLIB_ERR_NONE); } #if defined(RADIOLIB_BUILD_ARDUINO) @@ -365,30 +422,64 @@ int16_t LoRaWANNode::uplink(const char* str, uint8_t port) { } int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port) { + bool adrEn = true; // check destination port if(port > 0xDF) { return(RADIOLIB_ERR_INVALID_PORT); } + // port 0 is only allowed for MAC-only payloads + if(port == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND) { + if (!isMACPayload) { + return(RADIOLIB_ERR_INVALID_PORT); + } + // if this is MAC only payload, continue and reset for next uplink + isMACPayload = false; + } + + Module* mod = this->phyLayer->getMod(); - // check if there are some MAC commands to piggyback - size_t foptsLen = 0; - if(this->commandsUp.numCommands > 0) { + // check if there are some MAC commands to piggyback (only when piggybacking onto a application-frame) + uint8_t foptsLen = 0; + size_t foptsBufSize = 0; + if(this->commandsUp.numCommands > 0 && port != RADIOLIB_LORAWAN_FPORT_MAC_COMMAND) { // there are, assume the maximum possible FOpts len for buffer allocation - foptsLen = 15; + foptsLen = this->commandsUp.len; + foptsBufSize = 15; } // check maximum payload len as defined in phy - if(len > this->band->payloadLenMax[this->dataRate[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]]) { + if(len > this->band->payloadLenMax[this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]]) { return(RADIOLIB_ERR_PACKET_TOO_LONG); } + // get frame counter from persistent storage + uint32_t fcnt = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID) + 1; + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID, fcnt); + uint32_t adrFcnt = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_FCNT_ID); + uint8_t adrParams = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_LIM_DEL_ID); + uint32_t adrLimit = 0x01 << ((adrParams & 0xF0) >> 4); + uint32_t adrDelay = 0x01 << ((adrParams & 0x0F) >> 0); + bool adrAckReq = false; + if((fcnt - adrFcnt) == adrLimit) { + adrAckReq = true; + // add MAC command to queue + } else if ((fcnt - adrFcnt) == (adrLimit + adrDelay)) { + // set TX power to max + + // decrease DR if possible + if(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] > this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK].drMin) { + this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]--; + } else { + // enable all channels + } + } + // configure for uplink - // TODO select randomly from available channels + this->selectChannels(); int16_t state = this->configureChannel(RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK); RADIOLIB_ASSERT(state); // check if sufficient time has elapsed since the last uplink - Module* mod = this->phyLayer->getMod(); if(mod->hal->millis() - this->rxDelayStart < rxDelays[1]) { // not enough time elapsed since the last uplink, we may still be in an RX window return(RADIOLIB_ERR_UPLINK_UNAVAILABLE); @@ -396,7 +487,7 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port) { // build the uplink message // the first 16 bytes are reserved for MIC calculation blocks - size_t uplinkMsgLen = RADIOLIB_LORAWAN_FRAME_LEN(len, foptsLen); + size_t uplinkMsgLen = RADIOLIB_LORAWAN_FRAME_LEN(len, foptsBufSize); #if defined(RADIOLIB_STATIC_ONLY) uint8_t uplinkMsg[RADIOLIB_STATIC_ARRAY_SIZE]; #else @@ -407,31 +498,48 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port) { uplinkMsg[RADIOLIB_LORAWAN_FHDR_LEN_START_OFFS] = RADIOLIB_LORAWAN_MHDR_MTYPE_UNCONF_DATA_UP | RADIOLIB_LORAWAN_MHDR_MAJOR_R1; LoRaWANNode::hton(&uplinkMsg[RADIOLIB_LORAWAN_FHDR_DEV_ADDR_POS], this->devAddr); - // TODO implement adaptive data rate // length of fopts will be added later uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] = 0x00; + if(adrEn) { + uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] |= RADIOLIB_LORAWAN_FCTRL_ADR_ENABLED; + if(adrAckReq) { + uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] |= RADIOLIB_LORAWAN_FCTRL_ADR_ACK_REQ; + } + } - // get frame counter from persistent storage - uint32_t fcnt = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID) + 1; - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID, fcnt); LoRaWANNode::hton(&uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCNT_POS], (uint16_t)fcnt); - // check if we have some MAC command to append - // TODO implement appending multiple MAC commands - LoRaWANMacCommand_t cmd = { .cid = 0, .len = 0, .payload = { 0 }, .repeat = 0, }; - if(popMacCommand(&cmd, &this->commandsUp) == RADIOLIB_ERR_NONE) { - // we do, add it to fopts - uint8_t foptsBuff[RADIOLIB_AES128_BLOCK_SIZE]; - foptsBuff[0] = cmd.cid; - for(size_t i = 1; i < cmd.len; i++) { - foptsBuff[i] = cmd.payload[i]; + // check if we have some MAC commands to append + if(foptsLen > 0) { + uint8_t foptsNum = this->commandsUp.numCommands; + uint8_t foptsBuff[foptsBufSize]; + size_t idx = 0; + for (size_t i = 0; i < foptsNum; i++) { + LoRaWANMacCommand_t cmd = { .cid = 0, .len = 0, .payload = { 0 }, .repeat = 0, }; + popMacCommand(&cmd, &this->commandsUp, i); + if (cmd.cid == 0) { + break; + } + foptsBuff[idx] = cmd.cid; + for(size_t i = 0; i < cmd.len; i++) { + foptsBuff[idx + 1 + i] = cmd.payload[i]; + } + idx += cmd.len + 1; } - foptsLen = 1 + cmd.len; + + RADIOLIB_DEBUG_PRINTLN("Uplink MAC payload (%d commands):", foptsNum); + RADIOLIB_DEBUG_HEXDUMP(foptsBuff, foptsBufSize); + uplinkMsgLen = RADIOLIB_LORAWAN_FRAME_LEN(len, foptsLen); uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] |= foptsLen; // encrypt it processAES(foptsBuff, foptsLen, this->nwkSEncKey, &uplinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], fcnt, RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK, 0x01, true); + + // write the current MAC command queue to nvm for next uplink + uint8_t queueBuff[sizeof(LoRaWANMacCommandQueue_t)]; + memcpy(queueBuff, &this->commandsUp, sizeof(LoRaWANMacCommandQueue_t)); + mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FOPTS_ID), queueBuff, sizeof(LoRaWANMacCommandQueue_t)); } // set the port @@ -444,6 +552,7 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port) { } // encrypt the frame payload + // TODO check ctrId --> erratum says it should be 0x01? processAES(data, len, encKey, &uplinkMsg[RADIOLIB_LORAWAN_FRAME_PAYLOAD_POS(foptsLen)], fcnt, RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK, 0x00, true); // create blocks for MIC calculation @@ -457,8 +566,8 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port) { uint8_t block1[RADIOLIB_AES128_BLOCK_SIZE] = { 0 }; memcpy(block1, block0, RADIOLIB_AES128_BLOCK_SIZE); // TODO implement confirmed frames - block1[RADIOLIB_LORAWAN_MIC_DATA_RATE_POS] = this->dataRate[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]; - block1[RADIOLIB_LORAWAN_MIC_CH_INDEX_POS] = this->chIndex[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]; + block1[RADIOLIB_LORAWAN_MIC_DATA_RATE_POS] = this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]; + block1[RADIOLIB_LORAWAN_MIC_CH_INDEX_POS] = this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK].idx; RADIOLIB_DEBUG_PRINTLN("uplinkMsg pre-MIC:"); RADIOLIB_DEBUG_HEXDUMP(uplinkMsg, uplinkMsgLen); @@ -520,7 +629,7 @@ int16_t LoRaWANNode::downlink(String& str) { int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { // check if there are any upcoming Rx windows Module* mod = this->phyLayer->getMod(); - const uint32_t scanGuard = 500; + 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 @@ -555,7 +664,9 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { if(waitLen > scanGuard) { waitLen -= scanGuard; } + RADIOLIB_DEBUG_PRINTLN("Waiting for %d ms...", waitLen); mod->hal->delay(waitLen); + RADIOLIB_DEBUG_PRINTLN("Opening Rx%d window...", (i+1)); // wait until we get a preamble uint32_t scanStart = mod->hal->millis(); @@ -579,6 +690,15 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { state = this->phyLayer->getChannelScanResult(); if((state == RADIOLIB_PREAMBLE_DETECTED) || (state == RADIOLIB_LORA_DETECTED)) { packetDetected = true; + + // channel scan is finished, swap the actions + this->phyLayer->clearChannelScanAction(); + this->phyLayer->setPacketReceivedAction(LoRaWANNodeOnDownlink); + + // start receiving + state = this->phyLayer->startReceive(0x00, 0b0000001001100010, 0b0000000000000010, 0); // RxSingle + RADIOLIB_ASSERT(state); + break; } @@ -586,18 +706,18 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { // check if we have a packet if(packetDetected) { + RADIOLIB_DEBUG_PRINTLN("Detected a packet..."); break; } else if(i == 0) { // nothing in the first window, configure for the second - state = this->phyLayer->setFrequency(this->backupFreq); + state = this->phyLayer->setFrequency(this->rx2.freq); RADIOLIB_ASSERT(state); DataRate_t dataRate; - findDataRate(RADIOLIB_LORAWAN_DATA_RATE_UNUSED, &dataRate, &this->band->backupChannel); + findDataRate(this->rx2.drMax, &dataRate); state = this->phyLayer->setDataRate(dataRate); RADIOLIB_ASSERT(state); - } } @@ -609,33 +729,23 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { this->phyLayer->invertIQ(false); } - // restore the original uplink channel - this->configureChannel(RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK); - return(RADIOLIB_ERR_RX_TIMEOUT); } - // channel scan is finished, swap the actions - this->phyLayer->clearChannelScanAction(); - downlinkReceived = false; - this->phyLayer->setPacketReceivedAction(LoRaWANNodeOnDownlink); - - // start receiving - state = this->phyLayer->startReceive(); - RADIOLIB_ASSERT(state); - // wait for reception with some timeout uint32_t rxStart = mod->hal->millis(); while(!downlinkReceived) { mod->hal->yield(); - // let's hope 30 seconds is long enough timeout - if(mod->hal->millis() - rxStart >= 30000) { + // 3 seconds is maximum airtime + if(mod->hal->millis() - rxStart >= 3000) { // timed out + RADIOLIB_DEBUG_PRINTLN("Packet length: %d", this->phyLayer->getPacketLength()); this->phyLayer->standby(); if(!this->FSK) { this->phyLayer->invertIQ(false); } - return(RADIOLIB_ERR_RX_TIMEOUT); + // return(RADIOLIB_ERR_RX_TIMEOUT); + break; } } @@ -681,15 +791,6 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { if(state == RADIOLIB_ERR_LORA_HEADER_DAMAGED) { state = RADIOLIB_ERR_NONE; } - - // get the frame counter and set it to the MIC calculation block - // TODO this will not handle overflow into 32-bits! - // TODO cache the ADR bit? - uint16_t fcnt = LoRaWANNode::ntoh(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FCNT_POS]); - LoRaWANNode::hton(&downlinkMsg[RADIOLIB_LORAWAN_BLOCK_FCNT_POS], fcnt); - - RADIOLIB_DEBUG_PRINTLN("downlinkMsg:"); - RADIOLIB_DEBUG_HEXDUMP(downlinkMsg, RADIOLIB_AES128_BLOCK_SIZE + downlinkMsgLen); if(state != RADIOLIB_ERR_NONE) { #if !defined(RADIOLIB_STATIC_ONLY) @@ -698,6 +799,58 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { return(state); } + // get the frame counter and set it to the MIC calculation block + // TODO cache the ADR bit? + uint16_t fcnt16 = LoRaWANNode::ntoh(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FCNT_POS]); + LoRaWANNode::hton(&downlinkMsg[RADIOLIB_LORAWAN_BLOCK_FCNT_POS], fcnt16); + uint32_t fcnt32 = fcnt16; // calculate possible rollover once decided if this is network downlink or application downlink + + RADIOLIB_DEBUG_PRINTLN("downlinkMsg:"); + RADIOLIB_DEBUG_HEXDUMP(downlinkMsg, RADIOLIB_AES128_BLOCK_SIZE + downlinkMsgLen); + + // calculate length of FOpts and payload + uint8_t foptsLen = downlinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] & RADIOLIB_LORAWAN_FHDR_FOPTS_LEN_MASK; + int payLen = downlinkMsgLen - 8 - foptsLen - sizeof(uint32_t); + + bool isAppDownlink = true; + if (payLen <= 0 && this->rev == 1) { // no payload => MAC commands only => Network frame (LoRaWAN v1.1 only) + isAppDownlink = false; + } + + // check the FcntDown value (Network or Application) + uint32_t fcntDownPrev = 0; + if (isAppDownlink) { + fcntDownPrev = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_A_FCNT_DOWN_ID); + } else { + fcntDownPrev = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_N_FCNT_DOWN_ID); + } + + // if this is not the first downlink... + // assume a 16-bit to 32-bit rollover if difference between counters in LSB is smaller than MAX_FCNT_GAP + // if that isn't the case and the received fcnt is smaller or equal to the last heard fcnt, then error + if(fcntDownPrev > 0) { + if((fcnt16 <= fcntDownPrev) && ((0xFFFF - (uint16_t)fcntDownPrev + fcnt16) > RADIOLIB_LORAWAN_MAX_FCNT_GAP)) { + #if !defined(RADIOLIB_STATIC_ONLY) + delete[] downlinkMsg; + #endif + if (isAppDownlink) { + return(RADIOLIB_ERR_A_FCNT_DOWN_INVALID); + } else { + return(RADIOLIB_ERR_N_FCNT_DOWN_INVALID); + } + } else if (fcnt16 <= fcntDownPrev) { + uint16_t msb = (fcntDownPrev >> 16) + 1; // assume a rollover + fcnt32 |= (msb << 16); // add back the MSB part + } + } + + // save current fcnt to NVM + if (isAppDownlink) { + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_A_FCNT_DOWN_ID, fcnt32); + } else { + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_N_FCNT_DOWN_ID, fcnt32); + } + // check the MIC if(!verifyMIC(downlinkMsg, RADIOLIB_AES128_BLOCK_SIZE + downlinkMsgLen, this->sNwkSIntKey)) { #if !defined(RADIOLIB_STATIC_ONLY) @@ -716,15 +869,14 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { return(RADIOLIB_ERR_DOWNLINK_MALFORMED); } - // check fopts len - uint8_t foptsLen = downlinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] & RADIOLIB_LORAWAN_FHDR_FOPTS_LEN_MASK; + // process FOpts (if there are any) if(foptsLen > 0) { // there are some Fopts, decrypt them uint8_t fopts[RADIOLIB_LORAWAN_FHDR_FOPTS_LEN_MASK]; - // according to the specification, the last two arguments should be 0x00 and false, - // but that will fail even for LoRaWAN 1.1.0 server - processAES(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], (size_t)foptsLen, this->nwkSEncKey, fopts, fcnt, RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, 0x01, true); + // TODO it COULD be the case that the assumed rollover is incorrect, if possible figure out a way to catch this and retry with just fcnt16 + uint8_t ctrId = 0x01 + isAppDownlink; // see LoRaWAN v1.1 errata + processAES(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], (size_t)foptsLen, this->nwkSEncKey, fopts, fcnt32, RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, ctrId, true); RADIOLIB_DEBUG_PRINTLN("fopts:"); RADIOLIB_DEBUG_HEXDUMP(fopts, foptsLen); @@ -749,29 +901,65 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { remLen -= processedLen; foptsPtr += processedLen; } + + // if FOptsLen for the next uplink is larger than can be piggybacked onto an uplink, send separate uplink + if(this->commandsUp.len > 15) { + uint8_t foptsNum = this->commandsUp.numCommands; + size_t foptsBufSize = this->commandsUp.len; + uint8_t foptsBuff[foptsBufSize]; + size_t idx = 0; + for(size_t i = 0; i < foptsNum; i++) { + LoRaWANMacCommand_t cmd = { .cid = 0, .len = 0, .payload = { 0 }, .repeat = 0, }; + popMacCommand(&cmd, &this->commandsUp, i); + if(cmd.cid == 0) { + break; + } + foptsBuff[idx] = cmd.cid; + for(size_t i = 1; i < cmd.len; i++) { + foptsBuff[idx + i] = cmd.payload[i]; + } + idx += cmd.len + 1; + } + RADIOLIB_DEBUG_PRINTLN("Uplink MAC payload (%d commands):", foptsNum); + RADIOLIB_DEBUG_HEXDUMP(foptsBuff, foptsBufSize); + + isMACPayload = true; + this->uplink(foptsBuff, foptsBufSize, RADIOLIB_LORAWAN_FPORT_MAC_COMMAND); + } + + // write the MAC command queue to nvm for next uplink + uint8_t queueBuff[sizeof(LoRaWANMacCommandQueue_t)]; + memcpy(queueBuff, &this->commandsUp, sizeof(LoRaWANMacCommandQueue_t)); + mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FOPTS_ID), queueBuff, sizeof(LoRaWANMacCommandQueue_t)); } - // fopts are processed or not present, check if there is payload - int payLen = downlinkMsgLen - 8 - foptsLen - sizeof(uint32_t); + // a downlink was received, so reset the ADR counter to this uplink's fcnt + uint32_t fcntUp = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID); + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_FCNT_ID, fcntUp); + + // process payload (if there is any) if(payLen <= 0) { // no payload *len = 0; #if !defined(RADIOLIB_STATIC_ONLY) delete[] downlinkMsg; #endif + return(RADIOLIB_ERR_NONE); } // there is payload, and so there should be a port too // TODO pass the port? *len = payLen - 1; - processAES(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], downlinkMsgLen, this->appSKey, data, fcnt, RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, 0x00, true); + // TODO it COULD be the case that the assumed rollover is incorrect, then figure out a way to catch this and retry with just fcnt16 + // TODO does the erratum hold here as well? + processAES(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], downlinkMsgLen, this->appSKey, data, fcnt32, RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, 0x00, true); #if !defined(RADIOLIB_STATIC_ONLY) delete[] downlinkMsg; #endif - return(state); + return(RADIOLIB_ERR_NONE); } void LoRaWANNode::setDeviceStatus(uint8_t battLevel) { @@ -810,20 +998,20 @@ 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); + // 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 int8_t pwr = this->band->powerMax; @@ -858,67 +1046,167 @@ int16_t LoRaWANNode::setPhyProperties() { return(state); } -int16_t LoRaWANNode::setupChannels() { - // find appropriate channel IDs for uplink and downlink, the uplink channel is random - int8_t chMin = -1; - int8_t chMax = -1; - if(this->band->cfListType == RADIOLIB_LORAWAN_CFLIST_TYPE_MASK) { - chMin = this->startChannel; - chMax = this->startChannel + this->numChannels; - } - int16_t state = this->findChannelId(RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK, - &this->chIndex[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK], - &this->dataRate[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK], chMin, chMax); - RADIOLIB_ASSERT(state); - - // RX1 channel is not random, but determined by uplink channel +int16_t LoRaWANNode::setupChannels(uint8_t* cfList) { + uint8_t num = 0; + LoRaWANChannel_t chnl; + + // in case of frequency list-type band, copy the default TX channels into the available channels, with RX1 = TX if(this->band->cfListType == RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES) { - // for frequency-list type bands, it's just the previous uplink channel - this->chIndex[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = this->chIndex[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]; - this->dataRate[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = this->dataRate[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]; - - } else { - // for mask type bands, it's the uplink mod num_downlink_channels - for(uint8_t i = 0; i < this->band->numChannelSpans; i++) { - const LoRaWANChannelSpan_t* span = &this->band->defaultChannels[i]; - if(span->direction == RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK) { - this->chIndex[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = this->chIndex[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] % span->numChannels; - this->dataRate[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = span->joinRequestDataRate; - break; + for(uint8_t i = 0; i < 3; i++) { + chnl = this->band->txFreqs[i]; + if(chnl.enabled) { + availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num] = chnl; + availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][num] = chnl; + RADIOLIB_DEBUG_PRINTLN("Channel UL/DL %d frequency = %f MHz", num, availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].freq); + num++; + } + } + // if(cfList != nullptr) { + // for(uint8_t i = 0; i < 5; i++) { + // 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 + // availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num] = chnl; + // availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][num] = chnl; + // RADIOLIB_DEBUG_PRINTLN("Channel UL/DL %d frequency = %f MHz", num, availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].freq); + // num++; + // } + // } + } else { // RADIOLIB_LORAWAN_CFLIST_TYPE_MASK + 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(uint8_t i = 0; i < 5; i++) { + uint16_t mask = LoRaWANNode::ntoh(&cfList[2*i]); + RADIOLIB_DEBUG_PRINTLN("mask[%d] = 0x%04x", i, mask); + for(uint8_t j = 0; j < 16; j++) { + // 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(mask & (1UL << j)) { + if(chSpan >= this->band->numTxSpans || chNum >= this->band->txSpans[chSpan].numChannels) { + RADIOLIB_DEBUG_PRINTLN("channel bitmask overrun!"); + return(RADIOLIB_ERR_UNKNOWN); + } + chnl.enabled = true; + chnl.idx = i*16 + j; + 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; + RADIOLIB_DEBUG_PRINTLN("Channel UL %d frequency = %f MHz", num, availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].freq); + num++; + } + chNum++; } } } + return(RADIOLIB_ERR_NONE); +} - // based on the channel IDs, find the frequencies - state = this->findChannelFreq(RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK, - this->chIndex[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK], - &this->channelFreq[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]); - RADIOLIB_ASSERT(state); - state = this->findChannelFreq(RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, - this->chIndex[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK], - &this->channelFreq[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK]); - RADIOLIB_ASSERT(state); +int16_t LoRaWANNode::selectChannelsJR(uint16_t devNonce) { + LoRaWANChannel_t channelUp; + LoRaWANChannel_t channelDn; + if(this->band->cfListType == RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES) { + // count the number of available channels for a join-request + uint8_t numJRChannels = 0; + for(uint8_t i = 0; i < 3; i++) { + if(this->band->txFreqs[i].idx != RADIOLIB_LORAWAN_CHANNEL_INDEX_NONE) { + numJRChannels++; + } + if(this->band->txJoinReq[i].idx != RADIOLIB_LORAWAN_CHANNEL_INDEX_NONE) { + numJRChannels++; + } + } + uint8_t channelId = devNonce % numJRChannels; // cycle through channels (seed with devNonce) + if(channelId < 3) { + channelUp = this->band->txFreqs[channelId]; + } else { + channelUp = this->band->txJoinReq[channelId - 3]; + } + channelDn = channelUp; // RX1 is equal to TX + + // configure data rates for TX and RX1: for TX the (floored) average of min and max; for RX1 identical with base offset + this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = int((channelUp.drMax + channelUp.drMin) / 2); + // TODO check bounds + this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = getDownlinkDataRate(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK], + this->rx1DrOffset, this->band->rx1DataRateBase, channelDn.drMin, channelDn.drMax); + } else { // RADIOLIB_LORAWAN_CFLIST_TYPE_MASK + channelUp.enabled = true; + uint8_t numBlocks = this->band->txSpans[0].numChannels / 8; // calculate number of 8-channel blocks + uint8_t numBlockChannels = 8 + this->band->txSpans[1].numChannels > 0 ? 1 : 0; // add a 9th channel if there's a second span + uint8_t blockID = devNonce % numBlocks; // currently selected block (seed with devNonce) + uint8_t channelID = this->phyLayer->random(numBlockChannels); // select randomly from these 8 or 9 channels + uint8_t spanID; + if(channelID < 8) { + spanID = 0; + channelUp.idx = blockID * numBlockChannels + channelID; + } else { + spanID = 1; + channelUp.idx = blockID; + } + channelUp.freq = this->band->txSpans[spanID].freqStart + channelUp.idx*this->band->txSpans[spanID].freqStep; - // configure channel for uplink - state = this->configureChannel(RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK); - return(state); + channelDn.idx = blockID % this->band->rx1Span.numChannels; + channelDn.freq = this->band->rx1Span.freqStart + channelDn.idx*this->band->rx1Span.freqStep; + + // configure data rates for TX and RX1: for TX the specified value for this band; for RX1 identical with base offset + this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = this->band->txSpans[spanID].joinRequestDataRate; + // TODO check bounds + this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = getDownlinkDataRate(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK], + this->rx1DrOffset, this->band->rx1DataRateBase, channelDn.drMin, channelDn.drMax); + } + this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = channelUp; + this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = channelDn; + + Module* mod = this->phyLayer->getMod(); + uint8_t txDrRx2Dr = (this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] << 4) | this->rx2.drMax; + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXDR_RX2DR_ID, txDrRx2Dr); + return(RADIOLIB_ERR_NONE); } -uint8_t LoRaWANNode::findDataRate(uint8_t dr, DataRate_t* dataRate, const LoRaWANChannelSpan_t* span) { - uint8_t dataRateBand = 0; - uint8_t dataRateFound = 0; - if(dr == RADIOLIB_LORAWAN_DATA_RATE_UNUSED) { - for(uint8_t i = 0; i < RADIOLIB_LORAWAN_CHANNEL_NUM_DATARATES; i++) { - if(span->dataRates[i] != RADIOLIB_LORAWAN_DATA_RATE_UNUSED) { - dataRateBand = span->dataRates[i]; - dataRateFound = i; - break; - } +int16_t LoRaWANNode::selectChannels() { + // figure out which channel IDs are enabled (chMask may have disabled some) and are valid for the current datarate + uint8_t numChannels = 0; + uint8_t channelsEnabled[RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS]; + for(uint8_t i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { + if(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled) { + if(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] >= this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].drMin + && this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] <= this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].drMax) { + channelsEnabled[numChannels] = i; + numChannels++; + } + } else { + break; } - } else { - dataRateBand = span->dataRates[dr]; - dataRateFound = dr; } + // 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]; + + // in case of frequency list-type band, downlink is equal to uplink, otherwise retrieve `modulo` numChannels + if(this->band->cfListType == RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES) { + this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][channelID]; + } else { // RADIOLIB_LORAWAN_CFLIST_TYPE_MASK + LoRaWANChannel_t channelDn; + channelDn.enabled = true; + channelDn.idx = this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK].idx % this->band->rx1Span.numChannels; + channelDn.freq = this->band->rx1Span.freqStart + channelDn.idx*this->band->rx1Span.freqStep; + channelDn.drMin = this->band->rx1Span.drMin; + channelDn.drMax = this->band->rx1Span.drMax; + this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = channelDn; + } + return(RADIOLIB_ERR_NONE); +} + +int16_t LoRaWANNode::findDataRate(uint8_t dr, DataRate_t* dataRate) { + uint8_t dataRateBand = this->band->dataRates[dr]; if(dataRateBand & RADIOLIB_LORAWAN_DATA_RATE_FSK_50_K) { dataRate->fsk.bitRate = 50; @@ -942,164 +1230,69 @@ uint8_t LoRaWANNode::findDataRate(uint8_t dr, DataRate_t* dataRate, const LoRaWA dataRate->lora.spreadingFactor = ((dataRateBand & 0x70) >> 4) + 6; dataRate->lora.codingRate = (dataRateBand & 0x03) + 5; + RADIOLIB_DEBUG_PRINTLN("DR %d: LORA (SF: %d, BW: %f, CR: %d)", + dataRateBand, dataRate->lora.spreadingFactor, dataRate->lora.bandwidth, dataRate->lora.codingRate); } - return(dataRateFound); -} - -int16_t LoRaWANNode::findChannelId(uint8_t dir, uint8_t* ch, uint8_t* dr, int8_t min, int8_t max) { - // find the first channel span that supports the requested direction - uint8_t spanId = 0; - LoRaWANChannelSpan_t* span = NULL; - for(; spanId < this->band->numChannelSpans; spanId++) { - span = (LoRaWANChannelSpan_t*)&this->band->defaultChannels[spanId]; - if((span->direction == dir) || (span->direction == RADIOLIB_LORAWAN_CHANNEL_DIR_BOTH)) { - break; - } - } - - // shouldn't happen, but just to be sure - if(!span) { - RADIOLIB_DEBUG_PRINTLN("findChannelId span not found"); - return(RADIOLIB_ERR_INVALID_CHANNEL); - } - - // if requested, save the data rate - if(dr) { - *dr = span->joinRequestDataRate; - } - - // determine min and max based on number of channels in span and user constraints - uint8_t chMin = (min > 0) ? min : 0; - uint8_t chMax = (max > 0) ? max : span->numChannels; - - // select channel ID as random number between min and max (global number 0 - N for single direction) - int32_t chId = this->phyLayer->random(chMin, chMax); - *ch = chId; - return(RADIOLIB_ERR_NONE); -} - -LoRaWANChannelSpan_t* LoRaWANNode::findChannelSpan(uint8_t dir, uint8_t ch, uint8_t* spanChannelId) { - // find the span based on the channel ID - uint8_t chanCtr = 0; - *spanChannelId = 0; - for(uint8_t span = 0; span < this->band->numChannelSpans; span++) { - // check if this channel span can be used - uint8_t direction = this->band->defaultChannels[span].direction; - if((direction != dir) && (direction != RADIOLIB_LORAWAN_CHANNEL_DIR_BOTH)) { - continue; - } - - // iterate over the usable spans to the channel ID - for(; *spanChannelId < this->band->defaultChannels[span].numChannels; (*spanChannelId)++) { - if(chanCtr >= ch) { - // we found it, return the pointer (channel ID within the span is already set) - return((LoRaWANChannelSpan_t*)&this->band->defaultChannels[span]); - } - chanCtr++; - } - } - - return(NULL); -} - -int16_t LoRaWANNode::findChannelFreq(uint8_t dir, uint8_t ch, float* freq) { - // find the channel span based on channel ID and direction - uint8_t spanChannelId = 0; - LoRaWANChannelSpan_t* span = findChannelSpan(dir, ch, &spanChannelId); - if(!span) { - return(RADIOLIB_ERR_INVALID_CHANNEL); - } - - // set the frequency - *freq = span->freqStart + span->freqStep * (float)spanChannelId; return(RADIOLIB_ERR_NONE); } int16_t LoRaWANNode::configureChannel(uint8_t dir) { // set the frequency - RADIOLIB_DEBUG_PRINTLN("Channel frequency %cL = %f MHz", dir ? 'D' : 'U', this->channelFreq[dir]); - int state = this->phyLayer->setFrequency(this->channelFreq[dir]); + RADIOLIB_DEBUG_PRINTLN(""); + RADIOLIB_DEBUG_PRINTLN("Channel frequency %cL = %f MHz", dir ? 'D' : 'U', this->currentChannels[dir].freq); + RADIOLIB_DEBUG_PRINTLN("Datarate index: %d", this->dataRates[dir]); + int state = this->phyLayer->setFrequency(this->currentChannels[dir].freq); RADIOLIB_ASSERT(state); - // find the channel span based on channel ID and direction - uint8_t spanChannelId = 0; - LoRaWANChannelSpan_t* span = findChannelSpan(dir, this->chIndex[dir], &spanChannelId); - if(!span) { - return(RADIOLIB_ERR_INVALID_CHANNEL); - } - // set the data rate DataRate_t dataRate; - this->dataRate[dir] = findDataRate(this->dataRate[dir], &dataRate, span); + findDataRate(this->dataRates[dir], &dataRate); + state = this->phyLayer->setDataRate(dataRate); return(state); } -int16_t LoRaWANNode::sendMacCommand(uint8_t cid, uint8_t* payload, size_t payloadLen, uint8_t* reply, size_t replyLen) { - // build the command - size_t macReqLen = 1 + payloadLen; - #if !defined(RADIOLIB_STATIC_ONLY) - uint8_t* macReqBuff = new uint8_t[macReqLen]; - #else - uint8_t macReqBuff[RADIOLIB_STATIC_ARRAY_SIZE]; - #endif - macReqBuff[0] = cid; - memcpy(&macReqBuff[1], payload, payloadLen); - - // uplink it - int16_t state = this->uplink(macReqBuff, macReqLen, RADIOLIB_LORAWAN_FPORT_MAC_COMMAND); - #if !defined(RADIOLIB_STATIC_ONLY) - delete[] macReqBuff; - #endif - RADIOLIB_ASSERT(state); - - // build the reply buffer - size_t macRplLen = 1 + replyLen; - #if !defined(RADIOLIB_STATIC_ONLY) - uint8_t* macRplBuff = new uint8_t[this->band->payloadLenMax[this->dataRate[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK]]]; - #else - uint8_t macRplBuff[RADIOLIB_STATIC_ARRAY_SIZE]; - #endif - - // wait for reply from the server - size_t rxRplLen = 0; - state = this->downlink(macRplBuff, &rxRplLen); - if(state != RADIOLIB_ERR_NONE) { - #if !defined(RADIOLIB_STATIC_ONLY) - delete[] macRplBuff; - #endif - return(state); - } - - RADIOLIB_DEBUG_PRINTLN("macRplBuff:"); - RADIOLIB_DEBUG_HEXDUMP(macRplBuff, rxRplLen); - - // check the length - it may be longer than expected - // if the server decided to append more MAC commands, but never shorter - // TODO how to handle the additional command(s)? - if(rxRplLen < macRplLen) { - #if !defined(RADIOLIB_STATIC_ONLY) - delete[] macRplBuff; - #endif - return(RADIOLIB_ERR_DOWNLINK_MALFORMED); +int16_t LoRaWANNode::saveChannels() { + uint8_t bytesPerChannel = 5; + uint8_t numBytes = 2 * RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS * bytesPerChannel; + 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; + 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); +} - // check the CID - if(macRplBuff[0] != cid) { - #if !defined(RADIOLIB_STATIC_ONLY) - delete[] macRplBuff; - #endif - return(RADIOLIB_ERR_INVALID_CID); +int16_t LoRaWANNode::restoreChannels() { + uint8_t bytesPerChannel = 5; + uint8_t numBytes = 2 * RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS * bytesPerChannel; + uint8_t buffer[numBytes]; + 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[5] = { 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; + } } - - // copy the data - memcpy(reply, &macRplBuff[1], replyLen); - #if !defined(RADIOLIB_STATIC_ONLY) - delete[] macRplBuff; - #endif - - return(state); + return(RADIOLIB_ERR_NONE); } int16_t LoRaWANNode::pushMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQueue_t* queue) { @@ -1118,40 +1311,72 @@ int16_t LoRaWANNode::pushMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQ queue->commands[queue->numCommands - 1].payload[4], queue->commands[queue->numCommands - 1].repeat);*/ queue->numCommands++; + queue->len += 1 + cmd->len; // 1 byte for command ID, len bytes for payload return(RADIOLIB_ERR_NONE); } -int16_t LoRaWANNode::popMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQueue_t* queue, bool force) { +int16_t LoRaWANNode::popMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQueue_t* queue, size_t index) { if(queue->numCommands == 0) { return(RADIOLIB_ERR_COMMAND_QUEUE_EMPTY); } if(cmd) { - /*RADIOLIB_DEBUG_PRINTLN("pop MAC CID = %02x, len = %d, payload = %02x %02x %02x %02x %02x, repeat = %d ", - queue->commands[queue->numCommands - 1].cid, - queue->commands[queue->numCommands - 1].len, - queue->commands[queue->numCommands - 1].payload[0], - queue->commands[queue->numCommands - 1].payload[1], - queue->commands[queue->numCommands - 1].payload[2], - queue->commands[queue->numCommands - 1].payload[3], - queue->commands[queue->numCommands - 1].payload[4], - queue->commands[queue->numCommands - 1].repeat);*/ - memcpy(cmd, &queue->commands[queue->numCommands - 1], sizeof(LoRaWANMacCommand_t)); - } - - if((!force) && (queue->commands[queue->numCommands - 1].repeat > 0)) { - queue->commands[queue->numCommands - 1].repeat--; + // RADIOLIB_DEBUG_PRINTLN("pop MAC CID = %02x, len = %d, payload = %02x %02x %02x %02x %02x, repeat = %d ", + // queue->commands[index].cid, + // queue->commands[index].len, + // queue->commands[index].payload[0], + // queue->commands[index].payload[1], + // queue->commands[index].payload[2], + // queue->commands[index].payload[3], + // queue->commands[index].payload[4], + // queue->commands[index].repeat); + memcpy(cmd, &queue->commands[index], sizeof(LoRaWANMacCommand_t)); + } + + if(queue->commands[index].repeat > 0) { + queue->commands[index].repeat--; } else { - queue->commands[queue->numCommands - 1].repeat = 0; - queue->numCommands--; + deleteMacCommand(queue->commands[index].cid, queue); } return(RADIOLIB_ERR_NONE); } +int16_t LoRaWANNode::deleteMacCommand(uint8_t cid, LoRaWANMacCommandQueue_t* queue) { + 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) { + // RADIOLIB_DEBUG_PRINTLN("delete MAC CID = %02x, len = %d, payload = %02x %02x %02x %02x %02x, repeat = %d ", + // queue->commands[index].cid, + // queue->commands[index].len, + // queue->commands[index].payload[0], + // queue->commands[index].payload[1], + // queue->commands[index].payload[2], + // queue->commands[index].payload[3], + // queue->commands[index].payload[4], + // queue->commands[index].repeat); + 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) { + memmove(&queue->commands[index], &queue->commands[index + 1], (RADIOLIB_LORAWAN_MAC_COMMAND_QUEUE_SIZE - index) * sizeof(LoRaWANMacCommand_t)); + } + // set the latest element to all 0 + memset(&queue->commands[RADIOLIB_LORAWAN_MAC_COMMAND_QUEUE_SIZE - 1], 0x00, sizeof(LoRaWANMacCommand_t)); + queue->numCommands--; + return(RADIOLIB_ERR_NONE); + } + } + + return(RADIOLIB_ERR_COMMAND_QUEUE_ITEM_NOT_FOUND); +} + size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { RADIOLIB_DEBUG_PRINTLN("exe MAC CID = %02x, len = %d", cmd->cid, cmd->len); + Module* mod = this->phyLayer->getMod(); if(cmd->cid >= RADIOLIB_LORAWAN_MAC_CMD_PROPRIETARY) { // TODO call user-provided callback for proprietary MAC commands? @@ -1165,7 +1390,7 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { RADIOLIB_DEBUG_PRINTLN("Server version: 1.%d", srvVersion); if(srvVersion == this->rev) { // valid server version, stop sending the ResetInd MAC command - popMacCommand(NULL, &this->commandsUp, true); + deleteMacCommand(RADIOLIB_LORAWAN_MAC_CMD_RESET, &this->commandsUp); } return(1); } break; @@ -1191,46 +1416,55 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { // apply the configuration uint8_t drAck = 0; - if(dr != 0x0F) { - // first figure out which channel span this data rate applies to - // TODO do that by processing the chMask/chMaskCntl? - uint8_t spanChannelId = 0; - LoRaWANChannelSpan_t* span = findChannelSpan(RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK, this->chIndex[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK], &spanChannelId); - - // seems to be only applicable to uplink - if(span) { - DataRate_t dataRate; - this->dataRate[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = findDataRate(dr, &dataRate, span); - if(this->phyLayer->setDataRate(dataRate) == RADIOLIB_ERR_NONE) { - RADIOLIB_DEBUG_PRINTLN("ADR set dr = %d channel = %d", dr, spanChannelId); - drAck = 1; - } + if(dr == 0x0F) { + drAck = 1; + } else if (this->band->dataRates[dr] != RADIOLIB_LORAWAN_DATA_RATE_UNUSED) { + this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = dr; + this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = dr + this->band->rx1DataRateBase - this->rx1DrOffset; + if(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] < this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK].drMin) { + this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK].drMin; + } else if(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] > this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK].drMax) { + this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK].drMax; } - } else { + uint8_t txDrRx2Dr = (this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] << 4) | this->rx2.drMax; + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXDR_RX2DR_ID, txDrRx2Dr); drAck = 1; - - } + } // try to apply the power configuration uint8_t pwrAck = 0; - if(txPower != 0x0F) { + if(txPower == 0x0F) { + pwrAck = 1; + } else { int8_t pwr = this->band->powerMax - 2*txPower; if(this->phyLayer->setOutputPower(pwr) == RADIOLIB_ERR_NONE) { RADIOLIB_DEBUG_PRINTLN("ADR set pwr = %d", pwr); pwrAck = 1; } - - } else { - pwrAck = 1; } - // TODO implement repeated uplinks with nbTrans - (void)nbTrans; + this->nbTrans = nbTrans; // TODO implement channel mask - uint8_t chMaskAck = 0; + uint8_t chMaskAck = 1; (void)chMask; (void)chMaskCntl; + if(this->band->cfListType == RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES) { + for(uint8_t i = 0; i < 16; i++) { + // check if this channel ID should be enabled + 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].enabled == false) { + chMaskAck = 0; + break; + } + } else { + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = false; + } + } + } else { + + } // send the reply cmd->len = 1; @@ -1251,28 +1485,26 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { case(RADIOLIB_LORAWAN_MAC_CMD_RX_PARAM_SETUP): { // get the configuration - uint8_t rx1DrOffset = (cmd->payload[0] & 0x70) >> 4; - uint8_t rx2DataRate = cmd->payload[0] & 0x0F; + this->rx1DrOffset = (cmd->payload[0] & 0x70) >> 4; + uint8_t rx1OffsAck = 1; + this->rx2.drMax = cmd->payload[0] & 0x0F; + uint8_t rx2Ack = 1; uint32_t freqRaw = LoRaWANNode::ntoh(&cmd->payload[1], 3); - float freq = (float)freqRaw/10000.0; - RADIOLIB_DEBUG_PRINTLN("RX Param: rx1DrOffset = %d, rx2DataRate = %d, freq = %f", rx1DrOffset, rx2DataRate, freq); + this->rx2.freq = (float)freqRaw/10000.0; + RADIOLIB_DEBUG_PRINTLN("Rx param REQ: rx1DrOffset = %d, rx2DataRate = %d, freq = %f", this->rx1DrOffset, this->rx2.drMax, this->rx2.freq); // apply the configuration - this->backupFreq = freq; - float prevFreq = this->channelFreq[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK]; uint8_t chanAck = 0; - if(this->phyLayer->setFrequency(freq) == RADIOLIB_ERR_NONE) { + if(this->phyLayer->setFrequency(this->rx2.freq) == RADIOLIB_ERR_NONE) { chanAck = 1; - this->phyLayer->setFrequency(prevFreq); + this->phyLayer->setFrequency(this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK].freq); } - // TODO process the RX2 data rate - (void)rx2DataRate; - uint8_t rx2Ack = 0; - - // TODO process the data rate offset - (void)rx1DrOffset; - uint8_t rx1OffsAck = 0; + // update saved values + uint8_t txDrRx2Dr = (this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] << 4) | this->rx2.drMax; + uint8_t rx1DrOffDel = (this->rx1DrOffset << 4) | (this->rxDelays[0] / 1000); + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXDR_RX2DR_ID, txDrRx2Dr); + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_RX1_DROFF_DEL_ID, rx1DrOffDel); // send the reply cmd->len = 1; @@ -1302,15 +1534,81 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { uint8_t maxDr = (cmd->payload[4] & 0xF0) >> 4; uint8_t minDr = cmd->payload[4] & 0x0F; 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 == 0) { + 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; + } + } + + // update saved frequencies + this->saveChannels(); + + // send the reply + cmd->len = 1; + cmd->payload[0] = (newChAck << 1) | (freqAck << 0); + + pushMacCommand(cmd, &this->commandsUp); - // TODO implement this - (void)chIndex; - (void)freq; - (void)maxDr; - (void)minDr; return(5); } break; + case(RADIOLIB_LORAWAN_MAC_CMD_DL_CHANNEL): { + // get the configuration + uint8_t chIndex = cmd->payload[0]; + uint32_t freqRaw = LoRaWANNode::ntoh(&cmd->payload[1], 3); + float freq = (float)freqRaw/10000.0; + RADIOLIB_DEBUG_PRINTLN("DL channel: index = %d, freq = %f MHz", chIndex, freq); + uint8_t freqDlAck = 0; + uint8_t freqUlAck = 0; + + // check if the frequency is possible + if(this->phyLayer->setFrequency(freq) == RADIOLIB_ERR_NONE) { + freqDlAck = 1; + this->phyLayer->setFrequency(this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK].freq); + } + + // update the downlink frequency + for(int i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) { + if(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].idx == chIndex) { + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].freq = freq; + // check if the corresponding uplink frequency is actually set + if(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].freq > 0) { + freqUlAck = 1; + } + } + } + + // update saved frequencies + this->saveChannels(); + + // send the reply + cmd->len = 1; + cmd->payload[0] = (freqUlAck << 1) | (freqDlAck << 0); + + pushMacCommand(cmd, &this->commandsUp); + + return(4); + } break; + case(RADIOLIB_LORAWAN_MAC_CMD_RX_TIMING_SETUP): { // get the configuration uint8_t delay = cmd->payload[0] & 0x0F; @@ -1323,6 +1621,10 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { this->rxDelays[0] = delay * 1000; this->rxDelays[1] = this->rxDelays[0] + 1000; + // update saved values + uint8_t rx1DrOffDel = (this->rx1DrOffset << 4) | (this->rxDelays[0] / 1000); + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_RX1_DROFF_DEL_ID, rx1DrOffDel); + // send the reply cmd->len = 0; @@ -1349,37 +1651,25 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { return(1); } break; - case(RADIOLIB_LORAWAN_MAC_CMD_DL_CHANNEL): { - // get the configuration - uint8_t chIndex = cmd->payload[0]; - uint32_t freqRaw = LoRaWANNode::ntoh(&cmd->payload[1], 3); - float freq = (float)freqRaw/10000.0; - RADIOLIB_DEBUG_PRINTLN("DL channel: index = %d, freq = %f MHz", chIndex, freq); - - // TODO implement this - (void)chIndex; - (void)freq; - return(4); - } break; - case(RADIOLIB_LORAWAN_MAC_CMD_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 - popMacCommand(NULL, &this->commandsUp, true); + deleteMacCommand(RADIOLIB_LORAWAN_MAC_CMD_REKEY, &this->commandsUp); } return(1); } break; case(RADIOLIB_LORAWAN_MAC_CMD_ADR_PARAM_SETUP): { - // TODO implement this uint8_t limitExp = (cmd->payload[0] & 0xF0) >> 4; uint8_t delayExp = cmd->payload[0] & 0x0F; RADIOLIB_DEBUG_PRINTLN("ADR param setup: limitExp = %d, delayExp = %d", limitExp, delayExp); - (void)limitExp; - (void)delayExp; + + // update saved values + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_LIM_DEL_ID, cmd->payload[0]); + return(1); } break; diff --git a/src/protocols/LoRaWAN/LoRaWAN.h b/src/protocols/LoRaWAN/LoRaWAN.h index e0ec0af43..636c70c30 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.h +++ b/src/protocols/LoRaWAN/LoRaWAN.h @@ -5,6 +5,9 @@ #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) + // preamble format #define RADIOLIB_LORAWAN_LORA_SYNC_WORD (0x34) #define RADIOLIB_LORAWAN_LORA_PREAMBLE_LEN (8) @@ -68,7 +71,8 @@ #define RADIOLIB_LORAWAN_CHANNEL_DIR_NONE (0x03 << 0) #define RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES (0) #define RADIOLIB_LORAWAN_CFLIST_TYPE_MASK (1) -#define RADIOLIB_LORAWAN_CHANNEL_NUM_DATARATES (16) +#define RADIOLIB_LORAWAN_CHANNEL_NUM_DATARATES (15) +#define RADIOLIB_LORAWAN_CHANNEL_INDEX_NONE (0xFF < 0) // recommended default settings #define RADIOLIB_LORAWAN_RECEIVE_DELAY_1_MS (1000) @@ -78,8 +82,8 @@ #define RADIOLIB_LORAWAN_JOIN_ACCEPT_DELAY_1_MS (5000) #define RADIOLIB_LORAWAN_JOIN_ACCEPT_DELAY_2_MS (6000) #define RADIOLIB_LORAWAN_MAX_FCNT_GAP (16384) -#define RADIOLIB_LORAWAN_ADR_ACK_LIMIT (64) -#define RADIOLIB_LORAWAN_ADR_ACK_DELAY (32) +#define RADIOLIB_LORAWAN_ADR_ACK_LIMIT_EXP (0x06) +#define RADIOLIB_LORAWAN_ADR_ACK_DELAY_EXP (0x05) #define RADIOLIB_LORAWAN_RETRANSMIT_TIMEOUT_MIN_MS (1000) #define RADIOLIB_LORAWAN_RETRANSMIT_TIMEOUT_MAX_MS (3000) #define RADIOLIB_LORAWAN_POWER_STEP_SIZE_DBM (-2) @@ -147,7 +151,7 @@ #define RADIOLIB_LORAWAN_MIC_CH_INDEX_POS (4) // magic word saved in persistent memory upon activation -#define RADIOLIB_LORAWAN_MAGIC (0x12AD101B) +#define RADIOLIB_LORAWAN_MAGIC (0x39EA) // MAC commands #define RADIOLIB_LORAWAN_MAC_CMD_RESET (0x01) @@ -171,20 +175,39 @@ #define RADIOLIB_LORAWAN_MAC_COMMAND_QUEUE_SIZE (8) // the maximum number of simultaneously available channels -#define RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS (8) +#define RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS (16) /*! \struct LoRaWANChannelSpan_t \brief Structure to save information about LoRaWAN channels. To save space, adjacent channels are saved in "spans". */ -struct LoRaWANChannelSpan_t { - /*! \brief Whether this channel span is for uplink, downlink, or both directions*/ - uint8_t direction; +struct LoRaWANChannel_t { + /*! \brief Whether this channel is enabled (can be used) or is disabled */ + bool enabled; - /*! \brief Allowed data rates for a join request message */ - uint8_t joinRequestDataRate; + /*! \brief The channel number, as specified by defaults or the network */ + uint8_t idx; + + /*! \brief The channel frequency */ + float freq; + + /*! \brief Minimum allowed datarate for this channel */ + uint8_t drMin; + /*! \brief Maximum allowed datarate for this channel (inclusive) */ + uint8_t drMax; +}; + +// alias for unused channel +#define RADIOLIB_LORAWAN_CHANNEL_NONE { .enabled = false, .idx = RADIOLIB_LORAWAN_CHANNEL_INDEX_NONE, .freq = 0, .drMin = 0, .drMax = 0 } + +/*! + \struct LoRaWANChannelSpan_t + \brief Structure to save information about LoRaWAN channels. + To save space, adjacent channels are saved in "spans". +*/ +struct LoRaWANChannelSpan_t { /*! \brief Total number of channels in the span */ uint8_t numChannels; @@ -194,24 +217,24 @@ struct LoRaWANChannelSpan_t { /*! \brief Frequency step between adjacent channels */ float freqStep; - /*! \brief Array of datarates supported by all channels in the span */ - uint8_t dataRates[RADIOLIB_LORAWAN_CHANNEL_NUM_DATARATES]; + /*! \brief Minimum allowed datarate for all channels in this span */ + uint8_t drMin; + + /*! \brief Maximum allowed datarate for all channels in this span (inclusive) */ + uint8_t drMax; + + /*! \brief Allowed data rates for a join request message */ + uint8_t joinRequestDataRate; }; // alias for unused channel span -#define RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE { .direction = RADIOLIB_LORAWAN_CHANNEL_DIR_NONE, .joinRequestDataRate = RADIOLIB_LORAWAN_DATA_RATE_UNUSED, .numChannels = 0, .freqStart = 0, .freqStep = 0, .dataRates = { 0 } } +#define RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE { .numChannels = 0, .freqStart = 0, .freqStep = 0, .drMin = 0, .drMax = 0, .joinRequestDataRate = RADIOLIB_LORAWAN_DATA_RATE_UNUSED } /*! \struct LoRaWANBand_t \brief Structure to save information about LoRaWAN band */ struct LoRaWANBand_t { - /*! \brief The base downlink data rate. Used to calculate data rate changes for adaptive data rate */ - uint8_t downlinkDataRateBase; - - /*! \brief The minimum allowed downlink data rate. Used to calculate data rate changes for adaptive data rate */ - uint8_t downlinkDataRateMin; - /*! \brief Array of allowed maximum payload lengths for each data rate */ uint8_t payloadLenMax[RADIOLIB_LORAWAN_CHANNEL_NUM_DATARATES]; @@ -224,17 +247,29 @@ struct LoRaWANBand_t { /*! \brief Whether the optional channels are defined as list of frequencies or bit mask */ uint8_t cfListType; - /*! \brief FSK channel frequency */ - float fskFreq; + /*! \brief A set of default uplink (TX) channels for frequency-type bands */ + LoRaWANChannel_t txFreqs[3]; + + /*! \brief A set of possible extra channels for the Join-Request message for frequency-type bands */ + LoRaWANChannel_t txJoinReq[3]; - /*! \brief Number of channel spans in the band */ - uint8_t numChannelSpans; + /*! \brief The number of TX channel spans for mask-type bands */ + uint8_t numTxSpans; + + /*! \brief Default uplink (TX) channel spans for mask-type bands, including Join-Request parameters */ + LoRaWANChannelSpan_t txSpans[2]; + + /*! \brief Default downlink (RX1) channel span for mask-type bands */ + LoRaWANChannelSpan_t rx1Span; + + /*! \brief The base downlink data rate. Used to calculate data rate changes for adaptive data rate */ + uint8_t rx1DataRateBase; - /*! \brief Default uplink (TX/RX1) channels defined by LoRaWAN Regional Parameters */ - LoRaWANChannelSpan_t defaultChannels[3]; + /*! \brief Backup channel for downlink (RX2) window */ + LoRaWANChannel_t rx2; - /*! \brief Backup downlink (RX2) channel - just a single channel, but using the same structure for convenience */ - LoRaWANChannelSpan_t backupChannel; + /*! \brief The corresponding datarates, bandwidths and coding rates for DR index */ + uint8_t dataRates[RADIOLIB_LORAWAN_CHANNEL_NUM_DATARATES]; }; // supported bands @@ -257,7 +292,7 @@ struct LoRaWANMacCommand_t { uint8_t cid; /*! \brief Length of the payload */ - size_t len; + uint8_t len; /*! \brief Payload buffer (5 bytes is the longest possible) */ uint8_t payload[5]; @@ -267,8 +302,9 @@ struct LoRaWANMacCommand_t { }; struct LoRaWANMacCommandQueue_t { + uint8_t numCommands; + uint8_t len; LoRaWANMacCommand_t commands[RADIOLIB_LORAWAN_MAC_COMMAND_QUEUE_SIZE]; - size_t numCommands; }; /*! @@ -292,6 +328,15 @@ class LoRaWANNode { (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; + + // RX2 channel properties - may be changed by MAC command + LoRaWANChannel_t rx2; + + // Number of allowed frame retransmissions + uint8_t nbTrans; + /*! \brief Default constructor. \param phy Pointer to the PhysicalLayer radio module. @@ -306,10 +351,10 @@ class LoRaWANNode { void wipe(); /*! - \brief Join network by loading information from persistent storage. + \brief Restore session by loading information from persistent storage. \returns \ref status_codes */ - int16_t begin(); + int16_t restore(); /*! \brief Join network by performing over-the-air activation. By this procedure, @@ -331,9 +376,10 @@ class LoRaWANNode { \param appSKey Pointer to the application session AES-128 key. \param fNwkSIntKey Pointer to the network session F key (LoRaWAN 1.1), unused for LoRaWAN 1.0. \param sNwkSIntKey Pointer to the network session S key (LoRaWAN 1.1), unused for LoRaWAN 1.0. + \param force Set to true to force a new session, even if one exists. \returns \ref status_codes */ - int16_t beginAPB(uint32_t addr, uint8_t* nwkSKey, uint8_t* appSKey, uint8_t* fNwkSIntKey = NULL, uint8_t* sNwkSIntKey = NULL); + int16_t beginABP(uint32_t addr, uint8_t* nwkSKey, uint8_t* appSKey, uint8_t* fNwkSIntKey = NULL, uint8_t* sNwkSIntKey = NULL, bool force = false); #if defined(RADIOLIB_BUILD_ARDUINO) /*! @@ -393,12 +439,14 @@ class LoRaWANNode { const LoRaWANBand_t* band = NULL; LoRaWANMacCommandQueue_t commandsUp = { - .commands = { { .cid = 0, .len = 0, .payload = { 0 }, .repeat = 0, } }, .numCommands = 0, + .len = 0, + .commands = { { .cid = 0, .len = 0, .payload = { 0 }, .repeat = 0, } }, }; LoRaWANMacCommandQueue_t commandsDown = { - .commands = { { .cid = 0, .len = 0, .payload = { 0 }, .repeat = 0, } }, .numCommands = 0, + .len = 0, + .commands = { { .cid = 0, .len = 0, .payload = { 0 }, .repeat = 0, } }, }; // the following is either provided by the network server (OTAA) @@ -411,24 +459,17 @@ class LoRaWANNode { uint8_t jSIntKey[RADIOLIB_AES128_KEY_SIZE] = { 0 }; // available channel frequencies from list passed during OTA activation - float availableChannelsFreq[2][RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS] = { { 0 }, { 0 } }; + LoRaWANChannel_t availableChannels[2][RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS] = { { 0 }, { 0 } }; - // currently configured channel frequency - float channelFreq[2] = { 0 }; + // currently configured channels for TX and RX1 + LoRaWANChannel_t currentChannels[2] = { RADIOLIB_LORAWAN_CHANNEL_NONE, RADIOLIB_LORAWAN_CHANNEL_NONE }; + + // currently configured datarates for TX and RX1 + uint8_t dataRates[2] = { RADIOLIB_LORAWAN_DATA_RATE_UNUSED, RADIOLIB_LORAWAN_DATA_RATE_UNUSED }; // LoRaWAN revision (1.0 vs 1.1) uint8_t rev = 0; - // currently configured data rate for uplink and downlink: DR0 - DR15 (band-dependent!) - uint8_t dataRate[2] = { 0 }; - - // currently configured channel for uplink and downlink (band-dependent!) - uint8_t chIndex[2] = { 0 }; - - // backup channel properties - may be changed by MAC command - float backupFreq = 0; - uint8_t backupDataRate = 0; - // timestamp to measure the RX1/2 delay (from uplink end) uint32_t rxDelayStart = 0; @@ -438,6 +479,9 @@ class LoRaWANNode { // device status - battery level uint8_t battLevel = 0xFF; + // indicates whether an uplink has MAC commands as payload + bool isMACPayload = false; + // method to generate message integrity code uint32_t generateMIC(uint8_t* msg, size_t len, uint8_t* key); @@ -451,31 +495,34 @@ class LoRaWANNode { // setup uplink/downlink channel data rates and frequencies // will attempt to randomly select based on currently used band plan - int16_t setupChannels(); - - // find the first usable data rate in a given channel span - uint8_t findDataRate(uint8_t dr, DataRate_t* dataRate, const LoRaWANChannelSpan_t* span); + int16_t setupChannels(uint8_t* cfList); - // find a channel ID that conforms to the requested direction and ID range - int16_t findChannelId(uint8_t dir, uint8_t* ch, uint8_t* dr, int8_t min, int8_t max); + // select a set of semi-random TX/RX channels for the join-request and -accept message + int16_t selectChannelsJR(uint16_t devNonce); - // find a channel span that any given channel id belongs to - LoRaWANChannelSpan_t* findChannelSpan(uint8_t dir, uint8_t ch, uint8_t* spanChannelId); + // select a set of random TX/RX channels for up- and downlink + int16_t selectChannels(); - // calculate channel frequency in MHz based on channel ID and direction - int16_t findChannelFreq(uint8_t dir, uint8_t ch, float* freq); + // find the first usable data rate for the given band + int16_t findDataRate(uint8_t dr, DataRate_t* dataRate); // configure channel based on cached data rate ID and frequency int16_t configureChannel(uint8_t dir); - // send a MAC command to the network server - int16_t sendMacCommand(uint8_t cid, uint8_t* payload, size_t payloadLen, uint8_t* reply, size_t replyLen); + // save all available channels to persistent storage + int16_t saveChannels(); + + // restore all available channels from persistent storage + int16_t restoreChannels(); // push MAC command to queue, done by copy int16_t pushMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQueue_t* queue); // pop MAC command from queue, done by copy unless CMD is NULL - int16_t popMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQueue_t* queue, bool force = false); + int16_t popMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQueue_t* queue, size_t index); + + // delete a specific MAC command from queue, indicated by the command ID + int16_t deleteMacCommand(uint8_t cid, LoRaWANMacCommandQueue_t* queue); // execute mac command, return the number of processed bytes for sequential processing size_t execMacCommand(LoRaWANMacCommand_t* cmd); diff --git a/src/protocols/LoRaWAN/LoRaWANBands.cpp b/src/protocols/LoRaWAN/LoRaWANBands.cpp index c25b0af9f..efc9c6221 100644 --- a/src/protocols/LoRaWAN/LoRaWANBands.cpp +++ b/src/protocols/LoRaWAN/LoRaWANBands.cpp @@ -2,729 +2,447 @@ #if !defined(RADIOLIB_EXCLUDE_LORAWAN) -uint8_t getDownlinkDataRate(uint8_t uplink, uint8_t offset, uint8_t base, uint8_t lim) { +uint8_t getDownlinkDataRate(uint8_t uplink, uint8_t offset, uint8_t base, uint8_t min, uint8_t max) { int8_t dr = uplink - offset + base; - if(dr < lim) { - dr = lim; + if(dr < min) { + dr = min; + } else if (dr > max) { + dr = max; } return(dr); } const LoRaWANBand_t EU868 = { - .downlinkDataRateBase = 0, - .downlinkDataRateMin = 0, - .payloadLenMax = { - 59, 59, 59, 123, 230, 230, 230, 230, - 0, 0, 0, 0, 0, 0, 0, 0 }, + .payloadLenMax = { 59, 59, 59, 123, 230, 230, 230, 230, 0, 0, 0, 0, 0, 0, 0 }, .powerMax = 16, .powerNumSteps = 7, .cfListType = RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES, - .fskFreq = 868.8, - .numChannelSpans = 1, - .defaultChannels = { - { - .direction = RADIOLIB_LORAWAN_CHANNEL_DIR_BOTH, - .joinRequestDataRate = RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - .numChannels = 3, - .freqStart = 868.1, - .freqStep = 0.2, - .dataRates = { - RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_11 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_250_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_FSK_50_K, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED - } - }, - RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE, + .txFreqs = { + { .enabled = true, .idx = 0, .freq = 868.100, .drMin = 0, .drMax = 5}, + { .enabled = true, .idx = 1, .freq = 868.300, .drMin = 0, .drMax = 5}, + { .enabled = true, .idx = 2, .freq = 868.500, .drMin = 0, .drMax = 5}, + }, + .txJoinReq = { + RADIOLIB_LORAWAN_CHANNEL_NONE, + RADIOLIB_LORAWAN_CHANNEL_NONE, + RADIOLIB_LORAWAN_CHANNEL_NONE + }, + .numTxSpans = 0, + .txSpans = { RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE, + RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE }, - .backupChannel = { - .direction = RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, - .joinRequestDataRate = RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - .numChannels = 1, - .freqStart = 869.858, - .freqStep = 0, - .dataRates = { - RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - } + .rx1Span = RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE, + .rx1DataRateBase = 0, + .rx2 = { .enabled = true, .idx = 0, .freq = 869.525, .drMin = 0, .drMax = 0 }, + .dataRates = { + RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_11 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_250_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_FSK_50_K, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED } }; const LoRaWANBand_t US915 = { - .downlinkDataRateBase = 10, - .downlinkDataRateMin = 8, - .payloadLenMax = { - 19, 61, 133, 250, 250, 0, 0, 0, - 41, 117, 230, 230, 230, 230, 0, 0 }, + .payloadLenMax = { 19, 61, 133, 250, 250, 0, 0, 0, 41, 117, 230, 230, 230, 230, 0 }, .powerMax = 30, .powerNumSteps = 10, .cfListType = RADIOLIB_LORAWAN_CFLIST_TYPE_MASK, - .fskFreq = 0, - .numChannelSpans = 3, - .defaultChannels = { + .txFreqs = { + RADIOLIB_LORAWAN_CHANNEL_NONE, + RADIOLIB_LORAWAN_CHANNEL_NONE, + RADIOLIB_LORAWAN_CHANNEL_NONE + }, + .txJoinReq = { + RADIOLIB_LORAWAN_CHANNEL_NONE, + RADIOLIB_LORAWAN_CHANNEL_NONE, + RADIOLIB_LORAWAN_CHANNEL_NONE + }, + .numTxSpans = 2, + .txSpans = { { - .direction = RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK, - .joinRequestDataRate = 0, .numChannels = 64, - .freqStart = 902.3, - .freqStep = 0.2, - .dataRates = { - RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - } - }, { - .direction = RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK, - .joinRequestDataRate = 4, - .numChannels = 8, - .freqStart = 903, - .freqStep = 1.6, - .dataRates = { - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - } - }, { - .direction = RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, - .joinRequestDataRate = 10, - .numChannels = 8, - .freqStart = 923.3, - .freqStep = 0.6, - .dataRates = { - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_11 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - } + .freqStart = 902.300, + .freqStep = 0.200, + .drMin = 0, + .drMax = 3, + .joinRequestDataRate = 0 }, - }, - .backupChannel = { - .direction = RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, - .joinRequestDataRate = RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - .numChannels = 1, - .freqStart = 923.3, - .freqStep = 0, - .dataRates = { - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + { + .numChannels = 8, + .freqStart = 903.000, + .freqStep = 1.600, + .drMin = 4, + .drMax = 4, + .joinRequestDataRate = 4 } + }, + .rx1Span = { + .numChannels = 8, + .freqStart = 923.300, + .freqStep = 0.600, + .drMin = 8, + .drMax = 13, + .joinRequestDataRate = RADIOLIB_LORAWAN_DATA_RATE_UNUSED + }, + .rx1DataRateBase = 10, + .rx2 = { .enabled = true, .idx = 0, .freq = 923.300, .drMin = 8, .drMax = 8 }, + .dataRates = { + RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_11 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED } }; const LoRaWANBand_t CN780 = { - .downlinkDataRateBase = 0, - .downlinkDataRateMin = 0, - .payloadLenMax = { - 59, 59, 59, 123, 230, 230, 250, 230, - 0, 0, 0, 0, 0, 0, 0, 0 }, + .payloadLenMax = { 59, 59, 59, 123, 230, 230, 250, 230, 0, 0, 0, 0, 0, 0, 0 }, .powerMax = 12, .powerNumSteps = 5, .cfListType = RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES, - .fskFreq = 0, - .numChannelSpans = 1, - .defaultChannels = { - { - .direction = RADIOLIB_LORAWAN_CHANNEL_DIR_BOTH, - .joinRequestDataRate = RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - .numChannels = 6, - .freqStart = 779.5, - .freqStep = 0.2, - .dataRates = { - RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_11 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_250_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_FSK_50_K, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED - } - }, - RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE, + .txFreqs = { + { .enabled = true, .idx = 0, .freq = 779.500, .drMin = 0, .drMax = 5}, + { .enabled = true, .idx = 1, .freq = 779.700, .drMin = 0, .drMax = 5}, + { .enabled = true, .idx = 2, .freq = 779.900, .drMin = 0, .drMax = 5}, + }, + .txJoinReq = { + { .enabled = true, .idx = 3, .freq = 780.500, .drMin = 0, .drMax = 5}, + { .enabled = true, .idx = 4, .freq = 780.700, .drMin = 0, .drMax = 5}, + { .enabled = true, .idx = 5, .freq = 780.900, .drMin = 0, .drMax = 5} + }, + .numTxSpans = 0, + .txSpans = { RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE, + RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE }, - .backupChannel = { - .direction = RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, - .joinRequestDataRate = RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - .numChannels = 1, - .freqStart = 786, - .freqStep = 0, - .dataRates = { - RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - } + .rx1Span = RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE, + .rx1DataRateBase = 0, + .rx2 = { .enabled = true, .idx = 0, .freq = 786.000, .drMin = 0, .drMax = 0 }, + .dataRates = { + RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_11 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_250_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_FSK_50_K, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED } }; const LoRaWANBand_t EU433 = { - .downlinkDataRateBase = 0, - .downlinkDataRateMin = 0, - .payloadLenMax = { - 59, 59, 59, 123, 230, 230, 230, 230, - 0, 0, 0, 0, 0, 0, 0, 0 }, + .payloadLenMax = { 59, 59, 59, 123, 230, 230, 230, 230, 0, 0, 0, 0, 0, 0, 0 }, .powerMax = 12, .powerNumSteps = 5, .cfListType = RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES, - .fskFreq = 0, - .numChannelSpans = 1, - .defaultChannels = { - { - .direction = RADIOLIB_LORAWAN_CHANNEL_DIR_BOTH, - .joinRequestDataRate = RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - .numChannels = 3, - .freqStart = 433.175, - .freqStep = 0.2, - .dataRates = { - RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_11 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_250_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_FSK_50_K, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED - } - }, - RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE, + .txFreqs = { + { .enabled = true, .idx = 0, .freq = 433.175, .drMin = 0, .drMax = 5}, + { .enabled = true, .idx = 1, .freq = 433.375, .drMin = 0, .drMax = 5}, + { .enabled = true, .idx = 2, .freq = 433.575, .drMin = 0, .drMax = 5}, + }, + .txJoinReq = { + RADIOLIB_LORAWAN_CHANNEL_NONE, + RADIOLIB_LORAWAN_CHANNEL_NONE, + RADIOLIB_LORAWAN_CHANNEL_NONE + }, + .numTxSpans = 0, + .txSpans = { RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE, + RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE }, - .backupChannel = { - .direction = RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, - .joinRequestDataRate = RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - .numChannels = 1, - .freqStart = 434.665, - .freqStep = 0, - .dataRates = { - RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - } + .rx1Span = RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE, + .rx1DataRateBase = 0, + .rx2 = { .enabled = true, .idx = 0, .freq = 434.665, .drMin = 0, .drMax = 0 }, + .dataRates = { + RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_11 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_250_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_FSK_50_K, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED } }; const LoRaWANBand_t AU915 = { - .downlinkDataRateBase = 8, - .downlinkDataRateMin = 8, - .payloadLenMax = { - 59, 59, 59, 123, 230, 230, 230, 0, - 41, 117, 230, 230, 230, 230, 0, 0 }, + .payloadLenMax = { 59, 59, 59, 123, 230, 230, 230, 0, 41, 117, 230, 230, 230, 230, 0 }, .powerMax = 30, .powerNumSteps = 10, .cfListType = RADIOLIB_LORAWAN_CFLIST_TYPE_MASK, - .fskFreq = 0, - .numChannelSpans = 3, - .defaultChannels = { + .txFreqs = { + RADIOLIB_LORAWAN_CHANNEL_NONE, + RADIOLIB_LORAWAN_CHANNEL_NONE, + RADIOLIB_LORAWAN_CHANNEL_NONE + }, + .txJoinReq = { + RADIOLIB_LORAWAN_CHANNEL_NONE, + RADIOLIB_LORAWAN_CHANNEL_NONE, + RADIOLIB_LORAWAN_CHANNEL_NONE + }, + .numTxSpans = 2, + .txSpans = { { - .direction = RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK, - .joinRequestDataRate = 0, .numChannels = 64, - .freqStart = 915.2, - .freqStep = 0.2, - .dataRates = { - RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_11 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - } - }, { - .direction = RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK, - .joinRequestDataRate = 6, - .numChannels = 8, - .freqStart = 915.9, - .freqStep = 1.6, - .dataRates = { - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - } - }, { - .direction = RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, - .joinRequestDataRate = RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - .numChannels = 8, - .freqStart = 923.3, - .freqStep = 0.6, - .dataRates = { - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_11 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - } + .freqStart = 915.200, + .freqStep = 0.200, + .drMin = 0, + .drMax = 5, + .joinRequestDataRate = 0 }, - }, - .backupChannel = { - .direction = RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, - .joinRequestDataRate = RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - .numChannels = 1, - .freqStart = 923.3, - .freqStep = 0, - .dataRates = { - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + { + .numChannels = 8, + .freqStart = 915.900, + .freqStep = 1.600, + .drMin = 6, + .drMax = 6, + .joinRequestDataRate = 6 } + }, + .rx1Span = { + .numChannels = 8, + .freqStart = 923.300, + .freqStep = 0.600, + .drMin = 8, + .drMax = 13, + .joinRequestDataRate = RADIOLIB_LORAWAN_DATA_RATE_UNUSED + }, + .rx1DataRateBase = 8, + .rx2 = { .enabled = true, .idx = 0, .freq = 923.300, .drMin = 8, .drMax = 8 }, + .dataRates = { + RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_11 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_11 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED } }; const LoRaWANBand_t CN500 = { - .downlinkDataRateBase = 0, - .downlinkDataRateMin = 0, - .payloadLenMax = { - 59, 59, 59, 123, 230, 230, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0 }, + .payloadLenMax = { 59, 59, 59, 123, 230, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, .powerMax = 19, .powerNumSteps = 7, .cfListType = RADIOLIB_LORAWAN_CFLIST_TYPE_MASK, - .fskFreq = 0, - .numChannelSpans = 2, - .defaultChannels = { + .txFreqs = { + RADIOLIB_LORAWAN_CHANNEL_NONE, + RADIOLIB_LORAWAN_CHANNEL_NONE, + RADIOLIB_LORAWAN_CHANNEL_NONE + }, + .txJoinReq = { + RADIOLIB_LORAWAN_CHANNEL_NONE, + RADIOLIB_LORAWAN_CHANNEL_NONE, + RADIOLIB_LORAWAN_CHANNEL_NONE + }, + .numTxSpans = 1, + .txSpans = { { - .direction = RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK, - .joinRequestDataRate = RADIOLIB_LORAWAN_DATA_RATE_UNUSED, .numChannels = 96, - .freqStart = 470.3, - .freqStep = 0.2, - .dataRates = { - RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_11 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - } - }, { - .direction = RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, - .joinRequestDataRate = RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - .numChannels = 48, - .freqStart = 500.3, - .freqStep = 0.2, - .dataRates = { - RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_11 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - } + .freqStart = 470.300, + .freqStep = 0.200, + .drMin = 0, + .drMax = 5, + .joinRequestDataRate = 0 }, - RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE, + RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE }, - .backupChannel = { - .direction = RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, - .joinRequestDataRate = RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - .numChannels = 1, - .freqStart = 505.3, - .freqStep = 0, - .dataRates = { - RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - } + .rx1Span = { + .numChannels = 48, + .freqStart = 500.300, + .freqStep = 0.200, + .drMin = 0, + .drMax = 5, + .joinRequestDataRate = RADIOLIB_LORAWAN_DATA_RATE_UNUSED + }, + .rx1DataRateBase = 0, + .rx2 = { .enabled = true, .idx = 0, .freq = 505.300, .drMin = 0, .drMax = 0 }, + .dataRates = { + RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_11 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED } }; const LoRaWANBand_t AS923 = { - .downlinkDataRateBase = 0, - .downlinkDataRateMin = 0, - .payloadLenMax = { - 59, 59, 59, 123, 230, 230, 230, 230, - 0, 0, 0, 0, 0, 0, 0, 0 }, + .payloadLenMax = { 59, 59, 59, 123, 230, 230, 230, 230, 0, 0, 0, 0, 0, 0, 0 }, .powerMax = 16, .powerNumSteps = 7, .cfListType = RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES, - .fskFreq = 921.8, - .numChannelSpans = 1, - .defaultChannels = { - { - .direction = RADIOLIB_LORAWAN_CHANNEL_DIR_BOTH, - .joinRequestDataRate = 2, - .numChannels = 2, - .freqStart = 923.2, - .freqStep = 0.2, - .dataRates = { - RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_11 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_250_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_FSK_50_K, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED - } - }, - RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE, + .txFreqs = { + { .enabled = true, .idx = 0, .freq = 923.200, .drMin = 0, .drMax = 5}, + { .enabled = true, .idx = 1, .freq = 923.400, .drMin = 0, .drMax = 5}, + RADIOLIB_LORAWAN_CHANNEL_NONE + }, + .txJoinReq = { + RADIOLIB_LORAWAN_CHANNEL_NONE, + RADIOLIB_LORAWAN_CHANNEL_NONE, + RADIOLIB_LORAWAN_CHANNEL_NONE + }, + .numTxSpans = 0, + .txSpans = { RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE, + RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE }, - .backupChannel = { - .direction = RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, - .joinRequestDataRate = RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - .numChannels = 1, - .freqStart = 923.2, - .freqStep = 0, - .dataRates = { - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - } + .rx1Span = RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE, + .rx1DataRateBase = 0, + .rx2 = { .enabled = true, .idx = 0, .freq = 923.200, .drMin = 2, .drMax = 2 }, + .dataRates = { + RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_11 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_250_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_FSK_50_K, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED } }; const LoRaWANBand_t KR920 = { - .downlinkDataRateBase = 0, - .downlinkDataRateMin = 0, - .payloadLenMax = { - 59, 59, 59, 123, 230, 230, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0 }, + .payloadLenMax = { 59, 59, 59, 123, 230, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, .powerMax = 14, .powerNumSteps = 7, .cfListType = RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES, - .fskFreq = 0, - .numChannelSpans = 1, - .defaultChannels = { - { - .direction = RADIOLIB_LORAWAN_CHANNEL_DIR_BOTH, - .joinRequestDataRate = RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - .numChannels = 3, - .freqStart = 922.1, - .freqStep = 0.2, - .dataRates = { - RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_11 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_250_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_FSK_50_K, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED - } - }, - RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE, + .txFreqs = { + { .enabled = true, .idx = 0, .freq = 922.100, .drMin = 0, .drMax = 5}, + { .enabled = true, .idx = 1, .freq = 922.300, .drMin = 0, .drMax = 5}, + { .enabled = true, .idx = 2, .freq = 922.500, .drMin = 0, .drMax = 5} + }, + .txJoinReq = { + RADIOLIB_LORAWAN_CHANNEL_NONE, + RADIOLIB_LORAWAN_CHANNEL_NONE, + RADIOLIB_LORAWAN_CHANNEL_NONE + }, + .numTxSpans = 0, + .txSpans = { RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE, + RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE }, - .backupChannel = { - .direction = RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, - .joinRequestDataRate = RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - .numChannels = 1, - .freqStart = 921.9, - .freqStep = 0, - .dataRates = { - RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - } + .rx1Span = RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE, + .rx1DataRateBase = 0, + .rx2 = { .enabled = true, .idx = 0, .freq = 921.900, .drMin = 0, .drMax = 0 }, + .dataRates = { + RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_11 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED } }; const LoRaWANBand_t IN865 = { - .downlinkDataRateBase = 0, - .downlinkDataRateMin = 0, - .payloadLenMax = { - 59, 59, 59, 123, 230, 230, 230, 230, - 0, 0, 0, 0, 0, 0, 0, 0 }, + .payloadLenMax = { 59, 59, 59, 123, 230, 230, 230, 230, 0, 0, 0, 0, 0, 0, 0 }, .powerMax = 30, .powerNumSteps = 10, .cfListType = RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES, - .fskFreq = 0, - .numChannelSpans = 1, - .defaultChannels = { - { - .direction = RADIOLIB_LORAWAN_CHANNEL_DIR_BOTH, - .joinRequestDataRate = RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - .numChannels = 3, - .freqStart = 865.0625, - .freqStep = 0.36, - .dataRates = { - RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_11 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_FSK_50_K, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED - } - }, - RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE, + .txFreqs = { + { .enabled = true, .idx = 0, .freq = 865.0625, .drMin = 0, .drMax = 5}, + { .enabled = true, .idx = 1, .freq = 865.4025, .drMin = 0, .drMax = 5}, + { .enabled = true, .idx = 2, .freq = 865.9850, .drMin = 0, .drMax = 5} + }, + .txJoinReq = { + RADIOLIB_LORAWAN_CHANNEL_NONE, + RADIOLIB_LORAWAN_CHANNEL_NONE, + RADIOLIB_LORAWAN_CHANNEL_NONE + }, + .numTxSpans = 0, + .txSpans = { RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE, + RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE }, - .backupChannel = { - .direction = RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, - .joinRequestDataRate = RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - .numChannels = 1, - .freqStart = 866.55, - .freqStep = 0, - .dataRates = { - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - RADIOLIB_LORAWAN_DATA_RATE_UNUSED, - } + .rx1Span = RADIOLIB_LORAWAN_CHANNEL_SPAN_NONE, + .rx1DataRateBase = 0, + .rx2 = { .enabled = true, .idx = 0, .freq = 866.550, .drMin = 2, .drMax = 2 }, + .dataRates = { + RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_11 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_FSK_50_K, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED, + RADIOLIB_LORAWAN_DATA_RATE_UNUSED } }; From c8bc1570905b382502d36ee77a22d0febca47930 Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Mon, 23 Oct 2023 23:14:17 +0200 Subject: [PATCH 02/21] [LoRaWAN] Change Rx windows from CAD to RxSingle --- src/modules/SX126x/SX126x.cpp | 30 +++ src/modules/SX126x/SX126x.h | 14 +- src/protocols/LoRaWAN/LoRaWAN.cpp | 172 +++++++----------- src/protocols/PhysicalLayer/PhysicalLayer.cpp | 8 + src/protocols/PhysicalLayer/PhysicalLayer.h | 10 + 5 files changed, 121 insertions(+), 113 deletions(-) diff --git a/src/modules/SX126x/SX126x.cpp b/src/modules/SX126x/SX126x.cpp index 7f739cf80..77412c5cf 100644 --- a/src/modules/SX126x/SX126x.cpp +++ b/src/modules/SX126x/SX126x.cpp @@ -1436,6 +1436,36 @@ uint32_t SX126x::getTimeOnAir(size_t len) { } } +// Table below defines the size of one symbol as +// symtime = 256us * 2^T(sf,bw) +// 256us is called one symunit. +// SF: +// BW: |__7___8___9__10__11__12 +// 125kHz | 2 3 4 5 6 7 +// 250kHz | 1 2 3 4 5 6 +// 500kHz | 0 1 2 3 4 5 +// +uint32_t SX126x::calculateRxTimeout(uint8_t numSymbols, DataRate_t *datarate, uint32_t offsetUs, uint32_t& timeoutUs) { + uint8_t exponent = 0; + exponent += datarate->lora.spreadingFactor - 7; + + switch((int)datarate->lora.bandwidth) { + case 125: + exponent += 2; + break; + case 250: + exponent += 1; + break; + default: // includes 500 kHz + exponent += 0; + break; + } + + timeoutUs = (uint32_t)numSymbols * 256 * (0x01 << exponent) + offsetUs; + uint32_t timeout = timeoutUs / 15.625; + return(timeout); +} + int16_t SX126x::implicitHeader(size_t len) { return(setHeaderType(RADIOLIB_SX126X_LORA_HEADER_IMPLICIT, len)); } diff --git a/src/modules/SX126x/SX126x.h b/src/modules/SX126x/SX126x.h index 5b18b969e..1dcf04c3e 100644 --- a/src/modules/SX126x/SX126x.h +++ b/src/modules/SX126x/SX126x.h @@ -56,7 +56,7 @@ #define RADIOLIB_SX126X_CMD_SET_PACKET_PARAMS 0x8C #define RADIOLIB_SX126X_CMD_SET_CAD_PARAMS 0x88 #define RADIOLIB_SX126X_CMD_SET_BUFFER_BASE_ADDRESS 0x8F -#define RADIOLIB_SX126X_CMD_SET_LORA_SYMB_NUM_TIMEOUT 0x0A +#define RADIOLIB_SX126X_CMD_SET_LORA_SYMB_NUM_TIMEOUT 0xA0 // status commands #define RADIOLIB_SX126X_CMD_GET_STATUS 0xC0 @@ -219,7 +219,7 @@ #define RADIOLIB_SX126X_IRQ_HEADER_ERR 0b0000000000100000 // 5 5 LoRa header CRC error #define RADIOLIB_SX126X_IRQ_HEADER_VALID 0b0000000000010000 // 4 4 valid LoRa header received #define RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID 0b0000000000001000 // 3 3 valid sync word detected -#define RADIOLIB_SX126X_IRQ_RADIOLIB_PREAMBLE_DETECTED 0b0000000000000100 // 2 2 preamble detected +#define RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED 0b0000000000000100 // 2 2 preamble detected #define RADIOLIB_SX126X_IRQ_RX_DONE 0b0000000000000010 // 1 1 packet received #define RADIOLIB_SX126X_IRQ_TX_DONE 0b0000000000000001 // 0 0 packet transmission completed #define RADIOLIB_SX126X_IRQ_RX_DEFAULT 0b0000001001100010 // 14 0 default for Rx (RX_DONE, TIMEOUT, CRC_ERR and HEADER_ERR) @@ -949,6 +949,16 @@ class SX126x: public PhysicalLayer { */ uint32_t getTimeOnAir(size_t len) override; + /*! + \brief Get the Rx timeout required to listen to a preamble of a certain number of symbols + \param numSymbols Number of symbols to listen for + \param datarate The datarate for which to calculate the timeout + \param offsetUs Additional offset in microseconds to allow increasing the timeout + \param timeoutUs Returns the timeout in microseconds for the host to sleep + \returns Timeout value in a unit that is specific for the used module + */ + uint32_t calculateRxTimeout(uint8_t numSymbols, DataRate_t *datarate, uint32_t offsetUs, uint32_t& timeoutUss); + /*! \brief Set implicit header mode for future reception/transmission. \param len Payload length in bytes. diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index db3b543ec..19aaf8980 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -20,14 +20,14 @@ static void LoRaWANNodeOnDownlink(void) { } // flag to indicate whether channel scan operation is complete -static volatile bool scanFlag = false; +static volatile bool downlinkTimeout = false; // interrupt service routine to handle downlinks automatically #if defined(ESP8266) || defined(ESP32) IRAM_ATTR #endif static void LoRaWANNodeOnChannelScan(void) { - scanFlag = true; + downlinkTimeout = true; } LoRaWANNode::LoRaWANNode(PhysicalLayer* phy, const LoRaWANBand_t* band) { @@ -629,7 +629,7 @@ int16_t LoRaWANNode::downlink(String& str) { int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { // check if there are any upcoming Rx windows Module* mod = this->phyLayer->getMod(); - const uint32_t scanGuard = 10; + const uint32_t scanGuard = 50; 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 @@ -646,18 +646,22 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { RADIOLIB_ASSERT(state); } - // calculate the channel scanning timeout - // according to the spec, this must be at least enough time to effectively detect a preamble - uint32_t scanTimeout = this->phyLayer->getTimeOnAir(0)/1000; - - // set up everything for channel scan + downlinkTimeout = false; downlinkReceived = false; - scanFlag = false; - bool packetDetected = false; this->phyLayer->setChannelScanAction(LoRaWANNodeOnChannelScan); + // calculate the Rx timeout + // 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 + DataRate_t dataRate; + findDataRate(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK], &dataRate); + uint32_t timeoutHost = 0; + uint32_t timeoutMod = this->phyLayer->calculateRxTimeout(8, &dataRate, 2*scanGuard*1000, timeoutHost); + RADIOLIB_DEBUG_PRINTLN("RxTimeout host: %d, module: %06x", timeoutHost, timeoutMod); + // perform listening in the two Rx windows for(uint8_t i = 0; i < 2; i++) { + // wait for the start of the Rx window // the waiting duration is shortened a bit to cover any possible timing errors uint32_t waitLen = this->rxDelays[i] - (mod->hal->millis() - this->rxDelayStart); @@ -668,49 +672,27 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { mod->hal->delay(waitLen); RADIOLIB_DEBUG_PRINTLN("Opening Rx%d window...", (i+1)); - // wait until we get a preamble - uint32_t scanStart = mod->hal->millis(); - while((mod->hal->millis() - scanStart) < (scanTimeout + scanGuard)) { - // check channel detection timeout - state = this->phyLayer->startChannelScan(); - RADIOLIB_ASSERT(state); - - // wait with some timeout, though it should not be hit - uint32_t cadStart = mod->hal->millis(); - while(!scanFlag) { - mod->hal->yield(); - if(mod->hal->millis() - cadStart >= 3000) { - // timed out, stop waiting - break; - } - } - - // check the scan result - scanFlag = false; - state = this->phyLayer->getChannelScanResult(); - if((state == RADIOLIB_PREAMBLE_DETECTED) || (state == RADIOLIB_LORA_DETECTED)) { - packetDetected = true; - - // channel scan is finished, swap the actions - this->phyLayer->clearChannelScanAction(); - this->phyLayer->setPacketReceivedAction(LoRaWANNodeOnDownlink); - - // start receiving - state = this->phyLayer->startReceive(0x00, 0b0000001001100010, 0b0000000000000010, 0); // RxSingle - RADIOLIB_ASSERT(state); - - break; - } - + // TODO somehow make this module-independent, currently configured for SX126x + uint16_t irqFlags = 0b0000000000000010; // RxDone + uint16_t irqMask = 0b0000000000000010; + irqFlags |= 0b0000001000000000; // RxTimeout + irqMask |= 0b0000001000000000; + + uint32_t rxStart = mod->hal->millis(); + this->phyLayer->startReceive(timeoutMod, irqFlags, irqMask, 0); + RADIOLIB_DEBUG_PRINT("Opened Rx%d window (%d us timeout)... ", i+1, timeoutHost); + while(!downlinkTimeout && ((mod->hal->millis() - rxStart) < (timeoutHost / 1000))) { + mod->hal->yield(); } + RADIOLIB_DEBUG_PRINTLN("closing"); - // check if we have a packet - if(packetDetected) { - RADIOLIB_DEBUG_PRINTLN("Detected a packet..."); + // check if we detected something of a packet + if(!downlinkTimeout) { break; } else if(i == 0) { // nothing in the first window, configure for the second + this->phyLayer->standby(); state = this->phyLayer->setFrequency(this->rx2.freq); RADIOLIB_ASSERT(state); @@ -718,12 +700,15 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { findDataRate(this->rx2.drMax, &dataRate); state = this->phyLayer->setDataRate(dataRate); RADIOLIB_ASSERT(state); + timeoutMod = this->phyLayer->calculateRxTimeout(8, &dataRate, 2*scanGuard*1000, timeoutHost); + downlinkTimeout = false; } } + this->phyLayer->clearChannelScanAction(); // check if we received a packet at all - if(!packetDetected) { + if(downlinkTimeout) { this->phyLayer->standby(); if(!this->FSK) { this->phyLayer->invertIQ(false); @@ -731,27 +716,14 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { return(RADIOLIB_ERR_RX_TIMEOUT); } + this->phyLayer->setPacketReceivedAction(LoRaWANNodeOnDownlink); - // wait for reception with some timeout - uint32_t rxStart = mod->hal->millis(); while(!downlinkReceived) { mod->hal->yield(); - // 3 seconds is maximum airtime - if(mod->hal->millis() - rxStart >= 3000) { - // timed out - RADIOLIB_DEBUG_PRINTLN("Packet length: %d", this->phyLayer->getPacketLength()); - this->phyLayer->standby(); - if(!this->FSK) { - this->phyLayer->invertIQ(false); - } - // return(RADIOLIB_ERR_RX_TIMEOUT); - break; - } } // we have a message, clear actions, go to standby and reset the IQ inversion - downlinkReceived = false; - this->phyLayer->standby(); + this->phyLayer->standby(); // TODO check: this should be done automagically due to RxSingle? this->phyLayer->clearPacketReceivedAction(); if(!this->FSK) { state = this->phyLayer->invertIQ(false); @@ -760,6 +732,7 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { // get the packet length size_t downlinkMsgLen = this->phyLayer->getPacketLength(); + RADIOLIB_DEBUG_PRINTLN("Downlink message length: %d", downlinkMsgLen); // check the minimum required frame length // an extra byte is subtracted because downlink frames may not have a port @@ -803,7 +776,6 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { // TODO cache the ADR bit? uint16_t fcnt16 = LoRaWANNode::ntoh(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FCNT_POS]); LoRaWANNode::hton(&downlinkMsg[RADIOLIB_LORAWAN_BLOCK_FCNT_POS], fcnt16); - uint32_t fcnt32 = fcnt16; // calculate possible rollover once decided if this is network downlink or application downlink RADIOLIB_DEBUG_PRINTLN("downlinkMsg:"); RADIOLIB_DEBUG_HEXDUMP(downlinkMsg, RADIOLIB_AES128_BLOCK_SIZE + downlinkMsgLen); @@ -825,9 +797,12 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { fcntDownPrev = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_N_FCNT_DOWN_ID); } + RADIOLIB_DEBUG_PRINTLN("fcnt: %d, fcntPrev: %d, isAppDownlink: %d", fcnt16, fcntDownPrev, (int)isAppDownlink); + // if this is not the first downlink... // assume a 16-bit to 32-bit rollover if difference between counters in LSB is smaller than MAX_FCNT_GAP // if that isn't the case and the received fcnt is smaller or equal to the last heard fcnt, then error + uint32_t fcnt32 = fcnt16; if(fcntDownPrev > 0) { if((fcnt16 <= fcntDownPrev) && ((0xFFFF - (uint16_t)fcntDownPrev + fcnt16) > RADIOLIB_LORAWAN_MAX_FCNT_GAP)) { #if !defined(RADIOLIB_STATIC_ONLY) @@ -839,7 +814,7 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { return(RADIOLIB_ERR_N_FCNT_DOWN_INVALID); } } else if (fcnt16 <= fcntDownPrev) { - uint16_t msb = (fcntDownPrev >> 16) + 1; // assume a rollover + uint16_t msb = (fcntDownPrev >> 16) + 1; // assume a rollover fcnt32 |= (msb << 16); // add back the MSB part } } @@ -923,7 +898,7 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { RADIOLIB_DEBUG_PRINTLN("Uplink MAC payload (%d commands):", foptsNum); RADIOLIB_DEBUG_HEXDUMP(foptsBuff, foptsBufSize); - isMACPayload = true; + this->isMACPayload = true; this->uplink(foptsBuff, foptsBufSize, RADIOLIB_LORAWAN_FPORT_MAC_COMMAND); } @@ -951,9 +926,9 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { // there is payload, and so there should be a port too // TODO pass the port? *len = payLen - 1; + // TODO it COULD be the case that the assumed rollover is incorrect, then figure out a way to catch this and retry with just fcnt16 - // TODO does the erratum hold here as well? - processAES(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], downlinkMsgLen, this->appSKey, data, fcnt32, RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, 0x00, true); + processAES(&downlinkMsg[RADIOLIB_LORAWAN_FRAME_PAYLOAD_POS(foptsLen)], payLen - 1, this->appSKey, data, fcnt32, RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, 0x00, true); #if !defined(RADIOLIB_STATIC_ONLY) delete[] downlinkMsg; @@ -1061,20 +1036,20 @@ int16_t LoRaWANNode::setupChannels(uint8_t* cfList) { num++; } } - // if(cfList != nullptr) { - // for(uint8_t i = 0; i < 5; i++) { - // 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 - // availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num] = chnl; - // availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][num] = chnl; - // RADIOLIB_DEBUG_PRINTLN("Channel UL/DL %d frequency = %f MHz", num, availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].freq); - // num++; - // } - // } + if(cfList != nullptr) { + for(uint8_t i = 0; i < 5; i++) { + 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 + availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num] = chnl; + availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][num] = chnl; + RADIOLIB_DEBUG_PRINTLN("Channel UL/DL %d frequency = %f MHz", num, availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].freq); + num++; + } + } } else { // RADIOLIB_LORAWAN_CFLIST_TYPE_MASK uint8_t chSpan = 0; uint8_t chNum = 0; @@ -1301,15 +1276,6 @@ int16_t LoRaWANNode::pushMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQ } memcpy(&queue->commands[queue->numCommands], cmd, sizeof(LoRaWANMacCommand_t)); - /*RADIOLIB_DEBUG_PRINTLN("push MAC CID = %02x, len = %d, payload = %02x %02x %02x %02x %02x, repeat = %d ", - queue->commands[queue->numCommands - 1].cid, - queue->commands[queue->numCommands - 1].len, - queue->commands[queue->numCommands - 1].payload[0], - queue->commands[queue->numCommands - 1].payload[1], - queue->commands[queue->numCommands - 1].payload[2], - queue->commands[queue->numCommands - 1].payload[3], - queue->commands[queue->numCommands - 1].payload[4], - queue->commands[queue->numCommands - 1].repeat);*/ queue->numCommands++; queue->len += 1 + cmd->len; // 1 byte for command ID, len bytes for payload @@ -1322,15 +1288,6 @@ int16_t LoRaWANNode::popMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQu } if(cmd) { - // RADIOLIB_DEBUG_PRINTLN("pop MAC CID = %02x, len = %d, payload = %02x %02x %02x %02x %02x, repeat = %d ", - // queue->commands[index].cid, - // queue->commands[index].len, - // queue->commands[index].payload[0], - // queue->commands[index].payload[1], - // queue->commands[index].payload[2], - // queue->commands[index].payload[3], - // queue->commands[index].payload[4], - // queue->commands[index].repeat); memcpy(cmd, &queue->commands[index], sizeof(LoRaWANMacCommand_t)); } @@ -1350,15 +1307,6 @@ int16_t LoRaWANNode::deleteMacCommand(uint8_t cid, LoRaWANMacCommandQueue_t* que for(size_t index = 0; index < queue->numCommands; index++) { if(queue->commands[index].cid == cid) { - // RADIOLIB_DEBUG_PRINTLN("delete MAC CID = %02x, len = %d, payload = %02x %02x %02x %02x %02x, repeat = %d ", - // queue->commands[index].cid, - // queue->commands[index].len, - // queue->commands[index].payload[0], - // queue->commands[index].payload[1], - // queue->commands[index].payload[2], - // queue->commands[index].payload[3], - // queue->commands[index].payload[4], - // queue->commands[index].repeat); 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) { @@ -1412,7 +1360,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%04x, chMaskCntl = %02x, nbTrans = %d", dr, txPower, chMask, chMaskCntl, nbTrans); + RADIOLIB_DEBUG_PRINTLN("ADR REQ: dataRate = %d, txPower = %d, chMask = 0x%02x, chMaskCntl = %02x, nbTrans = %d", dr, txPower, chMask, chMaskCntl, nbTrans); // apply the configuration uint8_t drAck = 0; @@ -1447,17 +1395,18 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { this->nbTrans = nbTrans; // TODO implement channel mask uint8_t chMaskAck = 1; - (void)chMask; (void)chMaskCntl; if(this->band->cfListType == RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES) { for(uint8_t i = 0; i < 16; i++) { // check if this channel ID should be enabled + RADIOLIB_DEBUG_PRINTLN("ADR channel %d: %d --> %d", i, 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].enabled == false) { chMaskAck = 0; break; } + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = true; } else { this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = false; } @@ -1732,6 +1681,7 @@ void LoRaWANNode::processAES(uint8_t* in, size_t len, uint8_t* key, uint8_t* out // on downlink frames, this has a decryption effect because server actually "decrypts" the plaintext size_t remLen = len; for(size_t i = 0; i < numBlocks; i++) { + if(counter) { encBlock[RADIOLIB_LORAWAN_ENC_BLOCK_COUNTER_POS] = i + 1; } diff --git a/src/protocols/PhysicalLayer/PhysicalLayer.cpp b/src/protocols/PhysicalLayer/PhysicalLayer.cpp index b244b8565..de31e5719 100644 --- a/src/protocols/PhysicalLayer/PhysicalLayer.cpp +++ b/src/protocols/PhysicalLayer/PhysicalLayer.cpp @@ -293,6 +293,14 @@ uint32_t PhysicalLayer::getTimeOnAir(size_t len) { (void)len; return(0); } + +uint32_t PhysicalLayer::calculateRxTimeout(uint8_t numSymbols, DataRate_t *datarate, uint32_t offsetUs, uint32_t& timeoutUs) { + (void)numSymbols; + (void)datarate; + (void)offsetUs; + (void)timeoutUs; + return(RADIOLIB_ERR_UNSUPPORTED); +} int16_t PhysicalLayer::startChannelScan() { return(RADIOLIB_ERR_UNSUPPORTED); diff --git a/src/protocols/PhysicalLayer/PhysicalLayer.h b/src/protocols/PhysicalLayer/PhysicalLayer.h index c083e4765..12ce0a7ed 100644 --- a/src/protocols/PhysicalLayer/PhysicalLayer.h +++ b/src/protocols/PhysicalLayer/PhysicalLayer.h @@ -309,6 +309,16 @@ class PhysicalLayer { \returns Expected time-on-air in microseconds. */ virtual uint32_t getTimeOnAir(size_t len); + + /*! + \brief Get the Rx timeout required to listen to a preamble of a certain number of symbols + \param numSymbols Number of symbols to listen for + \param datarate The datarate for which to calculate the timeout + \param offsetUs Additional offset in microseconds to allow increasing the timeout + \param timeoutUs Returns the timeout in microseconds for the host to sleep + \returns Timeout value in a unit that is specific for the used module + */ + virtual uint32_t calculateRxTimeout(uint8_t numSymbols, DataRate_t* datarate, uint32_t offsetUs, uint32_t& timeoutUs); /*! \brief Interrupt-driven channel activity detection method. interrupt will be activated From 7c3670c430dd2eb0fdc240f82adac3cc742e0e84 Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Fri, 27 Oct 2023 01:18:53 +0200 Subject: [PATCH 03/21] [LoRaWAN] improve persistence, better Rx windows, wear leveling, confirmed frames --- src/Hal.cpp | 10 +- src/Hal.h | 7 +- src/modules/SX126x/SX126x.cpp | 43 +- src/modules/SX126x/SX126x.h | 12 + src/protocols/LoRaWAN/LoRaWAN.cpp | 611 ++++++++++++------ src/protocols/LoRaWAN/LoRaWAN.h | 59 +- src/protocols/PhysicalLayer/PhysicalLayer.cpp | 11 +- src/protocols/PhysicalLayer/PhysicalLayer.h | 12 + 8 files changed, 535 insertions(+), 230 deletions(-) diff --git a/src/Hal.cpp b/src/Hal.cpp index 6ba08bb75..1b4e818e2 100644 --- a/src/Hal.cpp +++ b/src/Hal.cpp @@ -60,14 +60,14 @@ uint32_t RadioLibHal::getPersistentAddr(uint32_t id) { } template -void RadioLibHal::setPersistentParameter(uint32_t id, T val) { +void RadioLibHal::setPersistentParameter(uint32_t id, T val, uint32_t offset) { uint8_t *ptr = (uint8_t*)&val; - this->writePersistentStorage(RADIOLIB_HAL_PERSISTENT_STORAGE_BASE + RadioLibPersistentParamTable[id], ptr, sizeof(T)); + this->writePersistentStorage(RADIOLIB_HAL_PERSISTENT_STORAGE_BASE + RadioLibPersistentParamTable[id] + offset, ptr, sizeof(T)); } -template void RadioLibHal::setPersistentParameter(uint32_t id, uint8_t val); -template void RadioLibHal::setPersistentParameter(uint32_t id, uint16_t val); -template void RadioLibHal::setPersistentParameter(uint32_t id, uint32_t val); +template void RadioLibHal::setPersistentParameter(uint32_t id, uint8_t val, uint32_t offset); +template void RadioLibHal::setPersistentParameter(uint32_t id, uint16_t val, uint32_t offset); +template void RadioLibHal::setPersistentParameter(uint32_t id, uint32_t val, uint32_t offset); template T RadioLibHal::getPersistentParameter(uint32_t id) { diff --git a/src/Hal.h b/src/Hal.h index 415e53fb9..34930938e 100644 --- a/src/Hal.h +++ b/src/Hal.h @@ -11,7 +11,7 @@ #define RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID (1) #define RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION (2) #define RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXDR_RX2DR_ID (3) -#define RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXPWR_CUR_MAX_ID (4) +#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) @@ -38,7 +38,7 @@ static const uint32_t RadioLibPersistentParamTable[] = { 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_MAX_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 @@ -299,9 +299,10 @@ class RadioLibHal { will be stored in the system endian! \param id Parameter ID to save at. \param val Value to set. + \param offset An additional offset added to the address. */ template - void setPersistentParameter(uint32_t id, T val); + void setPersistentParameter(uint32_t id, T val, uint32_t offset = 0); /*! \brief Method to get arbitrary parameter from persistent storage. diff --git a/src/modules/SX126x/SX126x.cpp b/src/modules/SX126x/SX126x.cpp index 77412c5cf..4080d2ffa 100644 --- a/src/modules/SX126x/SX126x.cpp +++ b/src/modules/SX126x/SX126x.cpp @@ -1436,36 +1436,29 @@ uint32_t SX126x::getTimeOnAir(size_t len) { } } -// Table below defines the size of one symbol as -// symtime = 256us * 2^T(sf,bw) -// 256us is called one symunit. -// SF: -// BW: |__7___8___9__10__11__12 -// 125kHz | 2 3 4 5 6 7 -// 250kHz | 1 2 3 4 5 6 -// 500kHz | 0 1 2 3 4 5 -// uint32_t SX126x::calculateRxTimeout(uint8_t numSymbols, DataRate_t *datarate, uint32_t offsetUs, uint32_t& timeoutUs) { - uint8_t exponent = 0; - exponent += datarate->lora.spreadingFactor - 7; - - switch((int)datarate->lora.bandwidth) { - case 125: - exponent += 2; - break; - case 250: - exponent += 1; - break; - default: // includes 500 kHz - exponent += 0; - break; - } - - timeoutUs = (uint32_t)numSymbols * 256 * (0x01 << exponent) + offsetUs; + uint32_t symbolTime = (0x00000001 << datarate->lora.spreadingFactor) * 1000 / datarate->lora.bandwidth; + timeoutUs = (uint32_t)numSymbols * symbolTime + offsetUs; uint32_t timeout = timeoutUs / 15.625; return(timeout); } +bool SX126x::isRxTimeout() { + uint16_t irq = getIrqStatus(); + bool isRxTimeout = false; + if(irq & RADIOLIB_SX126X_IRQ_TIMEOUT) { + isRxTimeout = true; + } + return(isRxTimeout); +} + +uint16_t SX126x::readIrq(bool clear) { + uint16_t irq = getIrqStatus(); + if(clear) + clearIrqStatus(); + return(irq); +} + int16_t SX126x::implicitHeader(size_t len) { return(setHeaderType(RADIOLIB_SX126X_LORA_HEADER_IMPLICIT, len)); } diff --git a/src/modules/SX126x/SX126x.h b/src/modules/SX126x/SX126x.h index 1dcf04c3e..3e4c69df2 100644 --- a/src/modules/SX126x/SX126x.h +++ b/src/modules/SX126x/SX126x.h @@ -959,6 +959,18 @@ class SX126x: public PhysicalLayer { */ uint32_t calculateRxTimeout(uint8_t numSymbols, DataRate_t *datarate, uint32_t offsetUs, uint32_t& timeoutUss); + /*! + \brief Check whether there is a RxTimeout flag set + \returns RxTimeout flag is set + */ + bool isRxTimeout(); + + /*! + \brief Check whether there is a RxTimeout flag set + \returns RxTimeout flag is set + */ + uint16_t readIrq(bool clear = false); + /*! \brief Set implicit header mode for future reception/transmission. \param len Payload length in bytes. diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index 19aaf8980..3b522153b 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -39,6 +39,7 @@ LoRaWANNode::LoRaWANNode(PhysicalLayer* phy, const LoRaWANBand_t* band) { this->rx2 = this->band->rx2; } +#if !defined(RADIOLIB_EEPROM_UNSUPPORTED) void LoRaWANNode::wipe() { Module* mod = this->phyLayer->getMod(); mod->hal->wipePersistentStorage(); @@ -65,27 +66,47 @@ int16_t LoRaWANNode::restore() { 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); + // get session parameters this->rev = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION); 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); - uint8_t txPwrCurMax = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXPWR_CUR_MAX_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; + 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->rx2.drMax = (txDrRx2Dr & 0x0F) >> 0; - this->rx1DrOffset = (rx1DrOffDel & 0xF0) >> 4; - this->rxDelays[0] = ((rx1DrOffDel & 0x0F) >> 0) * 1000; - if(this->rxDelays[0] == 0) { - this->rxDelays[0] = RADIOLIB_LORAWAN_RECEIVE_DELAY_1_MS; - } - this->rxDelays[1] = this->rxDelays[0] + 1000; - this->rx2.freq = (float)rx2Freq / 10000.0; - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = (txDrRx2Dr & 0xF0) >> 4; - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] + this->band->rx1DataRateBase + this->rx1DrOffset; + 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 + this->restoreFcntUp(); uint8_t queueBuff[sizeof(LoRaWANMacCommandQueue_t)] = { 0 }; mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FOPTS_ID), queueBuff, sizeof(LoRaWANMacCommandQueue_t)); @@ -101,30 +122,95 @@ int16_t LoRaWANNode::restore() { return(RADIOLIB_ERR_NONE); } +int16_t LoRaWANNode::restoreFcntUp() { + 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); + RADIOLIB_DEBUG_HEXDUMP(fcntBuff, 30); + + // copy the two most significant bytes from the first two bytes + uint8_t bits_30_22 = fcntBuff[0]; + uint8_t bits_22_14 = fcntBuff[1]; + + // the next 7 bits must be retrieved from the byte to which was written most recently + // this is the last byte that has its state bit (most significant bit) set equal to its predecessor + // we find the first byte that has its state bit different, and subtract one + uint8_t idx = 2; + uint8_t state = fcntBuff[idx] >> 7; + for(; idx < 5; idx++) { + if(fcntBuff[idx] >> 7 != state) { + break; + } + } + uint8_t bits_14_7 = fcntBuff[idx-1] & 0x7F; + + // equally, the last 7 bits must be retrieved from the byte to which was written most recently + // this is the last byte that has its state bit (most significant bit) set equal to its predecessor + // we find the first byte that has its state bit different, and subtract one + idx = 5; + state = fcntBuff[idx] >> 7; + for(; idx < 30; idx++) { + if(fcntBuff[idx] >> 7 != state) { + break; + } + } + uint8_t bits_7_0 = fcntBuff[idx-1] & 0x7F; + + this->fcntUp = (bits_30_22 << 22) | (bits_22_14 << 14) | (bits_14_7 << 7) | bits_7_0; + + return(RADIOLIB_ERR_NONE); +} + +int16_t LoRaWANNode::restoreChannels() { + uint8_t bytesPerChannel = 5; + uint8_t numBytes = 2 * RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS * bytesPerChannel; + uint8_t buffer[numBytes]; + 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[5] = { 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; + } + } + return(RADIOLIB_ERR_NONE); +} +#endif + int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKey, uint8_t* appKey, 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)) { // the device has joined already, we can just pull the data from persistent storage return(this->restore()); } +#endif // set the physical layer configuration + this->txPwrCur = this->band->powerMax; int16_t state = this->setPhyProperties(); RADIOLIB_ASSERT(state); - // get dev nonce from persistent storage and increment it - uint16_t devNonce = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_NONCE_ID); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_NONCE_ID, devNonce + 1); - // setup uplink/downlink frequencies and datarates - state = this->selectChannelsJR(devNonce); + state = this->selectChannelsJR(this->devNonce); RADIOLIB_ASSERT(state); // configure for uplink with default configuration state = this->configureChannel(RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK); RADIOLIB_ASSERT(state); + // increment devNonce as we are sending another join-request + this->devNonce += 1; + // build the join-request message uint8_t joinRequestMsg[RADIOLIB_LORAWAN_JOIN_REQUEST_LEN]; @@ -132,7 +218,7 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe joinRequestMsg[0] = RADIOLIB_LORAWAN_MHDR_MTYPE_JOIN_REQUEST | RADIOLIB_LORAWAN_MHDR_MAJOR_R1; LoRaWANNode::hton(&joinRequestMsg[RADIOLIB_LORAWAN_JOIN_REQUEST_JOIN_EUI_POS], joinEUI); LoRaWANNode::hton(&joinRequestMsg[RADIOLIB_LORAWAN_JOIN_REQUEST_DEV_EUI_POS], devEUI); - LoRaWANNode::hton(&joinRequestMsg[RADIOLIB_LORAWAN_JOIN_REQUEST_DEV_NONCE_POS], devNonce); + LoRaWANNode::hton(&joinRequestMsg[RADIOLIB_LORAWAN_JOIN_REQUEST_DEV_NONCE_POS], this->devNonce); // add the authentication code uint32_t mic = this->generateMIC(joinRequestMsg, RADIOLIB_LORAWAN_JOIN_REQUEST_LEN - sizeof(uint32_t), nwkKey); @@ -141,7 +227,7 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe // send it state = this->phyLayer->transmit(joinRequestMsg, RADIOLIB_LORAWAN_JOIN_REQUEST_LEN); RADIOLIB_ASSERT(state); - + // configure for downlink with default configuration state = this->configureChannel(RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK); RADIOLIB_ASSERT(state); @@ -159,7 +245,7 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe // start receiving uint32_t start = mod->hal->millis(); downlinkReceived = false; - state = this->phyLayer->startReceive(0x00, 0b0000001001100010, 0b0000000000000010, 0); + state = this->phyLayer->startReceive(); RADIOLIB_ASSERT(state); // wait for the reply or timeout @@ -216,15 +302,15 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe RADIOLIB_DEBUG_PRINTLN("joinAcceptMsg:"); RADIOLIB_DEBUG_HEXDUMP(joinAcceptMsg, lenRx); - // get current JoinNonce from downlink and previous JoinNonce from NVM - uint32_t joinNonce = LoRaWANNode::ntoh(&joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_JOIN_NONCE_POS], 3); - uint32_t joinNoncePrev = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_JOIN_NONCE_ID); - RADIOLIB_DEBUG_PRINTLN("JoinNoncePrev: %d, JoinNonce: %d", joinNoncePrev, joinNonce); - - // JoinNonce received must be greater than the last JoinNonce heard, else error - if((joinNoncePrev > 0) && (joinNonce <= joinNoncePrev)) { + // get current JoinNonce from downlink and previous JoinNonce from persistent storage + uint32_t joinNonceNew = LoRaWANNode::ntoh(&joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_JOIN_NONCE_POS], 3); + + RADIOLIB_DEBUG_PRINTLN("JoinNoncePrev: %d, JoinNonce: %d", this->joinNonce, joinNonceNew); + // JoinNonce received must be greater than the last JoinNonce heard, else error + if((this->joinNonce > 0) && (joinNonceNew <= this->joinNonce)) { return(RADIOLIB_ERR_JOIN_NONCE_INVALID); } + this->joinNonce = joinNonceNew; // check LoRaWAN revision (the MIC verification depends on this) uint8_t dlSettings = joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_DL_SETTINGS_POS]; @@ -246,7 +332,7 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe uint8_t micBuff[3*RADIOLIB_AES128_BLOCK_SIZE] = { 0 }; micBuff[0] = RADIOLIB_LORAWAN_JOIN_REQUEST_TYPE; LoRaWANNode::hton(&micBuff[1], joinEUI); - LoRaWANNode::hton(&micBuff[9], devNonce); + LoRaWANNode::hton(&micBuff[9], this->devNonce); memcpy(&micBuff[11], joinAcceptMsg, lenRx); if(!verifyMIC(micBuff, lenRx + 11, this->jSIntKey)) { @@ -262,7 +348,7 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe } // parse other contents - uint32_t homeNetId = LoRaWANNode::ntoh(&joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_HOME_NET_ID_POS], 3); + 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) @@ -280,7 +366,6 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe } else { this->setupChannels(nullptr); } - this->saveChannels(); // prepare buffer for key derivation uint8_t keyDerivationBuff[RADIOLIB_AES128_BLOCK_SIZE] = { 0 }; @@ -290,7 +375,7 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe if(this->rev == 1) { // 1.1 version, derive the keys LoRaWANNode::hton(&keyDerivationBuff[RADIOLIB_LORAWAN_JOIN_ACCEPT_JOIN_EUI_POS], joinEUI); - LoRaWANNode::hton(&keyDerivationBuff[RADIOLIB_LORAWAN_JOIN_ACCEPT_DEV_NONCE_POS], devNonce); + LoRaWANNode::hton(&keyDerivationBuff[RADIOLIB_LORAWAN_JOIN_ACCEPT_DEV_NONCE_POS], this->devNonce); keyDerivationBuff[0] = RADIOLIB_LORAWAN_JOIN_ACCEPT_APP_S_KEY; RadioLibAES128Instance.init(appKey); @@ -320,8 +405,8 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe } else { // 1.0 version, just derive the keys - LoRaWANNode::hton(&keyDerivationBuff[RADIOLIB_LORAWAN_JOIN_ACCEPT_HOME_NET_ID_POS], homeNetId, 3); - LoRaWANNode::hton(&keyDerivationBuff[RADIOLIB_LORAWAN_JOIN_ACCEPT_DEV_ADDR_POS], devNonce); + LoRaWANNode::hton(&keyDerivationBuff[RADIOLIB_LORAWAN_JOIN_ACCEPT_HOME_NET_ID_POS], this->homeNetId, 3); + LoRaWANNode::hton(&keyDerivationBuff[RADIOLIB_LORAWAN_JOIN_ACCEPT_DEV_ADDR_POS], this->devNonce); keyDerivationBuff[0] = RADIOLIB_LORAWAN_JOIN_ACCEPT_APP_S_KEY; RadioLibAES128Instance.init(nwkKey); RadioLibAES128Instance.encryptECB(keyDerivationBuff, RADIOLIB_AES128_BLOCK_SIZE, this->appSKey); @@ -334,56 +419,49 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe memcpy(this->nwkSEncKey, this->fNwkSIntKey, RADIOLIB_AES128_KEY_SIZE); } - - // store session configuration - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION, this->rev); - uint8_t txDrRx2Dr = (this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] << 4) | this->rx2.drMax; - uint8_t txPwrCurMax; - uint8_t rx1DrOffDel = (this->rx1DrOffset << 4) | (this->rxDelays[0] / 1000); - uint32_t rx2Freq = uint32_t(this->rx2.freq * 10000); - uint8_t rx2FreqBuf[3]; - LoRaWANNode::hton(&rx2FreqBuf[0], rx2Freq, 3); - uint8_t adrLimDel = (RADIOLIB_LORAWAN_ADR_ACK_LIMIT_EXP << 4) | (RADIOLIB_LORAWAN_ADR_ACK_DELAY_EXP << 0); - uint8_t nbTrans = (0x01 << 0); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXDR_RX2DR_ID, txDrRx2Dr); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXPWR_CUR_MAX_ID, txPwrCurMax); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_RX1_DROFF_DEL_ID, rx1DrOffDel); - mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_RX2FREQ_ID), rx2FreqBuf, 3); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_LIM_DEL_ID, adrLimDel); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_NBTRANS_ID, nbTrans); - // save the device address & keys + // reset all frame counters + this->fcntUp = 0; + this->aFcntDown = 0; + this->nFcntDown = 0; + this->confFcntUp = RADIOLIB_LORAWAN_FCNT_NONE; + this->confFcntDown = RADIOLIB_LORAWAN_FCNT_NONE; + 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 uplink parameters - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_HOME_NET_ID, homeNetId); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_JOIN_NONCE_ID, joinNonce); - - // all complete, reset all frame counters and set the magic number - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_A_FCNT_DOWN_ID, 0); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_N_FCNT_DOWN_ID, 0); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_CONF_FCNT_UP_ID, 0); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_CONF_FCNT_DOWN_ID, 0); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_FCNT_ID, 0); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID, 0); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID, RADIOLIB_LORAWAN_MAGIC); + // 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); + + this->saveSession(); + this->saveChannels(); - // everything written to NVM, write current table version to NVM + // 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); +#endif + 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) { + +#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)) { // the device has joined already, we can just pull the data from persistent storage return(this->restore()); } +#endif + this->devAddr = addr; memcpy(this->appSKey, appSKey, RADIOLIB_AES128_KEY_SIZE); memcpy(this->nwkSEncKey, nwkSKey, RADIOLIB_AES128_KEY_SIZE); @@ -398,6 +476,7 @@ int16_t LoRaWANNode::beginABP(uint32_t addr, uint8_t* nwkSKey, uint8_t* appSKey, } // set the physical layer configuration + this->txPwrCur = this->band->powerMax; int16_t state = this->setPhyProperties(); RADIOLIB_ASSERT(state); @@ -405,24 +484,185 @@ int16_t LoRaWANNode::beginABP(uint32_t addr, uint8_t* nwkSKey, uint8_t* appSKey, state = this->setupChannels(nullptr); RADIOLIB_ASSERT(state); - // everything written to NVM, write current version to NVM +#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); + + 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); +#endif + + return(RADIOLIB_ERR_NONE); +} + +#if !defined(RADIOLIB_EEPROM_UNSUPPORTED) +int16_t LoRaWANNode::saveSession() { + Module* mod = this->phyLayer->getMod(); + + // store session configuration (MAC commands) + if(mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION) != this->rev) + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION, 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); + + 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_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_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_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_PERSISTENT_PARAM_LORAWAN_ADR_FCNT_ID) != this->adrFcnt) + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_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)); + 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)); + } return(RADIOLIB_ERR_NONE); } +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); + RADIOLIB_DEBUG_HEXDUMP(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); + 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); + + // 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 + // if all have an equal state, restart from the beginning + // always flip the state bit of the byte that we write to, to indicate that this is the most recently written byte + uint8_t idx = 2; + uint8_t state = fcntBuff[idx] >> 7; + for(; idx < 5; idx++) { + if(fcntBuff[idx] >> 7 != state) { + break; + } + } + idx = idx < 5 ? idx : 2; + uint8_t bits_14_7 = (this->fcntUp >> 7) & 0x7F; + + // 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); + + // equally, the last 7 bits are stored into one of many indices + // this index is indicated by the first byte that has its state (most significant bit) different from its predecessor + // if all have an equal state, restart from the beginning + // always flip the state bit of the byte that we write to, to indicate that this is the most recently written byte + idx = 5; + state = fcntBuff[idx] >> 7; + for(; idx < 30; idx++) { + if(fcntBuff[idx] >> 7 != state) { + break; + } + } + idx = idx < 30 ? idx : 5; + uint8_t bits_7_0 = (this->fcntUp >> 0) & 0x7F; + + // 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); + + return(RADIOLIB_ERR_NONE); +} + +int16_t LoRaWANNode::saveChannels() { + uint8_t bytesPerChannel = 5; + uint8_t numBytes = 2 * RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS * bytesPerChannel; + 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; + 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) -int16_t LoRaWANNode::uplink(String& str, uint8_t port) { - return(this->uplink(str.c_str(), port)); +int16_t LoRaWANNode::uplink(String& str, uint8_t port, bool isConfirmed, bool adrEnabled) { + return(this->uplink(str.c_str(), port, isConfirmed, adrEnabled)); } #endif -int16_t LoRaWANNode::uplink(const char* str, uint8_t port) { - return(this->uplink((uint8_t*)str, strlen(str), port)); +int16_t LoRaWANNode::uplink(const char* str, uint8_t port, bool isConfirmed, bool adrEnabled) { + return(this->uplink((uint8_t*)str, strlen(str), port, isConfirmed, adrEnabled)); } -int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port) { - bool adrEn = true; +int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConfirmed, bool adrEnabled) { // check destination port if(port > 0xDF) { return(RADIOLIB_ERR_INVALID_PORT); @@ -452,26 +692,47 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port) { return(RADIOLIB_ERR_PACKET_TOO_LONG); } - // get frame counter from persistent storage - uint32_t fcnt = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID) + 1; - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID, fcnt); - uint32_t adrFcnt = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_FCNT_ID); - uint8_t adrParams = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_LIM_DEL_ID); - uint32_t adrLimit = 0x01 << ((adrParams & 0xF0) >> 4); - uint32_t adrDelay = 0x01 << ((adrParams & 0x0F) >> 0); + // 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((fcnt - adrFcnt) == adrLimit) { + if((this->fcntUp - this->adrFcnt) >= adrLimit) { adrAckReq = true; - // add MAC command to queue - } else if ((fcnt - adrFcnt) == (adrLimit + adrDelay)) { - // set TX power to max + } else if ((this->fcntUp - this->adrFcnt) == (adrLimit + adrDelay)) { + // try one of three, in order: set TxPower to max, set DR to min, enable all defined channels + + // set the maximum power supported by both the module and the band + int8_t pwr = this->band->powerMax; + 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(pwr--); + } + RADIOLIB_ASSERT(state); + if(pwr == this->txPwrCur) { + + // 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]--; + } 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; + } + } + } - // decrease DR if possible - if(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] > this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK].drMin) { - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]--; } else { - // enable all channels + this->txPwrCur = pwr; } + + // we tried something to improve the range, so increase the ADR frame counter by 'ADR delay' + this->adrFcnt += adrDelay; } // configure for uplink @@ -495,19 +756,30 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port) { #endif // set the packet fields - uplinkMsg[RADIOLIB_LORAWAN_FHDR_LEN_START_OFFS] = RADIOLIB_LORAWAN_MHDR_MTYPE_UNCONF_DATA_UP | RADIOLIB_LORAWAN_MHDR_MAJOR_R1; + if(isConfirmed) { + uplinkMsg[RADIOLIB_LORAWAN_FHDR_LEN_START_OFFS] = RADIOLIB_LORAWAN_MHDR_MTYPE_CONF_DATA_UP; + this->confFcntUp = this->fcntUp; + } else { + uplinkMsg[RADIOLIB_LORAWAN_FHDR_LEN_START_OFFS] = RADIOLIB_LORAWAN_MHDR_MTYPE_UNCONF_DATA_UP; + } + uplinkMsg[RADIOLIB_LORAWAN_FHDR_LEN_START_OFFS] |= RADIOLIB_LORAWAN_MHDR_MAJOR_R1; LoRaWANNode::hton(&uplinkMsg[RADIOLIB_LORAWAN_FHDR_DEV_ADDR_POS], this->devAddr); // length of fopts will be added later uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] = 0x00; - if(adrEn) { + if(adrEnabled) { uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] |= RADIOLIB_LORAWAN_FCTRL_ADR_ENABLED; if(adrAckReq) { uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] |= RADIOLIB_LORAWAN_FCTRL_ADR_ACK_REQ; } } - LoRaWANNode::hton(&uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCNT_POS], (uint16_t)fcnt); + // if the saved confirm-fcnt is equal to the last app-downlink, set the ACK bit and clear the confirm-fcnt + if(this->confFcntDown != RADIOLIB_LORAWAN_FCNT_NONE) { + uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] |= RADIOLIB_LORAWAN_FCTRL_ACK; + } + + LoRaWANNode::hton(&uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCNT_POS], (uint16_t)this->fcntUp); // check if we have some MAC commands to append if(foptsLen > 0) { @@ -534,12 +806,8 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port) { uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] |= foptsLen; // encrypt it - processAES(foptsBuff, foptsLen, this->nwkSEncKey, &uplinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], fcnt, RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK, 0x01, true); + processAES(foptsBuff, foptsLen, this->nwkSEncKey, &uplinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], this->fcntUp, RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK, 0x01, true); - // write the current MAC command queue to nvm for next uplink - uint8_t queueBuff[sizeof(LoRaWANMacCommandQueue_t)]; - memcpy(queueBuff, &this->commandsUp, sizeof(LoRaWANMacCommandQueue_t)); - mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FOPTS_ID), queueBuff, sizeof(LoRaWANMacCommandQueue_t)); } // set the port @@ -552,15 +820,17 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port) { } // encrypt the frame payload - // TODO check ctrId --> erratum says it should be 0x01? - processAES(data, len, encKey, &uplinkMsg[RADIOLIB_LORAWAN_FRAME_PAYLOAD_POS(foptsLen)], fcnt, RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK, 0x00, true); + processAES(data, len, encKey, &uplinkMsg[RADIOLIB_LORAWAN_FRAME_PAYLOAD_POS(foptsLen)], this->fcntUp, RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK, 0x00, true); // create blocks for MIC calculation uint8_t block0[RADIOLIB_AES128_BLOCK_SIZE] = { 0 }; block0[RADIOLIB_LORAWAN_BLOCK_MAGIC_POS] = RADIOLIB_LORAWAN_MIC_BLOCK_MAGIC; + if(this->confFcntDown != RADIOLIB_LORAWAN_FCNT_NONE) { + LoRaWANNode::hton(&block0[RADIOLIB_LORAWAN_BLOCK_CONF_FCNT_POS], (uint16_t)this->confFcntDown); + } block0[RADIOLIB_LORAWAN_BLOCK_DIR_POS] = RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK; LoRaWANNode::hton(&block0[RADIOLIB_LORAWAN_BLOCK_DEV_ADDR_POS], this->devAddr); - LoRaWANNode::hton(&block0[RADIOLIB_LORAWAN_BLOCK_FCNT_POS], fcnt); + LoRaWANNode::hton(&block0[RADIOLIB_LORAWAN_BLOCK_FCNT_POS], this->fcntUp); block0[RADIOLIB_LORAWAN_MIC_BLOCK_LEN_POS] = uplinkMsgLen - RADIOLIB_AES128_BLOCK_SIZE - sizeof(uint32_t); uint8_t block1[RADIOLIB_AES128_BLOCK_SIZE] = { 0 }; @@ -600,6 +870,10 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port) { // set the timestamp so that we can measure when to start receiving this->rxDelayStart = txStart + timeOnAir; + + // the downlink confirmation was transmitted, so clear the counter value + this->confFcntDown = RADIOLIB_LORAWAN_FCNT_NONE; + return(RADIOLIB_ERR_NONE); } @@ -629,7 +903,7 @@ int16_t LoRaWANNode::downlink(String& str) { int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { // check if there are any upcoming Rx windows Module* mod = this->phyLayer->getMod(); - const uint32_t scanGuard = 50; + 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 @@ -648,7 +922,7 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { downlinkTimeout = false; downlinkReceived = false; - this->phyLayer->setChannelScanAction(LoRaWANNodeOnChannelScan); + // this->phyLayer->setChannelScanAction(LoRaWANNodeOnChannelScan); // calculate the Rx timeout // according to the spec, this must be at least enough time to effectively detect a preamble @@ -656,7 +930,7 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { DataRate_t dataRate; findDataRate(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK], &dataRate); uint32_t timeoutHost = 0; - uint32_t timeoutMod = this->phyLayer->calculateRxTimeout(8, &dataRate, 2*scanGuard*1000, timeoutHost); + uint32_t timeoutMod = this->phyLayer->calculateRxTimeout(23, &dataRate, 2*scanGuard*1000, timeoutHost); RADIOLIB_DEBUG_PRINTLN("RxTimeout host: %d, module: %06x", timeoutHost, timeoutMod); // perform listening in the two Rx windows @@ -668,9 +942,7 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { if(waitLen > scanGuard) { waitLen -= scanGuard; } - RADIOLIB_DEBUG_PRINTLN("Waiting for %d ms...", waitLen); mod->hal->delay(waitLen); - RADIOLIB_DEBUG_PRINTLN("Opening Rx%d window...", (i+1)); // TODO somehow make this module-independent, currently configured for SX126x uint16_t irqFlags = 0b0000000000000010; // RxDone @@ -681,10 +953,11 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { uint32_t rxStart = mod->hal->millis(); this->phyLayer->startReceive(timeoutMod, irqFlags, irqMask, 0); RADIOLIB_DEBUG_PRINT("Opened Rx%d window (%d us timeout)... ", i+1, timeoutHost); - while(!downlinkTimeout && ((mod->hal->millis() - rxStart) < (timeoutHost / 1000))) { - mod->hal->yield(); - } + mod->hal->delay(timeoutHost / 1000 + scanGuard); RADIOLIB_DEBUG_PRINTLN("closing"); + if(this->phyLayer->isRxTimeout()) { + downlinkTimeout = true; + } // check if we detected something of a packet if(!downlinkTimeout) { @@ -700,12 +973,12 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { findDataRate(this->rx2.drMax, &dataRate); state = this->phyLayer->setDataRate(dataRate); RADIOLIB_ASSERT(state); - timeoutMod = this->phyLayer->calculateRxTimeout(8, &dataRate, 2*scanGuard*1000, timeoutHost); + timeoutMod = this->phyLayer->calculateRxTimeout(22, &dataRate, 2*scanGuard*1000, timeoutHost); downlinkTimeout = false; } } - this->phyLayer->clearChannelScanAction(); + // this->phyLayer->clearChannelScanAction(); // check if we received a packet at all if(downlinkTimeout) { @@ -716,9 +989,11 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { return(RADIOLIB_ERR_RX_TIMEOUT); } - this->phyLayer->setPacketReceivedAction(LoRaWANNodeOnDownlink); - while(!downlinkReceived) { + // this->phyLayer->setPacketReceivedAction(LoRaWANNodeOnDownlink); + + // while(!downlinkReceived) { + while(!this->phyLayer->readIrq()) { mod->hal->yield(); } @@ -773,9 +1048,16 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { } // get the frame counter and set it to the MIC calculation block - // TODO cache the ADR bit? uint16_t fcnt16 = LoRaWANNode::ntoh(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FCNT_POS]); LoRaWANNode::hton(&downlinkMsg[RADIOLIB_LORAWAN_BLOCK_FCNT_POS], fcnt16); + + // if this downlink is confirming an uplink, its MIC was generated with the least-significant 16 bits of that fcntUp + // TODO get this to the user somehow + bool isConfirmingUp = false; + if((downlinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] & RADIOLIB_LORAWAN_FCTRL_ACK) && (this->rev == 1)) { + isConfirmingUp = true; + LoRaWANNode::hton(&downlinkMsg[RADIOLIB_LORAWAN_BLOCK_CONF_FCNT_POS], (uint16_t)this->confFcntUp); + } RADIOLIB_DEBUG_PRINTLN("downlinkMsg:"); RADIOLIB_DEBUG_HEXDUMP(downlinkMsg, RADIOLIB_AES128_BLOCK_SIZE + downlinkMsgLen); @@ -792,9 +1074,9 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { // check the FcntDown value (Network or Application) uint32_t fcntDownPrev = 0; if (isAppDownlink) { - fcntDownPrev = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_A_FCNT_DOWN_ID); + fcntDownPrev = this->aFcntDown; } else { - fcntDownPrev = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_N_FCNT_DOWN_ID); + fcntDownPrev = this->nFcntDown; } RADIOLIB_DEBUG_PRINTLN("fcnt: %d, fcntPrev: %d, isAppDownlink: %d", fcnt16, fcntDownPrev, (int)isAppDownlink); @@ -819,13 +1101,6 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { } } - // save current fcnt to NVM - if (isAppDownlink) { - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_A_FCNT_DOWN_ID, fcnt32); - } else { - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_N_FCNT_DOWN_ID, fcnt32); - } - // check the MIC if(!verifyMIC(downlinkMsg, RADIOLIB_AES128_BLOCK_SIZE + downlinkMsgLen, this->sNwkSIntKey)) { #if !defined(RADIOLIB_STATIC_ONLY) @@ -833,6 +1108,19 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { #endif return(RADIOLIB_ERR_CRC_MISMATCH); } + + // save current fcnt to respective frame counter + if (isAppDownlink) { + this->aFcntDown = fcnt32; + } else { + this->nFcntDown = fcnt32; + } + + // if this is a confirmed frame, save the downlink number (only app frames can be confirmed) + bool isConfirmedDown = false; + if(downlinkMsg[RADIOLIB_LORAWAN_FHDR_LEN_START_OFFS] && 0xFE == RADIOLIB_LORAWAN_MHDR_MTYPE_CONF_DATA_DOWN) { + this->confFcntDown = this->aFcntDown; + } // check the address uint32_t addr = LoRaWANNode::ntoh(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_DEV_ADDR_POS]); @@ -900,17 +1188,16 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { this->isMACPayload = true; this->uplink(foptsBuff, foptsBufSize, RADIOLIB_LORAWAN_FPORT_MAC_COMMAND); + uint8_t strDown[this->band->payloadLenMax[this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK]]]; + size_t lenDown = 0; + state = this->downlink(strDown, &lenDown); + RADIOLIB_ASSERT(state); } - // write the MAC command queue to nvm for next uplink - uint8_t queueBuff[sizeof(LoRaWANMacCommandQueue_t)]; - memcpy(queueBuff, &this->commandsUp, sizeof(LoRaWANMacCommandQueue_t)); - mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FOPTS_ID), queueBuff, sizeof(LoRaWANMacCommandQueue_t)); } // a downlink was received, so reset the ADR counter to this uplink's fcnt - uint32_t fcntUp = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_FCNT_ID, fcntUp); + this->adrFcnt = this->fcntUp; // process payload (if there is any) if(payLen <= 0) { @@ -989,13 +1276,13 @@ int16_t LoRaWANNode::setPhyProperties() { // RADIOLIB_ASSERT(state); // set the maximum power supported by both the module and the band - int8_t pwr = this->band->powerMax; 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(pwr--); + state = this->phyLayer->setOutputPower(this->txPwrCur--); } RADIOLIB_ASSERT(state); + this->txPwrCur++; uint8_t syncWord[3] = { 0 }; uint8_t syncWordLen = 0; @@ -1109,7 +1396,6 @@ int16_t LoRaWANNode::selectChannelsJR(uint16_t devNonce) { // configure data rates for TX and RX1: for TX the (floored) average of min and max; for RX1 identical with base offset this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = int((channelUp.drMax + channelUp.drMin) / 2); - // TODO check bounds this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = getDownlinkDataRate(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK], this->rx1DrOffset, this->band->rx1DataRateBase, channelDn.drMin, channelDn.drMax); } else { // RADIOLIB_LORAWAN_CFLIST_TYPE_MASK @@ -1133,16 +1419,12 @@ int16_t LoRaWANNode::selectChannelsJR(uint16_t devNonce) { // configure data rates for TX and RX1: for TX the specified value for this band; for RX1 identical with base offset this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = this->band->txSpans[spanID].joinRequestDataRate; - // TODO check bounds this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = getDownlinkDataRate(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK], this->rx1DrOffset, this->band->rx1DataRateBase, channelDn.drMin, channelDn.drMax); } this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = channelUp; this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = channelDn; - Module* mod = this->phyLayer->getMod(); - uint8_t txDrRx2Dr = (this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] << 4) | this->rx2.drMax; - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXDR_RX2DR_ID, txDrRx2Dr); return(RADIOLIB_ERR_NONE); } @@ -1216,7 +1498,6 @@ int16_t LoRaWANNode::configureChannel(uint8_t dir) { // set the frequency RADIOLIB_DEBUG_PRINTLN(""); RADIOLIB_DEBUG_PRINTLN("Channel frequency %cL = %f MHz", dir ? 'D' : 'U', this->currentChannels[dir].freq); - RADIOLIB_DEBUG_PRINTLN("Datarate index: %d", this->dataRates[dir]); int state = this->phyLayer->setFrequency(this->currentChannels[dir].freq); RADIOLIB_ASSERT(state); @@ -1228,48 +1509,6 @@ int16_t LoRaWANNode::configureChannel(uint8_t dir) { return(state); } -int16_t LoRaWANNode::saveChannels() { - uint8_t bytesPerChannel = 5; - uint8_t numBytes = 2 * RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS * bytesPerChannel; - 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; - 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); -} - -int16_t LoRaWANNode::restoreChannels() { - uint8_t bytesPerChannel = 5; - uint8_t numBytes = 2 * RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS * bytesPerChannel; - uint8_t buffer[numBytes]; - 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[5] = { 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; - } - } - return(RADIOLIB_ERR_NONE); -} - 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); @@ -1355,6 +1594,7 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { case(RADIOLIB_LORAWAN_MAC_CMD_LINK_ADR): { // get the ADR configuration + // TODO all these configuration should only be set if all ACKs are set, otherwise retain previous state uint8_t dr = (cmd->payload[0] & 0xF0) >> 4; uint8_t txPower = cmd->payload[0] & 0x0F; uint16_t chMask = LoRaWANNode::ntoh(&cmd->payload[1]); @@ -1375,8 +1615,6 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK].drMax; } - uint8_t txDrRx2Dr = (this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] << 4) | this->rx2.drMax; - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXDR_RX2DR_ID, txDrRx2Dr); drAck = 1; } @@ -1390,6 +1628,7 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { RADIOLIB_DEBUG_PRINTLN("ADR set pwr = %d", pwr); pwrAck = 1; } + this->txPwrCur = pwr; } this->nbTrans = nbTrans; @@ -1449,12 +1688,7 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { this->phyLayer->setFrequency(this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK].freq); } - // update saved values - uint8_t txDrRx2Dr = (this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] << 4) | this->rx2.drMax; - uint8_t rx1DrOffDel = (this->rx1DrOffset << 4) | (this->rxDelays[0] / 1000); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TXDR_RX2DR_ID, txDrRx2Dr); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_RX1_DROFF_DEL_ID, rx1DrOffDel); - + // 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); @@ -1508,8 +1742,10 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { } } +#if !defined(RADIOLIB_EEPROM_UNSUPPORTED) // update saved frequencies this->saveChannels(); +#endif // send the reply cmd->len = 1; @@ -1546,9 +1782,11 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { } } +#if !defined(RADIOLIB_EEPROM_UNSUPPORTED) // update saved frequencies this->saveChannels(); - +#endif + // send the reply cmd->len = 1; cmd->payload[0] = (freqUlAck << 1) | (freqDlAck << 0); @@ -1570,10 +1808,6 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { this->rxDelays[0] = delay * 1000; this->rxDelays[1] = this->rxDelays[0] + 1000; - // update saved values - uint8_t rx1DrOffDel = (this->rx1DrOffset << 4) | (this->rxDelays[0] / 1000); - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_RX1_DROFF_DEL_ID, rx1DrOffDel); - // send the reply cmd->len = 0; @@ -1615,9 +1849,6 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { uint8_t limitExp = (cmd->payload[0] & 0xF0) >> 4; uint8_t delayExp = cmd->payload[0] & 0x0F; RADIOLIB_DEBUG_PRINTLN("ADR param setup: limitExp = %d, delayExp = %d", limitExp, delayExp); - - // update saved values - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_LIM_DEL_ID, cmd->payload[0]); return(1); } break; diff --git a/src/protocols/LoRaWAN/LoRaWAN.h b/src/protocols/LoRaWAN/LoRaWAN.h index 636c70c30..b88c414bd 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.h +++ b/src/protocols/LoRaWAN/LoRaWAN.h @@ -135,6 +135,7 @@ // payload encryption/MIC blocks common layout #define RADIOLIB_LORAWAN_BLOCK_MAGIC_POS (0) +#define RADIOLIB_LORAWAN_BLOCK_CONF_FCNT_POS (1) #define RADIOLIB_LORAWAN_BLOCK_DIR_POS (5) #define RADIOLIB_LORAWAN_BLOCK_DEV_ADDR_POS (6) #define RADIOLIB_LORAWAN_BLOCK_FCNT_POS (10) @@ -171,6 +172,9 @@ #define RADIOLIB_LORAWAN_MAC_CMD_REJOIN_PARAM_SETUP (0x0F) #define RADIOLIB_LORAWAN_MAC_CMD_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) @@ -334,9 +338,6 @@ class LoRaWANNode { // RX2 channel properties - may be changed by MAC command LoRaWANChannel_t rx2; - // Number of allowed frame retransmissions - uint8_t nbTrans; - /*! \brief Default constructor. \param phy Pointer to the PhysicalLayer radio module. @@ -344,6 +345,7 @@ class LoRaWANNode { */ LoRaWANNode(PhysicalLayer* phy, const LoRaWANBand_t* band); +#if !defined(RADIOLIB_EEPROM_UNSUPPORTED) /*! \brief Wipe internal persistent parameters. This will reset all counters and saved variables, so the device will have to rejoin the network. @@ -356,6 +358,14 @@ class LoRaWANNode { */ int16_t restore(); + /*! + \brief Restore frame counter for uplinks from persistent storage. + Note that the usable frame counter width is 'only' 30 bits for highly efficient wear-levelling. + \returns \ref status_codes + */ + int16_t restoreFcntUp(); +#endif + /*! \brief Join network by performing over-the-air activation. By this procedure, the device will perform an exchange with the network server and set all necessary configuration. @@ -381,32 +391,52 @@ 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 Save the current state of the session. + All variables are compared to what is saved and only the differences are rewritten. + \returns \ref status_codes + */ + int16_t saveSession(); + + /*! + \brief Save the current uplink frame counter. + Note that the usable frame counter width is 'only' 30 bits for highly efficient wear-levelling. + \returns \ref status_codes + */ + int16_t saveFcntUp(); + #if defined(RADIOLIB_BUILD_ARDUINO) /*! \brief Send a message to the server. \param str Address of Arduino String that will be transmitted. \param port Port number to send the message to. + \param isConfirmed Whether to send a confirmed uplink or not. + \param adrEnabled Whether ADR is enabled or not. \returns \ref status_codes */ - int16_t uplink(String& str, uint8_t port); + int16_t uplink(String& str, uint8_t port, bool isConfirmed = false, bool adrEnabled = true); #endif /*! \brief Send a message to the server. \param str C-string that will be transmitted. \param port Port number to send the message to. + \param isConfirmed Whether to send a confirmed uplink or not. + \param adrEnabled Whether ADR is enabled or not. \returns \ref status_codes */ - int16_t uplink(const char* str, uint8_t port); + int16_t uplink(const char* str, uint8_t port, bool isConfirmed = false, bool adrEnabled = true); /*! \brief Send a message to the server. \param data Data to send. \param len Length of the data. \param port Port number to send the message to. + \param isConfirmed Whether to send a confirmed uplink or not. + \param adrEnabled Whether ADR is enabled or not. \returns \ref status_codes */ - int16_t uplink(uint8_t* data, size_t len, uint8_t port); + int16_t uplink(uint8_t* data, size_t len, uint8_t port, bool isConfirmed = false, bool adrEnabled = true); #if defined(RADIOLIB_BUILD_ARDUINO) /*! @@ -457,6 +487,23 @@ class LoRaWANNode { uint8_t sNwkSIntKey[RADIOLIB_AES128_KEY_SIZE] = { 0 }; uint8_t nwkSEncKey[RADIOLIB_AES128_KEY_SIZE] = { 0 }; uint8_t jSIntKey[RADIOLIB_AES128_KEY_SIZE] = { 0 }; + + // device-specific parameters, persistent through sessions + uint16_t devNonce = 0; + uint32_t joinNonce = 0; + + // session-specific parameters + uint32_t homeNetId = 0; + 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; + uint32_t fcntUp = 0; + uint32_t aFcntDown = 0; + uint32_t nFcntDown = 0; + uint32_t confFcntUp = RADIOLIB_LORAWAN_FCNT_NONE; + uint32_t confFcntDown = RADIOLIB_LORAWAN_FCNT_NONE; + uint32_t adrFcnt = 0; // available channel frequencies from list passed during OTA activation LoRaWANChannel_t availableChannels[2][RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS] = { { 0 }, { 0 } }; diff --git a/src/protocols/PhysicalLayer/PhysicalLayer.cpp b/src/protocols/PhysicalLayer/PhysicalLayer.cpp index de31e5719..d558d4793 100644 --- a/src/protocols/PhysicalLayer/PhysicalLayer.cpp +++ b/src/protocols/PhysicalLayer/PhysicalLayer.cpp @@ -299,7 +299,16 @@ uint32_t PhysicalLayer::calculateRxTimeout(uint8_t numSymbols, DataRate_t *datar (void)datarate; (void)offsetUs; (void)timeoutUs; - return(RADIOLIB_ERR_UNSUPPORTED); + return(0); +} + +bool PhysicalLayer::isRxTimeout() { + return(0); +} + +uint16_t PhysicalLayer::readIrq(bool clear) { + (void)clear; + return(0); } int16_t PhysicalLayer::startChannelScan() { diff --git a/src/protocols/PhysicalLayer/PhysicalLayer.h b/src/protocols/PhysicalLayer/PhysicalLayer.h index 12ce0a7ed..e9f9f4da5 100644 --- a/src/protocols/PhysicalLayer/PhysicalLayer.h +++ b/src/protocols/PhysicalLayer/PhysicalLayer.h @@ -319,6 +319,18 @@ class PhysicalLayer { \returns Timeout value in a unit that is specific for the used module */ virtual uint32_t calculateRxTimeout(uint8_t numSymbols, DataRate_t* datarate, uint32_t offsetUs, uint32_t& timeoutUs); + + /*! + \brief Check whether there is a RxTimeout flag set + \returns RxTimeout flag is set + */ + virtual bool isRxTimeout(); + + /*! + \brief Check whether there is a RxTimeout flag set + \returns RxTimeout flag is set + */ + virtual uint16_t readIrq(bool clear = false); /*! \brief Interrupt-driven channel activity detection method. interrupt will be activated From e7b2b27dd5826707b01dd7132431f22ea017d0bc Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Mon, 30 Oct 2023 12:16:38 +0100 Subject: [PATCH 04/21] [LoRaWAN] Module-independent (OTAA) Rx windows, fix confirming downlinks --- src/modules/SX126x/SX126x.cpp | 25 +- src/modules/SX126x/SX126x.h | 24 +- src/protocols/LoRaWAN/LoRaWAN.cpp | 305 ++++++++---------- src/protocols/LoRaWAN/LoRaWAN.h | 6 + src/protocols/PhysicalLayer/PhysicalLayer.cpp | 15 +- src/protocols/PhysicalLayer/PhysicalLayer.h | 24 +- 6 files changed, 171 insertions(+), 228 deletions(-) diff --git a/src/modules/SX126x/SX126x.cpp b/src/modules/SX126x/SX126x.cpp index 4080d2ffa..7f3e23853 100644 --- a/src/modules/SX126x/SX126x.cpp +++ b/src/modules/SX126x/SX126x.cpp @@ -1436,27 +1436,18 @@ uint32_t SX126x::getTimeOnAir(size_t len) { } } -uint32_t SX126x::calculateRxTimeout(uint8_t numSymbols, DataRate_t *datarate, uint32_t offsetUs, uint32_t& timeoutUs) { - uint32_t symbolTime = (0x00000001 << datarate->lora.spreadingFactor) * 1000 / datarate->lora.bandwidth; - timeoutUs = (uint32_t)numSymbols * symbolTime + offsetUs; +uint32_t SX126x::calculateRxTimeout(uint8_t numSymbols, uint32_t timeoutUs) { + (void)numSymbols; // not used for these modules uint32_t timeout = timeoutUs / 15.625; return(timeout); } -bool SX126x::isRxTimeout() { - uint16_t irq = getIrqStatus(); - bool isRxTimeout = false; - if(irq & RADIOLIB_SX126X_IRQ_TIMEOUT) { - isRxTimeout = true; - } - return(isRxTimeout); -} - -uint16_t SX126x::readIrq(bool clear) { - uint16_t irq = getIrqStatus(); - if(clear) - clearIrqStatus(); - return(irq); +int16_t SX126x::irqRxDoneRxTimeout(uint16_t &irqFlags, uint16_t &irqMask) { + irqFlags = 0b0000000000000010; // RxDone + irqMask = 0b0000000000000010; + irqFlags |= 0b0000001000000000; // RxTimeout + irqMask |= 0b0000001000000000; + return(RADIOLIB_ERR_NONE); } int16_t SX126x::implicitHeader(size_t len) { diff --git a/src/modules/SX126x/SX126x.h b/src/modules/SX126x/SX126x.h index 3e4c69df2..ffef9fbdc 100644 --- a/src/modules/SX126x/SX126x.h +++ b/src/modules/SX126x/SX126x.h @@ -950,26 +950,20 @@ class SX126x: public PhysicalLayer { uint32_t getTimeOnAir(size_t len) override; /*! - \brief Get the Rx timeout required to listen to a preamble of a certain number of symbols - \param numSymbols Number of symbols to listen for - \param datarate The datarate for which to calculate the timeout - \param offsetUs Additional offset in microseconds to allow increasing the timeout - \param timeoutUs Returns the timeout in microseconds for the host to sleep + \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 + \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, DataRate_t *datarate, uint32_t offsetUs, uint32_t& timeoutUss); + uint32_t calculateRxTimeout(uint8_t numSymbols, uint32_t timeoutUs); /*! - \brief Check whether there is a RxTimeout flag set - \returns RxTimeout flag is set - */ - bool isRxTimeout(); - - /*! - \brief Check whether there is a RxTimeout flag set - \returns RxTimeout flag is set + \brief Create the flags that make up RxDone and RxTimeout used for receiving downlinks + \param irqFlags The flags for which IRQs must be triggered + \param irqMask Mask indicating which IRQ triggers a DIO + \returns \ref status_codes */ - uint16_t readIrq(bool clear = false); + int16_t irqRxDoneRxTimeout(uint16_t &irqFlags, uint16_t &irqMask); /*! \brief Set implicit header mode for future reception/transmission. diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index 3b522153b..65359b333 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -4,30 +4,19 @@ #if !defined(RADIOLIB_EXCLUDE_LORAWAN) -// flag to indicate whether we have received a downlink -static volatile bool downlinkReceived = false; - #if defined(RADIOLIB_EEPROM_UNSUPPORTED) #warning "Persistent storage not supported!" #endif -// interrupt service routine to handle downlinks automatically -#if defined(ESP8266) || defined(ESP32) - IRAM_ATTR -#endif -static void LoRaWANNodeOnDownlink(void) { - downlinkReceived = true; -} - -// flag to indicate whether channel scan operation is complete -static volatile bool downlinkTimeout = false; +// flag to indicate whether there was some action during Rx mode (timeout or downlink) +static volatile bool downlinkAction = false; // interrupt service routine to handle downlinks automatically #if defined(ESP8266) || defined(ESP32) IRAM_ATTR #endif -static void LoRaWANNodeOnChannelScan(void) { - downlinkTimeout = true; +static void LoRaWANNodeOnDownlinkAction(void) { + downlinkAction = true; } LoRaWANNode::LoRaWANNode(PhysicalLayer* phy, const LoRaWANBand_t* band) { @@ -48,10 +37,6 @@ void LoRaWANNode::wipe() { int16_t LoRaWANNode::restore() { // check the magic value Module* mod = this->phyLayer->getMod(); - if(mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID) != RADIOLIB_LORAWAN_MAGIC) { - // the magic value is not set, user will have to do perform the join procedure - return(RADIOLIB_ERR_NETWORK_NOT_JOINED); - } uint16_t nvm_table_version = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TABLE_VERSION_ID); // if (RADIOLIB_PERSISTENT_PARAM_LORAWAN_TABLE_VERSION > nvm_table_version) { @@ -59,6 +44,12 @@ int16_t LoRaWANNode::restore() { // } (void)nvm_table_version; + 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)); + // 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); @@ -67,10 +58,10 @@ int16_t LoRaWANNode::restore() { mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_NWK_SENC_KEY_ID), this->nwkSEncKey, RADIOLIB_AES128_BLOCK_SIZE); // get session parameters - this->rev = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION); + this->rev = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION); 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); + 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); @@ -79,25 +70,25 @@ int16_t LoRaWANNode::restore() { 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->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; + 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; + 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; + uint32_t rx2Freq = LoRaWANNode::ntoh(&rx2FreqBuf[0], 3); + this->rx2.drMax = (txDrRx2Dr >> 0) & 0x0F; + this->rx2.freq = (float)rx2Freq / 10000.0; - uint8_t adrLimDel = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_ADR_LIM_DEL_ID); - this->adrLimitExp = (adrLimDel >> 4) & 0x0F; - this->adrDelayExp = (adrLimDel >> 0) & 0x0F; + 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->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); @@ -127,7 +118,6 @@ int16_t LoRaWANNode::restoreFcntUp() { uint8_t fcntBuff[30] = { 0 }; mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID), fcntBuff, 30); - RADIOLIB_DEBUG_HEXDUMP(fcntBuff, 30); // copy the two most significant bytes from the first two bytes uint8_t bits_30_22 = fcntBuff[0]; @@ -226,46 +216,16 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe // send it state = this->phyLayer->transmit(joinRequestMsg, RADIOLIB_LORAWAN_JOIN_REQUEST_LEN); - RADIOLIB_ASSERT(state); - - // configure for downlink with default configuration - state = this->configureChannel(RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK); - RADIOLIB_ASSERT(state); - - // set the function that will be called when the reply is received - this->phyLayer->setPacketReceivedAction(LoRaWANNodeOnDownlink); - - // downlink messages are sent with inverted IQ - // TODO use downlink() for this - if(!this->FSK) { - state = this->phyLayer->invertIQ(true); - RADIOLIB_ASSERT(state); - } - - // start receiving - uint32_t start = mod->hal->millis(); - downlinkReceived = false; - state = this->phyLayer->startReceive(); + this->rxDelayStart = mod->hal->millis(); RADIOLIB_ASSERT(state); - // wait for the reply or timeout - while(!downlinkReceived) { - if(mod->hal->millis() - start >= RADIOLIB_LORAWAN_JOIN_ACCEPT_DELAY_2_MS + 2000) { - downlinkReceived = false; - if(!this->FSK) { - this->phyLayer->invertIQ(false); - } - return(RADIOLIB_ERR_RX_TIMEOUT); - } - } + // configure Rx delay for join-accept message - these are re-configured once a valid join-request is received + this->rxDelays[0] = RADIOLIB_LORAWAN_JOIN_ACCEPT_DELAY_1_MS; + this->rxDelays[1] = RADIOLIB_LORAWAN_JOIN_ACCEPT_DELAY_2_MS; - // we have a message, reset the IQ inversion - downlinkReceived = false; - this->phyLayer->clearPacketReceivedAction(); - if(!this->FSK) { - state = this->phyLayer->invertIQ(false); - RADIOLIB_ASSERT(state); - } + // handle Rx1 and Rx2 windows - returns RADIOLIB_ERR_NONE if a downlink is received + state = downlinkCommon(); + RADIOLIB_ASSERT(state); // build the buffer for the reply data uint8_t joinAcceptMsgEnc[RADIOLIB_LORAWAN_JOIN_ACCEPT_MAX_LEN]; @@ -508,22 +468,22 @@ int16_t LoRaWANNode::saveSession() { Module* mod = this->phyLayer->getMod(); // store session configuration (MAC commands) - if(mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION) != this->rev) - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION, this->rev); + if(mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION) != this->rev) + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION, 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); + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_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); + 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); + 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); + 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); @@ -536,26 +496,26 @@ int16_t LoRaWANNode::saveSession() { 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); + 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); + 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); + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_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); + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_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); + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_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); + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_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); + mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_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(); @@ -578,7 +538,6 @@ int16_t LoRaWANNode::saveFcntUp() { uint8_t fcntBuff[30] = { 0 }; mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID), fcntBuff, 30); - RADIOLIB_DEBUG_HEXDUMP(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 @@ -601,12 +560,16 @@ int16_t LoRaWANNode::saveFcntUp() { break; } } - idx = idx < 5 ? idx : 2; + // check if the last written byte is equal to current, only rewrite if different uint8_t bits_14_7 = (this->fcntUp >> 7) & 0x7F; + if((fcntBuff[idx - 1] & 0x7F) != bits_14_7) { + // find next index to write + idx = idx < 5 ? idx : 2; - // 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); + // 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); + } // equally, the last 7 bits are stored into one of many indices // this index is indicated by the first byte that has its state (most significant bit) different from its predecessor @@ -663,20 +626,26 @@ int16_t LoRaWANNode::uplink(const char* str, uint8_t port, bool isConfirmed, boo } int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConfirmed, bool adrEnabled) { + Module* mod = this->phyLayer->getMod(); + + // check if sufficient time has elapsed since the last uplink + if(mod->hal->millis() - this->rxDelayStart < rxDelays[1]) { + // not enough time elapsed since the last uplink, we may still be in an RX window + return(RADIOLIB_ERR_UPLINK_UNAVAILABLE); + } + // check destination port if(port > 0xDF) { return(RADIOLIB_ERR_INVALID_PORT); } // port 0 is only allowed for MAC-only payloads if(port == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND) { - if (!isMACPayload) { + if (!this->isMACPayload) { return(RADIOLIB_ERR_INVALID_PORT); } // if this is MAC only payload, continue and reset for next uplink - isMACPayload = false; + this->isMACPayload = false; } - - Module* mod = this->phyLayer->getMod(); // check if there are some MAC commands to piggyback (only when piggybacking onto a application-frame) uint8_t foptsLen = 0; @@ -701,7 +670,8 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf bool adrAckReq = false; if((this->fcntUp - this->adrFcnt) >= adrLimit) { adrAckReq = true; - } else if ((this->fcntUp - this->adrFcnt) == (adrLimit + adrDelay)) { + } + if ((this->fcntUp - this->adrFcnt) == (adrLimit + adrDelay)) { // try one of three, in order: set TxPower to max, set DR to min, enable all defined channels // set the maximum power supported by both the module and the band @@ -740,12 +710,6 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf int16_t state = this->configureChannel(RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK); RADIOLIB_ASSERT(state); - // check if sufficient time has elapsed since the last uplink - if(mod->hal->millis() - this->rxDelayStart < rxDelays[1]) { - // not enough time elapsed since the last uplink, we may still be in an RX window - return(RADIOLIB_ERR_UPLINK_UNAVAILABLE); - } - // build the uplink message // the first 16 bytes are reserved for MIC calculation blocks size_t uplinkMsgLen = RADIOLIB_LORAWAN_FRAME_LEN(len, foptsBufSize); @@ -774,7 +738,7 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf } } - // if the saved confirm-fcnt is equal to the last app-downlink, set the ACK bit and clear the confirm-fcnt + // if the saved confirm-fcnt is set, set the ACK bit if(this->confFcntDown != RADIOLIB_LORAWAN_FCNT_NONE) { uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] |= RADIOLIB_LORAWAN_FCTRL_ACK; } @@ -825,9 +789,6 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf // create blocks for MIC calculation uint8_t block0[RADIOLIB_AES128_BLOCK_SIZE] = { 0 }; block0[RADIOLIB_LORAWAN_BLOCK_MAGIC_POS] = RADIOLIB_LORAWAN_MIC_BLOCK_MAGIC; - if(this->confFcntDown != RADIOLIB_LORAWAN_FCNT_NONE) { - LoRaWANNode::hton(&block0[RADIOLIB_LORAWAN_BLOCK_CONF_FCNT_POS], (uint16_t)this->confFcntDown); - } block0[RADIOLIB_LORAWAN_BLOCK_DIR_POS] = RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK; LoRaWANNode::hton(&block0[RADIOLIB_LORAWAN_BLOCK_DEV_ADDR_POS], this->devAddr); LoRaWANNode::hton(&block0[RADIOLIB_LORAWAN_BLOCK_FCNT_POS], this->fcntUp); @@ -835,7 +796,9 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf uint8_t block1[RADIOLIB_AES128_BLOCK_SIZE] = { 0 }; memcpy(block1, block0, RADIOLIB_AES128_BLOCK_SIZE); - // TODO implement confirmed frames + if(this->confFcntDown != RADIOLIB_LORAWAN_FCNT_NONE) { + LoRaWANNode::hton(&block1[RADIOLIB_LORAWAN_BLOCK_CONF_FCNT_POS], (uint16_t)this->confFcntDown); + } block1[RADIOLIB_LORAWAN_MIC_DATA_RATE_POS] = this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]; block1[RADIOLIB_LORAWAN_MIC_CH_INDEX_POS] = this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK].idx; @@ -860,47 +823,24 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf RADIOLIB_DEBUG_HEXDUMP(uplinkMsg, uplinkMsgLen); // send it (without the MIC calculation blocks) - uint32_t txStart = mod->hal->millis(); - uint32_t timeOnAir = this->phyLayer->getTimeOnAir(uplinkMsgLen - RADIOLIB_LORAWAN_FHDR_LEN_START_OFFS) / 1000; state = this->phyLayer->transmit(&uplinkMsg[RADIOLIB_LORAWAN_FHDR_LEN_START_OFFS], uplinkMsgLen - RADIOLIB_LORAWAN_FHDR_LEN_START_OFFS); + + // set the timestamp so that we can measure when to start receiving + this->rxDelayStart = mod->hal->millis(); + RADIOLIB_DEBUG_PRINTLN("Uplink sent <-- Rx Delay start"); + #if !defined(RADIOLIB_STATIC_ONLY) delete[] uplinkMsg; #endif RADIOLIB_ASSERT(state); - - // set the timestamp so that we can measure when to start receiving - this->rxDelayStart = txStart + timeOnAir; - - // the downlink confirmation was transmitted, so clear the counter value + + // the downlink confirmation was acknowledged, so clear the counter value this->confFcntDown = RADIOLIB_LORAWAN_FCNT_NONE; return(RADIOLIB_ERR_NONE); } -#if defined(RADIOLIB_BUILD_ARDUINO) -int16_t LoRaWANNode::downlink(String& str) { - int16_t state = RADIOLIB_ERR_NONE; - - // build a temporary buffer - // LoRaWAN downlinks can have 250 bytes at most with 1 extra byte for NULL - size_t length = 0; - uint8_t data[251]; - - // wait for downlink - state = this->downlink(data, &length); - if(state == RADIOLIB_ERR_NONE) { - // add null terminator - data[length] = '\0'; - - // initialize Arduino String class - str = String((char*)data); - } - - return(state); -} -#endif - -int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { +int16_t LoRaWANNode::downlinkCommon() { // check if there are any upcoming Rx windows Module* mod = this->phyLayer->getMod(); const uint32_t scanGuard = 10; @@ -920,18 +860,19 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { RADIOLIB_ASSERT(state); } - downlinkTimeout = false; - downlinkReceived = false; - // this->phyLayer->setChannelScanAction(LoRaWANNodeOnChannelScan); - // calculate the Rx timeout // 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 - DataRate_t dataRate; - findDataRate(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK], &dataRate); - uint32_t timeoutHost = 0; - uint32_t timeoutMod = this->phyLayer->calculateRxTimeout(23, &dataRate, 2*scanGuard*1000, timeoutHost); - RADIOLIB_DEBUG_PRINTLN("RxTimeout host: %d, module: %06x", timeoutHost, timeoutMod); + uint32_t timeoutHost = this->phyLayer->getTimeOnAir(0) + 2*scanGuard*1000; + uint32_t timeoutMod = this->phyLayer->calculateRxTimeout(0, timeoutHost); + + // create the masks that are required for receiving downlinks + uint16_t irqFlags; + uint16_t irqMask; + this->phyLayer->irqRxDoneRxTimeout(irqFlags, irqMask); + + downlinkAction = false; + this->phyLayer->setPacketReceivedAction(LoRaWANNodeOnDownlinkAction); // perform listening in the two Rx windows for(uint8_t i = 0; i < 2; i++) { @@ -944,23 +885,16 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { } mod->hal->delay(waitLen); - // TODO somehow make this module-independent, currently configured for SX126x - uint16_t irqFlags = 0b0000000000000010; // RxDone - uint16_t irqMask = 0b0000000000000010; - irqFlags |= 0b0000001000000000; // RxTimeout - irqMask |= 0b0000001000000000; - - uint32_t rxStart = mod->hal->millis(); - this->phyLayer->startReceive(timeoutMod, irqFlags, irqMask, 0); - RADIOLIB_DEBUG_PRINT("Opened Rx%d window (%d us timeout)... ", i+1, timeoutHost); - mod->hal->delay(timeoutHost / 1000 + scanGuard); + // open Rx window by starting receive with specified timeout + state = this->phyLayer->startReceive(timeoutMod, irqFlags, irqMask, 0); + RADIOLIB_DEBUG_PRINTLN("Opening Rx%d window (%d us timeout)... <-- Rx Delay end ", i+1, timeoutHost); + + // wait for the timeout to complete (and a small additional delay) + mod->hal->delay(timeoutHost / 1000 + scanGuard / 2); RADIOLIB_DEBUG_PRINTLN("closing"); - if(this->phyLayer->isRxTimeout()) { - downlinkTimeout = true; - } - // check if we detected something of a packet - if(!downlinkTimeout) { + // check if the DIO has fired indicating a timeout; no timeout means a receive is ongoing + if(!downlinkAction) { break; } else if(i == 0) { @@ -973,16 +907,16 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { findDataRate(this->rx2.drMax, &dataRate); state = this->phyLayer->setDataRate(dataRate); RADIOLIB_ASSERT(state); - timeoutMod = this->phyLayer->calculateRxTimeout(22, &dataRate, 2*scanGuard*1000, timeoutHost); - downlinkTimeout = false; + timeoutHost = this->phyLayer->getTimeOnAir(0) + 2*scanGuard*1000; + timeoutMod = this->phyLayer->calculateRxTimeout(0, timeoutHost); + downlinkAction = false; } } - // this->phyLayer->clearChannelScanAction(); - // check if we received a packet at all - if(downlinkTimeout) { - this->phyLayer->standby(); + // if we got here due to a timeout, stop ongoing activities + if(downlinkAction) { + this->phyLayer->standby(); // TODO check: this should be done automagically due to RxSingle? if(!this->FSK) { this->phyLayer->invertIQ(false); } @@ -990,10 +924,8 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { return(RADIOLIB_ERR_RX_TIMEOUT); } - // this->phyLayer->setPacketReceivedAction(LoRaWANNodeOnDownlink); - - // while(!downlinkReceived) { - while(!this->phyLayer->readIrq()) { + // wait for the DIO to fire indicating a downlink is received + while(!downlinkAction) { mod->hal->yield(); } @@ -1005,6 +937,38 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { RADIOLIB_ASSERT(state); } + return(RADIOLIB_ERR_NONE); +} + +#if defined(RADIOLIB_BUILD_ARDUINO) +int16_t LoRaWANNode::downlink(String& str) { + int16_t state = RADIOLIB_ERR_NONE; + + // build a temporary buffer + // LoRaWAN downlinks can have 250 bytes at most with 1 extra byte for NULL + size_t length = 0; + uint8_t data[251]; + + // wait for downlink + state = this->downlink(data, &length); + if(state == RADIOLIB_ERR_NONE) { + // add null terminator + data[length] = '\0'; + + // initialize Arduino String class + str = String((char*)data); + } + + return(state); +} +#endif + +int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { + + // handle Rx1 and Rx2 windows - returns RADIOLIB_ERR_NONE if a downlink is received + int16_t state = downlinkCommon(); + RADIOLIB_ASSERT(state); + // get the packet length size_t downlinkMsgLen = this->phyLayer->getPacketLength(); RADIOLIB_DEBUG_PRINTLN("Downlink message length: %d", downlinkMsgLen); @@ -1025,7 +989,6 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { #endif // set the MIC calculation block - // TODO implement confirmed frames memset(downlinkMsg, 0x00, RADIOLIB_AES128_BLOCK_SIZE); downlinkMsg[RADIOLIB_LORAWAN_BLOCK_MAGIC_POS] = RADIOLIB_LORAWAN_MIC_BLOCK_MAGIC; LoRaWANNode::hton(&downlinkMsg[RADIOLIB_LORAWAN_BLOCK_DEV_ADDR_POS], this->devAddr); @@ -1118,7 +1081,7 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { // if this is a confirmed frame, save the downlink number (only app frames can be confirmed) bool isConfirmedDown = false; - if(downlinkMsg[RADIOLIB_LORAWAN_FHDR_LEN_START_OFFS] && 0xFE == RADIOLIB_LORAWAN_MHDR_MTYPE_CONF_DATA_DOWN) { + if((downlinkMsg[RADIOLIB_LORAWAN_FHDR_LEN_START_OFFS] & 0xFE) == RADIOLIB_LORAWAN_MHDR_MTYPE_CONF_DATA_DOWN) { this->confFcntDown = this->aFcntDown; } diff --git a/src/protocols/LoRaWAN/LoRaWAN.h b/src/protocols/LoRaWAN/LoRaWAN.h index b88c414bd..ff1644ad6 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.h +++ b/src/protocols/LoRaWAN/LoRaWAN.h @@ -438,6 +438,12 @@ class LoRaWANNode { */ int16_t uplink(uint8_t* data, size_t len, uint8_t port, bool isConfirmed = false, bool adrEnabled = true); + /*! + \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. diff --git a/src/protocols/PhysicalLayer/PhysicalLayer.cpp b/src/protocols/PhysicalLayer/PhysicalLayer.cpp index d558d4793..387001a0a 100644 --- a/src/protocols/PhysicalLayer/PhysicalLayer.cpp +++ b/src/protocols/PhysicalLayer/PhysicalLayer.cpp @@ -294,23 +294,18 @@ uint32_t PhysicalLayer::getTimeOnAir(size_t len) { return(0); } -uint32_t PhysicalLayer::calculateRxTimeout(uint8_t numSymbols, DataRate_t *datarate, uint32_t offsetUs, uint32_t& timeoutUs) { +uint32_t PhysicalLayer::calculateRxTimeout(uint8_t numSymbols, uint32_t timeoutUs) { (void)numSymbols; - (void)datarate; - (void)offsetUs; (void)timeoutUs; return(0); } -bool PhysicalLayer::isRxTimeout() { - return(0); +int16_t PhysicalLayer::irqRxDoneRxTimeout(uint16_t &irqFlags, uint16_t &irqMask) { + (void)irqFlags; + (void)irqMask; + return(RADIOLIB_ERR_UNSUPPORTED); } -uint16_t PhysicalLayer::readIrq(bool clear) { - (void)clear; - return(0); -} - int16_t PhysicalLayer::startChannelScan() { return(RADIOLIB_ERR_UNSUPPORTED); } diff --git a/src/protocols/PhysicalLayer/PhysicalLayer.h b/src/protocols/PhysicalLayer/PhysicalLayer.h index e9f9f4da5..4ffeb89ce 100644 --- a/src/protocols/PhysicalLayer/PhysicalLayer.h +++ b/src/protocols/PhysicalLayer/PhysicalLayer.h @@ -311,27 +311,21 @@ class PhysicalLayer { virtual uint32_t getTimeOnAir(size_t len); /*! - \brief Get the Rx timeout required to listen to a preamble of a certain number of symbols - \param numSymbols Number of symbols to listen for - \param datarate The datarate for which to calculate the timeout - \param offsetUs Additional offset in microseconds to allow increasing the timeout - \param timeoutUs Returns the timeout in microseconds for the host to sleep + \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 + \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, DataRate_t* datarate, uint32_t offsetUs, uint32_t& timeoutUs); + virtual uint32_t calculateRxTimeout(uint8_t numSymbols, uint32_t timeoutUs); /*! - \brief Check whether there is a RxTimeout flag set - \returns RxTimeout flag is set + \brief Create the flags that make up RxDone and RxTimeout used for receiving downlinks + \param irqFlags The flags for which IRQs must be triggered + \param irqMask Mask indicating which IRQ triggers a DIO + \returns \ref status_codes */ - virtual bool isRxTimeout(); + virtual int16_t irqRxDoneRxTimeout(uint16_t &irqFlags, uint16_t &irqMask); - /*! - \brief Check whether there is a RxTimeout flag set - \returns RxTimeout flag is set - */ - virtual uint16_t readIrq(bool clear = false); - /*! \brief Interrupt-driven channel activity detection method. interrupt will be activated when packet is detected. Must be implemented in module class. From f92e0e59d4ceb77bd7bb2d0326823418657587a3 Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Wed, 1 Nov 2023 16:51:24 +0100 Subject: [PATCH 05/21] [LoRaWAN] Implement SX127x support, fix MAC uplinking, support clock drift --- src/ArduinoHal.cpp | 16 ++++ src/BuildOpt.h | 12 +++ src/Hal.h | 2 +- src/modules/SX127x/SX127x.cpp | 29 +++++++ src/modules/SX127x/SX127x.h | 22 +++++ src/protocols/LoRaWAN/LoRaWAN.cpp | 129 ++++++++++++++---------------- src/protocols/LoRaWAN/LoRaWAN.h | 13 ++- 7 files changed, 146 insertions(+), 77 deletions(-) diff --git a/src/ArduinoHal.cpp b/src/ArduinoHal.cpp index b1d4a92f4..6beefa036 100644 --- a/src/ArduinoHal.cpp +++ b/src/ArduinoHal.cpp @@ -58,19 +58,35 @@ void inline ArduinoHal::detachInterrupt(uint32_t interruptNum) { } void inline ArduinoHal::delay(unsigned long ms) { +#if !defined(RADIOLIB_CLOCK_DRIFT_MS) ::delay(ms); +#else + ::delay(ms * (1000 - RADIOLIB_CLOCK_DRIFT_MS) / 1000); +#endif } void inline ArduinoHal::delayMicroseconds(unsigned long us) { +#if !defined(RADIOLIB_CLOCK_DRIFT_MS) ::delayMicroseconds(us); +#else + ::delayMicroseconds(us * (1000 - RADIOLIB_CLOCK_DRIFT_MS) / 1000); +#endif } unsigned long inline ArduinoHal::millis() { +#if !defined(RADIOLIB_CLOCK_DRIFT_MS) return(::millis()); +#else + return(::millis() * (1000 - RADIOLIB_CLOCK_DRIFT_MS) / 1000); +#endif } unsigned long inline ArduinoHal::micros() { +#if !defined(RADIOLIB_CLOCK_DRIFT_MS) return(::micros()); +#else + return(::micros() * (1000 - RADIOLIB_CLOCK_DRIFT_MS) / 1000); +#endif } long inline ArduinoHal::pulseIn(uint32_t pin, uint32_t state, unsigned long timeout) { diff --git a/src/BuildOpt.h b/src/BuildOpt.h index 1f8d952c1..b3fdbe7f3 100644 --- a/src/BuildOpt.h +++ b/src/BuildOpt.h @@ -446,6 +446,18 @@ #define RADIOLIB_HAL_PERSISTENT_STORAGE_SIZE (0x0180) #endif +/* + * Uncomment on boards whose clock runs too slow or too fast + * Set the value according to the following scheme: + * Enable timestamps on your terminal + * Print something to terminal, wait 1000 milliseconds, print something again + * If the difference is e.g. 1014 milliseconds between the prints, set this value to 14 + * Or, for more accuracy, wait for 100,000 milliseconds and divide the total drift by 100 + */ +#if !defined(RADIOLIB_CLOCK_DRIFT_MS) + //#define RADIOLIB_CLOCK_DRIFT_MS (0) +#endif + // This only compiles on STM32 boards with SUBGHZ module, but also // include when generating docs #if (!defined(ARDUINO_ARCH_STM32) || !defined(SUBGHZSPI_BASE)) && !defined(DOXYGEN) diff --git a/src/Hal.h b/src/Hal.h index 34930938e..780c34831 100644 --- a/src/Hal.h +++ b/src/Hal.h @@ -9,7 +9,7 @@ // 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 (2) +#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) diff --git a/src/modules/SX127x/SX127x.cpp b/src/modules/SX127x/SX127x.cpp index 1a8f2eaff..3f41c6186 100644 --- a/src/modules/SX127x/SX127x.cpp +++ b/src/modules/SX127x/SX127x.cpp @@ -405,6 +405,13 @@ int16_t SX127x::startReceive(uint8_t len, uint8_t mode) { state |= this->mod->SPIsetRegValue(RADIOLIB_SX127X_REG_FIFO_ADDR_PTR, RADIOLIB_SX127X_FIFO_RX_BASE_ADDR_MAX); RADIOLIB_ASSERT(state); + // 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) { + mode = RADIOLIB_SX127X_RXSINGLE; + state = this->mod->SPIsetRegValue(RADIOLIB_SX127X_REG_SYMB_TIMEOUT_LSB, len); + } + } else if(modem == RADIOLIB_SX127X_FSK_OOK) { // set DIO pin mapping state = this->mod->SPIsetRegValue(RADIOLIB_SX127X_REG_DIO_MAPPING_1, RADIOLIB_SX127X_DIO0_PACK_PAYLOAD_READY, 7, 6); @@ -1237,6 +1244,28 @@ uint32_t SX127x::getTimeOnAir(size_t len) { } +uint32_t SX127x::calculateRxTimeout(uint8_t numSymbols, uint32_t timeoutUs) { + (void)numSymbols; // not used for these modules + // numSymbols += (109 / 4) + 1; + float symbolLength = (float) (uint32_t(1) << this->spreadingFactor) / (float) this->bandwidth; + numSymbols = timeoutUs / symbolLength + 1; + 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; + return(RADIOLIB_ERR_NONE); +} + +bool SX127x::isRxTimeout() { + uint16_t irq = getIRQFlags(); + bool rxTimedOut = irq & RADIOLIB_SX127X_CLEAR_IRQ_FLAG_RX_TIMEOUT; + return(rxTimedOut); +} + int16_t SX127x::setCrcFiltering(bool enable) { this->crcOn = enable; diff --git a/src/modules/SX127x/SX127x.h b/src/modules/SX127x/SX127x.h index 9ef2890fb..bbfb3db83 100644 --- a/src/modules/SX127x/SX127x.h +++ b/src/modules/SX127x/SX127x.h @@ -1039,6 +1039,28 @@ 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 + \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); + + /*! + \brief Create the flags that make up RxDone and RxTimeout used for receiving downlinks + \param irqFlags The flags for which IRQs must be triggered + \param irqMask Mask indicating which IRQ triggers a DIO + \returns \ref status_codes + */ + int16_t irqRxDoneRxTimeout(uint16_t &irqFlags, uint16_t &irqMask); + + /*! + \brief Check whether the IRQ bit for RxTimeout is set + \returns \ref RxTimeout IRQ is set + */ + bool isRxTimeout(); + /*! \brief Enable CRC filtering and generation. \param enable Set or unset CRC filtering and generation. diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index 65359b333..6ff20c69e 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -38,7 +38,7 @@ int16_t LoRaWANNode::restore() { // check the magic value Module* mod = this->phyLayer->getMod(); - uint16_t nvm_table_version = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_TABLE_VERSION_ID); + 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) { // // set default values for variables that are new or something // } @@ -58,7 +58,7 @@ int16_t LoRaWANNode::restore() { mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_NWK_SENC_KEY_ID), this->nwkSEncKey, RADIOLIB_AES128_BLOCK_SIZE); // get session parameters - this->rev = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION); + this->rev = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_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); @@ -217,6 +217,7 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe // send it state = this->phyLayer->transmit(joinRequestMsg, RADIOLIB_LORAWAN_JOIN_REQUEST_LEN); this->rxDelayStart = mod->hal->millis(); + RADIOLIB_DEBUG_PRINTLN("Join-request sent <-- Rx Delay start"); RADIOLIB_ASSERT(state); // configure Rx delay for join-accept message - these are re-configured once a valid join-request is received @@ -356,8 +357,8 @@ 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, - .len = sizeof(uint8_t), .payload = { this->rev }, + .len = sizeof(uint8_t), .repeat = 0x01 << RADIOLIB_LORAWAN_ADR_ACK_LIMIT_EXP, }; state = pushMacCommand(&cmd, &this->commandsUp); @@ -468,8 +469,8 @@ int16_t LoRaWANNode::saveSession() { Module* mod = this->phyLayer->getMod(); // store session configuration (MAC commands) - if(mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION) != this->rev) - mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_VERSION, this->rev); + 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_PERSISTENT_PARAM_LORAWAN_DEV_NONCE_ID) != this->devNonce) mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_DEV_NONCE_ID, this->devNonce); @@ -629,7 +630,7 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf Module* mod = this->phyLayer->getMod(); // check if sufficient time has elapsed since the last uplink - if(mod->hal->millis() - this->rxDelayStart < rxDelays[1]) { + 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 return(RADIOLIB_ERR_UPLINK_UNAVAILABLE); } @@ -687,12 +688,14 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf // 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; } } } @@ -747,25 +750,31 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf // check if we have some MAC commands to append if(foptsLen > 0) { - uint8_t foptsNum = this->commandsUp.numCommands; uint8_t foptsBuff[foptsBufSize]; - size_t idx = 0; - for (size_t i = 0; i < foptsNum; i++) { - LoRaWANMacCommand_t cmd = { .cid = 0, .len = 0, .payload = { 0 }, .repeat = 0, }; - popMacCommand(&cmd, &this->commandsUp, i); - if (cmd.cid == 0) { - break; + uint8_t* foptsPtr = foptsBuff; + + // append all MAC replies into fopts buffer + size_t i = 0; + for (; i < this->commandsUp.numCommands; i++) { + LoRaWANMacCommand_t cmd = this->commandsUp.commands[i]; + memcpy(foptsPtr, &cmd, 1 + cmd.len); + foptsPtr += cmd.len + 1; + } + RADIOLIB_DEBUG_PRINTLN("Uplink MAC payload (%d commands):", this->commandsUp.numCommands); + RADIOLIB_DEBUG_HEXDUMP(foptsBuff, foptsLen); + + // pop the commands from back to front + for (; i >= 0; i--) { + if(this->commandsUp.commands[i].repeat > 0) { + this->commandsUp.commands[i].repeat--; + } else { + deleteMacCommand(this->commandsUp.commands[i].cid, &this->commandsUp); } - foptsBuff[idx] = cmd.cid; - for(size_t i = 0; i < cmd.len; i++) { - foptsBuff[idx + 1 + i] = cmd.payload[i]; + if(i == 0) { + break; } - idx += cmd.len + 1; } - RADIOLIB_DEBUG_PRINTLN("Uplink MAC payload (%d commands):", foptsNum); - RADIOLIB_DEBUG_HEXDUMP(foptsBuff, foptsBufSize); - uplinkMsgLen = RADIOLIB_LORAWAN_FRAME_LEN(len, foptsLen); uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] |= foptsLen; @@ -860,22 +869,22 @@ int16_t LoRaWANNode::downlinkCommon() { RADIOLIB_ASSERT(state); } - // calculate the Rx timeout - // 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); - // create the masks that are required for receiving downlinks uint16_t irqFlags; uint16_t irqMask; this->phyLayer->irqRxDoneRxTimeout(irqFlags, irqMask); - downlinkAction = false; this->phyLayer->setPacketReceivedAction(LoRaWANNodeOnDownlinkAction); // perform listening in the two Rx windows for(uint8_t i = 0; i < 2; i++) { + downlinkAction = false; + + // calculate the Rx timeout + // 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); // wait for the start of the Rx window // the waiting duration is shortened a bit to cover any possible timing errors @@ -886,15 +895,15 @@ int16_t LoRaWANNode::downlinkCommon() { mod->hal->delay(waitLen); // open Rx window by starting receive with specified timeout - state = this->phyLayer->startReceive(timeoutMod, irqFlags, irqMask, 0); + state = this->phyLayer->startReceive(timeoutMod, irqFlags, irqMask, timeoutMod); RADIOLIB_DEBUG_PRINTLN("Opening Rx%d window (%d us timeout)... <-- Rx Delay end ", i+1, timeoutHost); // wait for the timeout to complete (and a small additional delay) mod->hal->delay(timeoutHost / 1000 + scanGuard / 2); RADIOLIB_DEBUG_PRINTLN("closing"); - // check if the DIO has fired indicating a timeout; no timeout means a receive is ongoing - if(!downlinkAction) { + // check if the IRQ bit for Rx Timeout is set + if(!this->phyLayer->isRxTimeout()) { break; } else if(i == 0) { @@ -907,15 +916,12 @@ int16_t LoRaWANNode::downlinkCommon() { findDataRate(this->rx2.drMax, &dataRate); state = this->phyLayer->setDataRate(dataRate); RADIOLIB_ASSERT(state); - timeoutHost = this->phyLayer->getTimeOnAir(0) + 2*scanGuard*1000; - timeoutMod = this->phyLayer->calculateRxTimeout(0, timeoutHost); - downlinkAction = false; } } // if we got here due to a timeout, stop ongoing activities - if(downlinkAction) { + if(this->phyLayer->isRxTimeout()) { this->phyLayer->standby(); // TODO check: this should be done automagically due to RxSingle? if(!this->FSK) { this->phyLayer->invertIQ(false); @@ -1113,8 +1119,8 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { while(remLen > 0) { LoRaWANMacCommand_t cmd = { .cid = *foptsPtr, - .len = (uint8_t)(remLen - 1), .payload = { 0 }, + .len = (uint8_t)(remLen - 1), .repeat = 0, }; memcpy(cmd.payload, foptsPtr + 1, cmd.len); @@ -1130,24 +1136,30 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { // if FOptsLen for the next uplink is larger than can be piggybacked onto an uplink, send separate uplink if(this->commandsUp.len > 15) { - uint8_t foptsNum = this->commandsUp.numCommands; size_t foptsBufSize = this->commandsUp.len; uint8_t foptsBuff[foptsBufSize]; - size_t idx = 0; - for(size_t i = 0; i < foptsNum; i++) { - LoRaWANMacCommand_t cmd = { .cid = 0, .len = 0, .payload = { 0 }, .repeat = 0, }; - popMacCommand(&cmd, &this->commandsUp, i); - if(cmd.cid == 0) { - break; + uint8_t* foptsPtr = foptsBuff; + // append all MAC replies into fopts buffer + size_t i = 0; + for (; i < this->commandsUp.numCommands; i++) { + LoRaWANMacCommand_t cmd = this->commandsUp.commands[i]; + memcpy(foptsPtr, &cmd, 1 + cmd.len); + foptsPtr += cmd.len + 1; + } + RADIOLIB_DEBUG_PRINTLN("Uplink MAC payload (%d commands):", this->commandsUp.numCommands); + RADIOLIB_DEBUG_HEXDUMP(foptsBuff, foptsBufSize); + + // pop the commands from back to front + for (; i >= 0; i--) { + if(this->commandsUp.commands[i].repeat > 0) { + this->commandsUp.commands[i].repeat--; + } else { + deleteMacCommand(this->commandsUp.commands[i].cid, &this->commandsUp); } - foptsBuff[idx] = cmd.cid; - for(size_t i = 1; i < cmd.len; i++) { - foptsBuff[idx + i] = cmd.payload[i]; + if(i == 0) { + break; } - idx += cmd.len + 1; } - RADIOLIB_DEBUG_PRINTLN("Uplink MAC payload (%d commands):", foptsNum); - RADIOLIB_DEBUG_HEXDUMP(foptsBuff, foptsBufSize); this->isMACPayload = true; this->uplink(foptsBuff, foptsBufSize, RADIOLIB_LORAWAN_FPORT_MAC_COMMAND); @@ -1484,24 +1496,6 @@ int16_t LoRaWANNode::pushMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQ return(RADIOLIB_ERR_NONE); } -int16_t LoRaWANNode::popMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQueue_t* queue, size_t index) { - if(queue->numCommands == 0) { - return(RADIOLIB_ERR_COMMAND_QUEUE_EMPTY); - } - - if(cmd) { - memcpy(cmd, &queue->commands[index], sizeof(LoRaWANMacCommand_t)); - } - - if(queue->commands[index].repeat > 0) { - queue->commands[index].repeat--; - } else { - deleteMacCommand(queue->commands[index].cid, queue); - } - - return(RADIOLIB_ERR_NONE); -} - int16_t LoRaWANNode::deleteMacCommand(uint8_t cid, LoRaWANMacCommandQueue_t* queue) { if(queue->numCommands == 0) { return(RADIOLIB_ERR_COMMAND_QUEUE_EMPTY); @@ -1512,7 +1506,7 @@ int16_t LoRaWANNode::deleteMacCommand(uint8_t cid, LoRaWANMacCommandQueue_t* que 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) { - memmove(&queue->commands[index], &queue->commands[index + 1], (RADIOLIB_LORAWAN_MAC_COMMAND_QUEUE_SIZE - index) * sizeof(LoRaWANMacCommand_t)); + memmove(&queue->commands[index], &queue->commands[index + 1], (RADIOLIB_LORAWAN_MAC_COMMAND_QUEUE_SIZE - index - 1) * sizeof(LoRaWANMacCommand_t)); } // set the latest element to all 0 memset(&queue->commands[RADIOLIB_LORAWAN_MAC_COMMAND_QUEUE_SIZE - 1], 0x00, sizeof(LoRaWANMacCommand_t)); @@ -1526,7 +1520,6 @@ int16_t LoRaWANNode::deleteMacCommand(uint8_t cid, LoRaWANMacCommandQueue_t* que size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { RADIOLIB_DEBUG_PRINTLN("exe MAC CID = %02x, len = %d", cmd->cid, cmd->len); - Module* mod = this->phyLayer->getMod(); if(cmd->cid >= RADIOLIB_LORAWAN_MAC_CMD_PROPRIETARY) { // TODO call user-provided callback for proprietary MAC commands? diff --git a/src/protocols/LoRaWAN/LoRaWAN.h b/src/protocols/LoRaWAN/LoRaWAN.h index ff1644ad6..8eab3c815 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.h +++ b/src/protocols/LoRaWAN/LoRaWAN.h @@ -295,12 +295,12 @@ struct LoRaWANMacCommand_t { /*! \brief The command ID */ uint8_t cid; - /*! \brief Length of the payload */ - uint8_t len; - /*! \brief Payload buffer (5 bytes is the longest possible) */ uint8_t payload[5]; + /*! \brief Length of the payload */ + uint8_t len; + /*! \brief Repetition counter (the command will be uplinked repeat + 1 times) */ uint8_t repeat; }; @@ -477,12 +477,12 @@ class LoRaWANNode { LoRaWANMacCommandQueue_t commandsUp = { .numCommands = 0, .len = 0, - .commands = { { .cid = 0, .len = 0, .payload = { 0 }, .repeat = 0, } }, + .commands = { { .cid = 0, .payload = { 0 }, .len = 0, .repeat = 0, } }, }; LoRaWANMacCommandQueue_t commandsDown = { .numCommands = 0, .len = 0, - .commands = { { .cid = 0, .len = 0, .payload = { 0 }, .repeat = 0, } }, + .commands = { { .cid = 0, .payload = { 0 }, .len = 0, .repeat = 0, } }, }; // the following is either provided by the network server (OTAA) @@ -570,9 +570,6 @@ class LoRaWANNode { // push MAC command to queue, done by copy int16_t pushMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQueue_t* queue); - - // pop MAC command from queue, done by copy unless CMD is NULL - int16_t popMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQueue_t* queue, size_t index); // delete a specific MAC command from queue, indicated by the command ID int16_t deleteMacCommand(uint8_t cid, LoRaWANMacCommandQueue_t* queue); From 6c093b249169e365c3c6a779a0dcafc663a1d230 Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Thu, 2 Nov 2023 10:23:48 +0100 Subject: [PATCH 06/21] [ArduinoHal] fix clock drift calculation --- src/ArduinoHal.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ArduinoHal.cpp b/src/ArduinoHal.cpp index 6beefa036..99bdbd386 100644 --- a/src/ArduinoHal.cpp +++ b/src/ArduinoHal.cpp @@ -61,7 +61,7 @@ void inline ArduinoHal::delay(unsigned long ms) { #if !defined(RADIOLIB_CLOCK_DRIFT_MS) ::delay(ms); #else - ::delay(ms * (1000 - RADIOLIB_CLOCK_DRIFT_MS) / 1000); + ::delay(ms * 1000 / (1000 + RADIOLIB_CLOCK_DRIFT_MS)); #endif } @@ -69,7 +69,7 @@ void inline ArduinoHal::delayMicroseconds(unsigned long us) { #if !defined(RADIOLIB_CLOCK_DRIFT_MS) ::delayMicroseconds(us); #else - ::delayMicroseconds(us * (1000 - RADIOLIB_CLOCK_DRIFT_MS) / 1000); + ::delayMicroseconds(us * 1000 / (1000 + RADIOLIB_CLOCK_DRIFT_MS)); #endif } @@ -77,7 +77,7 @@ unsigned long inline ArduinoHal::millis() { #if !defined(RADIOLIB_CLOCK_DRIFT_MS) return(::millis()); #else - return(::millis() * (1000 - RADIOLIB_CLOCK_DRIFT_MS) / 1000); + return(::millis() * 1000 / (1000 + RADIOLIB_CLOCK_DRIFT_MS)); #endif } @@ -85,7 +85,7 @@ unsigned long inline ArduinoHal::micros() { #if !defined(RADIOLIB_CLOCK_DRIFT_MS) return(::micros()); #else - return(::micros() * (1000 - RADIOLIB_CLOCK_DRIFT_MS) / 1000); + return(::micros() * 1000 / (1000 + RADIOLIB_CLOCK_DRIFT_MS)); #endif } From 4138dacb19414152327796dacd4bbea2af79901e Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Fri, 3 Nov 2023 22:32:45 +0100 Subject: [PATCH 07/21] [LoRaWAN] Improve band & ADR logic, allow setting ADR, DR, subband, update examples --- .../LoRaWAN_End_Device/LoRaWAN_End_Device.ino | 33 +- .../LoRaWAN_End_Device_ABP.ino | 33 +- src/TypeDef.h | 5 + src/modules/SX126x/SX126x.cpp | 12 +- src/modules/SX126x/SX126x.h | 6 + src/modules/SX127x/SX127x.cpp | 6 +- src/protocols/LoRaWAN/LoRaWAN.cpp | 360 ++++++++++++------ src/protocols/LoRaWAN/LoRaWAN.h | 86 +++-- src/protocols/LoRaWAN/LoRaWANBands.cpp | 18 +- src/protocols/PhysicalLayer/PhysicalLayer.cpp | 4 + src/protocols/PhysicalLayer/PhysicalLayer.h | 6 + 11 files changed, 399 insertions(+), 170 deletions(-) diff --git a/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino b/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino index 63dfe2bf8..59a92bf43 100644 --- a/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino +++ b/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino @@ -87,17 +87,28 @@ void setup() { // and can be set to NULL // some frequency bands only use a subset of the available channels - // you can set the starting channel and their number - // for example, the following corresponds to US915 FSB2 in TTN + // 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.startChannel = 8; - node.numChannels = 8; + node.selectSubband(2); + node.selectSubband(8, 16); */ // now we can start the activation // this can take up to 20 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; + 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); + if(state == RADIOLIB_ERR_NONE) { Serial.println(F("success!")); } else { @@ -107,11 +118,11 @@ void setup() { } // after the device has been activated, - // network can be rejoined after device power cycle - // by calling "begin" + // 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.begin(); + state = node.restore(); if(state == RADIOLIB_ERR_NONE) { Serial.println(F("success!")); } else { @@ -178,6 +189,12 @@ void loop() { 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_ABP/LoRaWAN_End_Device_ABP.ino b/examples/LoRaWAN/LoRaWAN_End_Device_ABP/LoRaWAN_End_Device_ABP.ino index 2f1d4f184..91b49d716 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 @@ -83,16 +83,27 @@ void setup() { // and can be set to NULL // some frequency bands only use a subset of the available channels - // you can set the starting channel and their number - // for example, the following corresponds to US915 FSB2 in TTN + // 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.startChannel = 8; - node.numChannels = 8; + node.selectSubband(2); + node.selectSubband(8, 16); + */ + + // if using EU868 on ABP in TTN, you need to set the SF for RX2 window manually + /* + node.rx2.drMax = 3; + */ + + // to start a LoRaWAN v1.1 session, the user should also provide + // fNwkSIntKey and sNwkSIntKey similar to nwkSKey and appSKey + /* + state = node.beginABP(devAddr, nwkSKey, appSKey, fNwkSIntKey, sNwkSIntKey); */ // start the device by directly providing the encryption keys and device address Serial.print(F("[LoRaWAN] Attempting over-the-air activation ... ")); - state = node.beginAPB(devAddr, (uint8_t*)nwkSKey, (uint8_t*)appSKey); + state = node.beginABP(devAddr, nwkSKey, appSKey); if(state == RADIOLIB_ERR_NONE) { Serial.println(F("success!")); } else { @@ -102,11 +113,11 @@ void setup() { } // after the device has been activated, - // network can be rejoined after device power cycle - // by calling "begin" + // 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.begin(); + state = node.restore(); if(state == RADIOLIB_ERR_NONE) { Serial.println(F("success!")); } else { @@ -173,6 +184,12 @@ void loop() { 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); } diff --git a/src/TypeDef.h b/src/TypeDef.h index 0ebcbbb17..2e16145c0 100644 --- a/src/TypeDef.h +++ b/src/TypeDef.h @@ -553,6 +553,11 @@ */ #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/modules/SX126x/SX126x.cpp b/src/modules/SX126x/SX126x.cpp index 7f3e23853..ea50e8434 100644 --- a/src/modules/SX126x/SX126x.cpp +++ b/src/modules/SX126x/SX126x.cpp @@ -1443,13 +1443,17 @@ uint32_t SX126x::calculateRxTimeout(uint8_t numSymbols, uint32_t timeoutUs) { } int16_t SX126x::irqRxDoneRxTimeout(uint16_t &irqFlags, uint16_t &irqMask) { - irqFlags = 0b0000000000000010; // RxDone - irqMask = 0b0000000000000010; - irqFlags |= 0b0000001000000000; // RxTimeout - irqMask |= 0b0000001000000000; + irqFlags = RADIOLIB_SX126X_IRQ_RX_DEFAULT; // flags that can appear in the IRQ register + irqMask = RADIOLIB_SX126X_IRQ_RX_DONE | RADIOLIB_SX126X_IRQ_TIMEOUT; // flags that will trigger DIO0 return(RADIOLIB_ERR_NONE); } +bool SX126x::isRxTimeout() { + uint16_t irq = getIrqStatus(); + bool rxTimedOut = irq & RADIOLIB_SX126X_IRQ_TIMEOUT; + return(rxTimedOut); +} + int16_t SX126x::implicitHeader(size_t len) { return(setHeaderType(RADIOLIB_SX126X_LORA_HEADER_IMPLICIT, len)); } diff --git a/src/modules/SX126x/SX126x.h b/src/modules/SX126x/SX126x.h index ffef9fbdc..5e1945b4a 100644 --- a/src/modules/SX126x/SX126x.h +++ b/src/modules/SX126x/SX126x.h @@ -965,6 +965,12 @@ class SX126x: public PhysicalLayer { */ int16_t irqRxDoneRxTimeout(uint16_t &irqFlags, uint16_t &irqMask); + /*! + \brief Check whether the IRQ bit for RxTimeout is set + \returns \ref RxTimeout IRQ is set + */ + bool isRxTimeout(); + /*! \brief Set implicit header mode for future reception/transmission. \param len Payload length in bytes. diff --git a/src/modules/SX127x/SX127x.cpp b/src/modules/SX127x/SX127x.cpp index 3f41c6186..8d2c7a73c 100644 --- a/src/modules/SX127x/SX127x.cpp +++ b/src/modules/SX127x/SX127x.cpp @@ -1245,10 +1245,8 @@ uint32_t SX127x::getTimeOnAir(size_t len) { } uint32_t SX127x::calculateRxTimeout(uint8_t numSymbols, uint32_t timeoutUs) { - (void)numSymbols; // not used for these modules - // numSymbols += (109 / 4) + 1; - float symbolLength = (float) (uint32_t(1) << this->spreadingFactor) / (float) this->bandwidth; - numSymbols = timeoutUs / symbolLength + 1; + (void)timeoutUs; + numSymbols = 20; return(numSymbols); } diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index 6ff20c69e..22d476e31 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -174,7 +174,7 @@ int16_t LoRaWANNode::restoreChannels() { } #endif -int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKey, uint8_t* appKey, bool force) { +int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKey, uint8_t* appKey, uint8_t drJoinSubband, bool force) { // check if we actually need to send the join request Module* mod = this->phyLayer->getMod(); @@ -191,7 +191,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); + state = this->selectChannelsJR(this->devNonce, drJoinSubband); RADIOLIB_ASSERT(state); // configure for uplink with default configuration @@ -617,16 +617,16 @@ int16_t LoRaWANNode::saveChannels() { #if defined(RADIOLIB_BUILD_ARDUINO) -int16_t LoRaWANNode::uplink(String& str, uint8_t port, bool isConfirmed, bool adrEnabled) { - return(this->uplink(str.c_str(), port, isConfirmed, adrEnabled)); +int16_t LoRaWANNode::uplink(String& str, uint8_t port, bool isConfirmed) { + return(this->uplink(str.c_str(), port, isConfirmed)); } #endif -int16_t LoRaWANNode::uplink(const char* str, uint8_t port, bool isConfirmed, bool adrEnabled) { - return(this->uplink((uint8_t*)str, strlen(str), port, isConfirmed, adrEnabled)); +int16_t LoRaWANNode::uplink(const char* str, uint8_t port, bool isConfirmed) { + return(this->uplink((uint8_t*)str, strlen(str), port, isConfirmed)); } -int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConfirmed, bool adrEnabled) { +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 @@ -734,7 +734,7 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf // length of fopts will be added later uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] = 0x00; - if(adrEnabled) { + if(this->adrEnabled) { uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] |= RADIOLIB_LORAWAN_FCTRL_ADR_ENABLED; if(adrAckReq) { uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] |= RADIOLIB_LORAWAN_FCTRL_ADR_ACK_REQ; @@ -1284,22 +1284,20 @@ int16_t LoRaWANNode::setPhyProperties() { } int16_t LoRaWANNode::setupChannels(uint8_t* cfList) { - uint8_t num = 0; - LoRaWANChannel_t chnl; + size_t num = 0; // in case of frequency list-type band, copy the default TX channels into the available channels, with RX1 = TX - if(this->band->cfListType == RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES) { - for(uint8_t i = 0; i < 3; i++) { - chnl = this->band->txFreqs[i]; - if(chnl.enabled) { - availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num] = chnl; - availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][num] = chnl; - RADIOLIB_DEBUG_PRINTLN("Channel UL/DL %d frequency = %f MHz", num, availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].freq); - num++; - } + if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) { + // copy the default defined channels into the first slots + for(; num < 3 && this->band->txFreqs[num].enabled; num++) { + availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num] = this->band->txFreqs[num]; + availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][num] = this->band->txFreqs[num]; + RADIOLIB_DEBUG_PRINTLN("Channel UL/DL %d frequency = %f MHz", availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].idx, 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) { - for(uint8_t i = 0; i < 5; i++) { + 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); @@ -1308,97 +1306,175 @@ int16_t LoRaWANNode::setupChannels(uint8_t* cfList) { chnl.drMax = this->band->txFreqs[0].drMax; // drMax is equal for all channels availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num] = chnl; availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][num] = chnl; - RADIOLIB_DEBUG_PRINTLN("Channel UL/DL %d frequency = %f MHz", num, availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].freq); - num++; + RADIOLIB_DEBUG_PRINTLN("Channel UL/DL %d frequency = %f MHz", chnl.idx, availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].freq); } } - } else { // RADIOLIB_LORAWAN_CFLIST_TYPE_MASK - 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(uint8_t i = 0; i < 5; i++) { - uint16_t mask = LoRaWANNode::ntoh(&cfList[2*i]); - RADIOLIB_DEBUG_PRINTLN("mask[%d] = 0x%04x", i, mask); - for(uint8_t j = 0; j < 16; j++) { - // 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 + 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++; + } - if(mask & (1UL << j)) { - if(chSpan >= this->band->numTxSpans || chNum >= this->band->txSpans[chSpan].numChannels) { - RADIOLIB_DEBUG_PRINTLN("channel bitmask overrun!"); - return(RADIOLIB_ERR_UNKNOWN); + 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; + 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, availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].freq); + num++; } - chnl.enabled = true; - chnl.idx = i*16 + j; - 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; - RADIOLIB_DEBUG_PRINTLN("Channel UL %d frequency = %f MHz", num, availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].freq); - num++; + chNum++; } - chNum++; } } } return(RADIOLIB_ERR_NONE); } -int16_t LoRaWANNode::selectChannelsJR(uint16_t devNonce) { +int16_t LoRaWANNode::selectSubband(uint8_t idx) { + int16_t state = this->selectSubband((idx - 1) * 8, idx * 8); + return(state); +} + +int16_t LoRaWANNode::selectSubband(uint8_t startChannel, uint8_t endChannel) { + 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; + 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); + } + + 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; + 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); + } + return(RADIOLIB_ERR_NONE); +} + +int16_t LoRaWANNode::selectChannelsJR(uint16_t devNonce, uint8_t drJoinSubband) { LoRaWANChannel_t channelUp; - LoRaWANChannel_t channelDn; - if(this->band->cfListType == RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES) { - // count the number of available channels for a join-request + LoRaWANChannel_t channelDown; + uint8_t drUp; + uint8_t drDown; + 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(uint8_t i = 0; i < 3; i++) { - if(this->band->txFreqs[i].idx != RADIOLIB_LORAWAN_CHANNEL_INDEX_NONE) { + for(size_t i = 0; i < 3; i++) { + if(this->band->txFreqs[i].enabled) { numJRChannels++; } - if(this->band->txJoinReq[i].idx != RADIOLIB_LORAWAN_CHANNEL_INDEX_NONE) { + if(this->band->txJoinReq[i].enabled) { numJRChannels++; } } - uint8_t channelId = devNonce % numJRChannels; // cycle through channels (seed with devNonce) - if(channelId < 3) { - channelUp = this->band->txFreqs[channelId]; - } else { - channelUp = this->band->txJoinReq[channelId - 3]; + + // 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]; + break; + } + if(this->band->txJoinReq[i].idx == channelId) { + channelUp = this->band->txJoinReq[i]; + } + } + + // 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; + } 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; + } + } + if(drJoinSubband == RADIOLIB_LORAWAN_DATA_RATE_UNUSED) { + drUp = int((channelUp.drMax + channelUp.drMin) / 2); } - channelDn = channelUp; // RX1 is equal to TX - // configure data rates for TX and RX1: for TX the (floored) average of min and max; for RX1 identical with base offset - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = int((channelUp.drMax + channelUp.drMin) / 2); - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = getDownlinkDataRate(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK], - this->rx1DrOffset, this->band->rx1DataRateBase, channelDn.drMin, channelDn.drMax); - } else { // RADIOLIB_LORAWAN_CFLIST_TYPE_MASK + // 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 channelUp.enabled = true; uint8_t numBlocks = this->band->txSpans[0].numChannels / 8; // calculate number of 8-channel blocks - uint8_t numBlockChannels = 8 + this->band->txSpans[1].numChannels > 0 ? 1 : 0; // add a 9th channel if there's a second span + 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 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 * numBlockChannels + channelID; + 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; - - channelDn.idx = blockID % this->band->rx1Span.numChannels; - channelDn.freq = this->band->rx1Span.freqStart + channelDn.idx*this->band->rx1Span.freqStep; - // configure data rates for TX and RX1: for TX the specified value for this band; for RX1 identical with base offset - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = this->band->txSpans[spanID].joinRequestDataRate; - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = getDownlinkDataRate(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK], - this->rx1DrOffset, this->band->rx1DataRateBase, channelDn.drMin, channelDn.drMax); + // 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); + } this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = channelUp; - this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = channelDn; + 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); } @@ -1418,14 +1494,20 @@ int16_t LoRaWANNode::selectChannels() { break; } } + if(numChannels == 0) { + RADIOLIB_DEBUG_PRINTLN("There are no channels defined - are you in ABP mode with no defined subband?"); + return(RADIOLIB_ERR_INVALID_CHANNEL); + } // 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]; - // in case of frequency list-type band, downlink is equal to uplink, otherwise retrieve `modulo` numChannels - if(this->band->cfListType == RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES) { + 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]; - } else { // RADIOLIB_LORAWAN_CFLIST_TYPE_MASK + + } 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; channelDn.idx = this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK].idx % this->band->rx1Span.numChannels; @@ -1433,10 +1515,34 @@ 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; + } return(RADIOLIB_ERR_NONE); } +int16_t LoRaWANNode::setDatarate(uint8_t drUp) { + if(drUp < this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK].drMin) { + 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; + + return(RADIOLIB_ERR_NONE); +} + +void LoRaWANNode::setADR(bool enable) { + this->adrEnabled = enable; +} + int16_t LoRaWANNode::findDataRate(uint8_t dr, DataRate_t* dataRate) { uint8_t dataRateBand = this->band->dataRates[dr]; @@ -1550,27 +1656,23 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { case(RADIOLIB_LORAWAN_MAC_CMD_LINK_ADR): { // get the ADR configuration - // TODO all these configuration should only be set if all ACKs are set, otherwise retain previous state - uint8_t dr = (cmd->payload[0] & 0xF0) >> 4; + // TODO all these configuration should only be set if all ACKs are set, otherwise retain previous state (per spec) + uint8_t drUp = (cmd->payload[0] & 0xF0) >> 4; uint8_t txPower = cmd->payload[0] & 0x0F; 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", dr, txPower, chMask, chMaskCntl, nbTrans); + RADIOLIB_DEBUG_PRINTLN("ADR REQ: dataRate = %d, txPower = %d, chMask = 0x%02x, chMaskCntl = %02x, nbTrans = %d", drUp, txPower, chMask, chMaskCntl, nbTrans); // apply the configuration uint8_t drAck = 0; - if(dr == 0x0F) { + if(drUp == 0x0F) { drAck = 1; - } else if (this->band->dataRates[dr] != RADIOLIB_LORAWAN_DATA_RATE_UNUSED) { - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = dr; - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = dr + this->band->rx1DataRateBase - this->rx1DrOffset; - if(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] < this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK].drMin) { - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK].drMin; - } else if(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] > this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK].drMax) { - this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK].drMax; - } - + } 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); + this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = drUp; + this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = drDown; drAck = 1; } @@ -1587,28 +1689,66 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { this->txPwrCur = pwr; } - this->nbTrans = nbTrans; - // TODO implement channel mask uint8_t chMaskAck = 1; - (void)chMaskCntl; - if(this->band->cfListType == RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES) { - for(uint8_t i = 0; i < 16; i++) { - // check if this channel ID should be enabled - RADIOLIB_DEBUG_PRINTLN("ADR channel %d: %d --> %d", i, 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].enabled == false) { - chMaskAck = 0; - break; + 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", i, 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; + } + + } 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; } - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = true; - } else { - this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = false; } + } - } else { + } else { // RADIOLIB_LORAWAN_BAND_FIXED + // delete any prior ADR responses from the uplink queue, but do not care about 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 chNum = chMaskCntl*16; + uint8_t chSpan = 0; + for(size_t i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; 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); + } + 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][i] = 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); + } + 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; // send the reply cmd->len = 1; @@ -1679,8 +1819,8 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { // find first empty channel and configure this as the new channel if(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx == 0) { 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].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; diff --git a/src/protocols/LoRaWAN/LoRaWAN.h b/src/protocols/LoRaWAN/LoRaWAN.h index 8eab3c815..0dd60f2af 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.h +++ b/src/protocols/LoRaWAN/LoRaWAN.h @@ -69,8 +69,8 @@ #define RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK (0x01 << 0) #define RADIOLIB_LORAWAN_CHANNEL_DIR_BOTH (0x02 << 0) #define RADIOLIB_LORAWAN_CHANNEL_DIR_NONE (0x03 << 0) -#define RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES (0) -#define RADIOLIB_LORAWAN_CFLIST_TYPE_MASK (1) +#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 < 0) @@ -239,6 +239,9 @@ struct LoRaWANChannelSpan_t { \brief Structure to save information about LoRaWAN band */ struct LoRaWANBand_t { + /*! \brief Whether the channels are fixed per specification, or dynamically allocated through the network (plus defaults) */ + uint8_t bandType; + /*! \brief Array of allowed maximum payload lengths for each data rate */ uint8_t payloadLenMax[RADIOLIB_LORAWAN_CHANNEL_NUM_DATARATES]; @@ -248,9 +251,6 @@ struct LoRaWANBand_t { /*! \brief Number of power steps in this band */ int8_t powerNumSteps; - /*! \brief Whether the optional channels are defined as list of frequencies or bit mask */ - uint8_t cfListType; - /*! \brief A set of default uplink (TX) channels for frequency-type bands */ LoRaWANChannel_t txFreqs[3]; @@ -357,13 +357,6 @@ class LoRaWANNode { \returns \ref status_codes */ int16_t restore(); - - /*! - \brief Restore frame counter for uplinks from persistent storage. - Note that the usable frame counter width is 'only' 30 bits for highly efficient wear-levelling. - \returns \ref status_codes - */ - int16_t restoreFcntUp(); #endif /*! @@ -373,10 +366,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 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, bool force = false); + 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); /*! \brief Join network by performing activation by personalization. @@ -398,23 +393,15 @@ class LoRaWANNode { */ int16_t saveSession(); - /*! - \brief Save the current uplink frame counter. - Note that the usable frame counter width is 'only' 30 bits for highly efficient wear-levelling. - \returns \ref status_codes - */ - int16_t saveFcntUp(); - #if defined(RADIOLIB_BUILD_ARDUINO) /*! \brief Send a message to the server. \param str Address of Arduino String that will be transmitted. \param port Port number to send the message to. \param isConfirmed Whether to send a confirmed uplink or not. - \param adrEnabled Whether ADR is enabled or not. \returns \ref status_codes */ - int16_t uplink(String& str, uint8_t port, bool isConfirmed = false, bool adrEnabled = true); + int16_t uplink(String& str, uint8_t port, bool isConfirmed = false); #endif /*! @@ -422,10 +409,9 @@ class LoRaWANNode { \param str C-string that will be transmitted. \param port Port number to send the message to. \param isConfirmed Whether to send a confirmed uplink or not. - \param adrEnabled Whether ADR is enabled or not. \returns \ref status_codes */ - int16_t uplink(const char* str, uint8_t port, bool isConfirmed = false, bool adrEnabled = true); + int16_t uplink(const char* str, uint8_t port, bool isConfirmed = false); /*! \brief Send a message to the server. @@ -433,10 +419,9 @@ class LoRaWANNode { \param len Length of the data. \param port Port number to send the message to. \param isConfirmed Whether to send a confirmed uplink or not. - \param adrEnabled Whether ADR is enabled or not. \returns \ref status_codes */ - int16_t uplink(uint8_t* data, size_t len, uint8_t port, bool isConfirmed = false, bool adrEnabled = true); + 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 @@ -468,6 +453,34 @@ class LoRaWANNode { */ void setDeviceStatus(uint8_t battLevel); + /*! + \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); + + /*! + \brief Toggle ADR to on or off + \param enable Whether to disable ADR or not + */ + void setADR(bool enable = true); + + /*! + \brief Select a single subband (8 channels) for fixed bands such as US915 + \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 + \param startChannel The first channel of the band to be used (inclusive) + \param endChannel The last channel of the band to be used (exclusive) + \returns \ref status_codes + */ + int16_t selectSubband(uint8_t startChannel, uint8_t endChannel); + #if !defined(RADIOLIB_GODMODE) private: #endif @@ -511,6 +524,9 @@ class LoRaWANNode { uint32_t confFcntDown = RADIOLIB_LORAWAN_FCNT_NONE; uint32_t adrFcnt = 0; + // ADR is enabled by default + bool adrEnabled = true; + // available channel frequencies from list passed during OTA activation LoRaWANChannel_t availableChannels[2][RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS] = { { 0 }, { 0 } }; @@ -535,6 +551,22 @@ class LoRaWANNode { // indicates whether an uplink has MAC commands as payload bool isMACPayload = false; +#if !defined(RADIOLIB_EEPROM_UNSUPPORTED) + /*! + \brief Save the current uplink frame counter. + Note that the usable frame counter width is 'only' 30 bits for highly efficient wear-levelling. + \returns \ref status_codes + */ + int16_t saveFcntUp(); + + /*! + \brief Restore frame counter for uplinks from persistent storage. + Note that the usable frame counter width is 'only' 30 bits for highly efficient wear-levelling. + \returns \ref status_codes + */ + int16_t restoreFcntUp(); +#endif + // method to generate message integrity code uint32_t generateMIC(uint8_t* msg, size_t len, uint8_t* key); @@ -551,7 +583,7 @@ class LoRaWANNode { int16_t setupChannels(uint8_t* cfList); // select a set of semi-random TX/RX channels for the join-request and -accept message - int16_t selectChannelsJR(uint16_t devNonce); + int16_t selectChannelsJR(uint16_t devNonce, uint8_t drJoinSubband); // select a set of random TX/RX channels for up- and downlink int16_t selectChannels(); diff --git a/src/protocols/LoRaWAN/LoRaWANBands.cpp b/src/protocols/LoRaWAN/LoRaWANBands.cpp index efc9c6221..91f7d965d 100644 --- a/src/protocols/LoRaWAN/LoRaWANBands.cpp +++ b/src/protocols/LoRaWAN/LoRaWANBands.cpp @@ -13,10 +13,10 @@ uint8_t getDownlinkDataRate(uint8_t uplink, uint8_t offset, uint8_t base, uint8_ } const LoRaWANBand_t EU868 = { + .bandType = RADIOLIB_LORAWAN_BAND_DYNAMIC, .payloadLenMax = { 59, 59, 59, 123, 230, 230, 230, 230, 0, 0, 0, 0, 0, 0, 0 }, .powerMax = 16, .powerNumSteps = 7, - .cfListType = RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES, .txFreqs = { { .enabled = true, .idx = 0, .freq = 868.100, .drMin = 0, .drMax = 5}, { .enabled = true, .idx = 1, .freq = 868.300, .drMin = 0, .drMax = 5}, @@ -55,10 +55,10 @@ const LoRaWANBand_t EU868 = { }; const LoRaWANBand_t US915 = { + .bandType = RADIOLIB_LORAWAN_BAND_FIXED, .payloadLenMax = { 19, 61, 133, 250, 250, 0, 0, 0, 41, 117, 230, 230, 230, 230, 0 }, .powerMax = 30, .powerNumSteps = 10, - .cfListType = RADIOLIB_LORAWAN_CFLIST_TYPE_MASK, .txFreqs = { RADIOLIB_LORAWAN_CHANNEL_NONE, RADIOLIB_LORAWAN_CHANNEL_NONE, @@ -118,10 +118,10 @@ const LoRaWANBand_t US915 = { }; const LoRaWANBand_t CN780 = { + .bandType = RADIOLIB_LORAWAN_BAND_DYNAMIC, .payloadLenMax = { 59, 59, 59, 123, 230, 230, 250, 230, 0, 0, 0, 0, 0, 0, 0 }, .powerMax = 12, .powerNumSteps = 5, - .cfListType = RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES, .txFreqs = { { .enabled = true, .idx = 0, .freq = 779.500, .drMin = 0, .drMax = 5}, { .enabled = true, .idx = 1, .freq = 779.700, .drMin = 0, .drMax = 5}, @@ -160,10 +160,10 @@ const LoRaWANBand_t CN780 = { }; const LoRaWANBand_t EU433 = { + .bandType = RADIOLIB_LORAWAN_BAND_DYNAMIC, .payloadLenMax = { 59, 59, 59, 123, 230, 230, 230, 230, 0, 0, 0, 0, 0, 0, 0 }, .powerMax = 12, .powerNumSteps = 5, - .cfListType = RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES, .txFreqs = { { .enabled = true, .idx = 0, .freq = 433.175, .drMin = 0, .drMax = 5}, { .enabled = true, .idx = 1, .freq = 433.375, .drMin = 0, .drMax = 5}, @@ -202,10 +202,10 @@ const LoRaWANBand_t EU433 = { }; const LoRaWANBand_t AU915 = { + .bandType = RADIOLIB_LORAWAN_BAND_FIXED, .payloadLenMax = { 59, 59, 59, 123, 230, 230, 230, 0, 41, 117, 230, 230, 230, 230, 0 }, .powerMax = 30, .powerNumSteps = 10, - .cfListType = RADIOLIB_LORAWAN_CFLIST_TYPE_MASK, .txFreqs = { RADIOLIB_LORAWAN_CHANNEL_NONE, RADIOLIB_LORAWAN_CHANNEL_NONE, @@ -265,10 +265,10 @@ const LoRaWANBand_t AU915 = { }; const LoRaWANBand_t CN500 = { + .bandType = RADIOLIB_LORAWAN_BAND_FIXED, .payloadLenMax = { 59, 59, 59, 123, 230, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, .powerMax = 19, .powerNumSteps = 7, - .cfListType = RADIOLIB_LORAWAN_CFLIST_TYPE_MASK, .txFreqs = { RADIOLIB_LORAWAN_CHANNEL_NONE, RADIOLIB_LORAWAN_CHANNEL_NONE, @@ -321,10 +321,10 @@ const LoRaWANBand_t CN500 = { }; const LoRaWANBand_t AS923 = { + .bandType = RADIOLIB_LORAWAN_BAND_DYNAMIC, .payloadLenMax = { 59, 59, 59, 123, 230, 230, 230, 230, 0, 0, 0, 0, 0, 0, 0 }, .powerMax = 16, .powerNumSteps = 7, - .cfListType = RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES, .txFreqs = { { .enabled = true, .idx = 0, .freq = 923.200, .drMin = 0, .drMax = 5}, { .enabled = true, .idx = 1, .freq = 923.400, .drMin = 0, .drMax = 5}, @@ -363,10 +363,10 @@ const LoRaWANBand_t AS923 = { }; const LoRaWANBand_t KR920 = { + .bandType = RADIOLIB_LORAWAN_BAND_DYNAMIC, .payloadLenMax = { 59, 59, 59, 123, 230, 230, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, .powerMax = 14, .powerNumSteps = 7, - .cfListType = RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES, .txFreqs = { { .enabled = true, .idx = 0, .freq = 922.100, .drMin = 0, .drMax = 5}, { .enabled = true, .idx = 1, .freq = 922.300, .drMin = 0, .drMax = 5}, @@ -405,10 +405,10 @@ const LoRaWANBand_t KR920 = { }; const LoRaWANBand_t IN865 = { + .bandType = RADIOLIB_LORAWAN_BAND_DYNAMIC, .payloadLenMax = { 59, 59, 59, 123, 230, 230, 230, 230, 0, 0, 0, 0, 0, 0, 0 }, .powerMax = 30, .powerNumSteps = 10, - .cfListType = RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES, .txFreqs = { { .enabled = true, .idx = 0, .freq = 865.0625, .drMin = 0, .drMax = 5}, { .enabled = true, .idx = 1, .freq = 865.4025, .drMin = 0, .drMax = 5}, diff --git a/src/protocols/PhysicalLayer/PhysicalLayer.cpp b/src/protocols/PhysicalLayer/PhysicalLayer.cpp index 387001a0a..d6c107d57 100644 --- a/src/protocols/PhysicalLayer/PhysicalLayer.cpp +++ b/src/protocols/PhysicalLayer/PhysicalLayer.cpp @@ -306,6 +306,10 @@ int16_t PhysicalLayer::irqRxDoneRxTimeout(uint16_t &irqFlags, uint16_t &irqMask) return(RADIOLIB_ERR_UNSUPPORTED); } +bool PhysicalLayer::isRxTimeout() { + return(false); +} + int16_t PhysicalLayer::startChannelScan() { return(RADIOLIB_ERR_UNSUPPORTED); } diff --git a/src/protocols/PhysicalLayer/PhysicalLayer.h b/src/protocols/PhysicalLayer/PhysicalLayer.h index 4ffeb89ce..d7eccc131 100644 --- a/src/protocols/PhysicalLayer/PhysicalLayer.h +++ b/src/protocols/PhysicalLayer/PhysicalLayer.h @@ -326,6 +326,12 @@ class PhysicalLayer { */ virtual int16_t irqRxDoneRxTimeout(uint16_t &irqFlags, uint16_t &irqMask); + /*! + \brief Check whether the IRQ bit for RxTimeout is set + \returns \ref RxTimeout IRQ is set + */ + virtual bool isRxTimeout(); + /*! \brief Interrupt-driven channel activity detection method. interrupt will be activated when packet is detected. Must be implemented in module class. From d540855ee9d38525ea5b0cdcd86288dce75c2271 Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Fri, 3 Nov 2023 22:45:23 +0100 Subject: [PATCH 08/21] [LoRaWAN] Fix EU868 coding rate, improve example --- .../LoRaWAN_End_Device/LoRaWAN_End_Device.ino | 1 + src/protocols/LoRaWAN/LoRaWANBands.cpp | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino b/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino index 59a92bf43..50badb088 100644 --- a/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino +++ b/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino @@ -131,6 +131,7 @@ void setup() { while(true); } */ + delay(5000); } // counter to keep track of transmitted packets diff --git a/src/protocols/LoRaWAN/LoRaWANBands.cpp b/src/protocols/LoRaWAN/LoRaWANBands.cpp index 91f7d965d..cfa01b901 100644 --- a/src/protocols/LoRaWAN/LoRaWANBands.cpp +++ b/src/protocols/LoRaWAN/LoRaWANBands.cpp @@ -36,13 +36,13 @@ const LoRaWANBand_t EU868 = { .rx1DataRateBase = 0, .rx2 = { .enabled = true, .idx = 0, .freq = 869.525, .drMin = 0, .drMax = 0 }, .dataRates = { - RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_11 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, - RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_250_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_7, + RADIOLIB_LORAWAN_DATA_RATE_SF_12 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_11 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_10 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_9 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_8 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, + RADIOLIB_LORAWAN_DATA_RATE_SF_7 | RADIOLIB_LORAWAN_DATA_RATE_BW_250_KHZ | RADIOLIB_LORAWAN_DATA_RATE_CR_4_5, RADIOLIB_LORAWAN_DATA_RATE_FSK_50_K, RADIOLIB_LORAWAN_DATA_RATE_UNUSED, RADIOLIB_LORAWAN_DATA_RATE_UNUSED, From 466aa71329cff35f6653389b5928fb5d9d053c9e Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Fri, 3 Nov 2023 23:27:46 +0100 Subject: [PATCH 09/21] [LoRaWAN] fix unused channel index --- src/protocols/LoRaWAN/LoRaWAN.cpp | 10 ++++++++-- src/protocols/LoRaWAN/LoRaWAN.h | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index 22d476e31..abd554ba4 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -1309,6 +1309,9 @@ int16_t LoRaWANNode::setupChannels(uint8_t* cfList) { RADIOLIB_DEBUG_PRINTLN("Channel UL/DL %d frequency = %f MHz", chnl.idx, availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].freq); } } + 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) { @@ -1344,6 +1347,9 @@ int16_t LoRaWANNode::setupChannels(uint8_t* cfList) { chNum++; } } + for(; chNum < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; chNum++) { + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chNum] = RADIOLIB_LORAWAN_CHANNEL_NONE; + } } } return(RADIOLIB_ERR_NONE); @@ -1694,7 +1700,7 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { 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", i, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled, (chMask >> i) & 0x01); + 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) { @@ -1817,7 +1823,7 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { 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 == 0) { + 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; diff --git a/src/protocols/LoRaWAN/LoRaWAN.h b/src/protocols/LoRaWAN/LoRaWAN.h index 0dd60f2af..5b936ed1b 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 < 0) +#define RADIOLIB_LORAWAN_CHANNEL_INDEX_NONE 0xFF // recommended default settings #define RADIOLIB_LORAWAN_RECEIVE_DELAY_1_MS (1000) From 3b466ca9196f04f10943102c0a7ec737442e061f Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Fri, 3 Nov 2023 23:45:52 +0100 Subject: [PATCH 10/21] [LoRaWAN] fix merge issue (deleted line) --- src/protocols/LoRaWAN/LoRaWAN.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/protocols/LoRaWAN/LoRaWAN.h b/src/protocols/LoRaWAN/LoRaWAN.h index 6ec964588..ab458b2a7 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.h +++ b/src/protocols/LoRaWAN/LoRaWAN.h @@ -370,6 +370,7 @@ class LoRaWANNode { int16_t restore(); #endif + /*! \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. \param difsSlots Num of CADs to estimate a clear CH. From 60f50e0a04992c1942ca5b18c51f5a449eb4dbbe Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Fri, 3 Nov 2023 23:52:32 +0100 Subject: [PATCH 11/21] [LoRaWAN] fix CSMA calling now incorrect function --- src/protocols/LoRaWAN/LoRaWAN.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index 6f37255a3..6edd19713 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -327,7 +327,6 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe this->rxDelays[1] = this->rxDelays[0] + 1000; // process CFlist if present - uint8_t cfList[RADIOLIB_LORAWAN_JOIN_ACCEPT_CFLIST_LEN] = { 0 }; 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); @@ -2095,7 +2094,7 @@ void LoRaWANNode::performCSMA() { RADIOLIB_DEBUG_PRINTLN("OCCUPIED CHANNEL DURING DIFS"); channelFreeDuringDIFS = false; // Channel is occupied during DIFS, hop to another. - this->setupChannels(); + this->selectChannels(); break; } } @@ -2107,7 +2106,7 @@ void LoRaWANNode::performCSMA() { if (performCAD()) { RADIOLIB_DEBUG_PRINTLN("OCCUPIED CHANNEL DURING BO"); // Channel is busy during CAD, hop to another and return to DIFS state again. - this->setupChannels(); + this->selectChannels(); break; // Exit loop. Go back to DIFS state. } BO--; // Decrement BO by one if channel is free From ccb28f3b7b554c1f59cdf85f3c85ca22e0947451 Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Sat, 4 Nov 2023 07:59:42 +0100 Subject: [PATCH 12/21] [LoRaWAN] fix include logic --- src/protocols/LoRaWAN/LoRaWAN.cpp | 128 +++++++++++++++--------------- src/protocols/LoRaWAN/LoRaWAN.h | 38 ++++----- 2 files changed, 85 insertions(+), 81 deletions(-) diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index 6edd19713..afddfab54 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -1,5 +1,4 @@ #include "LoRaWAN.h" -#include "LoRaWANBands.cpp" #include #if !defined(RADIOLIB_EXCLUDE_LORAWAN) @@ -19,6 +18,16 @@ static void LoRaWANNodeOnDownlinkAction(void) { downlinkAction = true; } +uint8_t getDownlinkDataRate(uint8_t uplink, uint8_t offset, uint8_t base, uint8_t min, uint8_t max) { + int8_t dr = uplink - offset + base; + if(dr < min) { + dr = min; + } else if (dr > max) { + dr = max; + } + return(dr); +} + LoRaWANNode::LoRaWANNode(PhysicalLayer* phy, const LoRaWANBand_t* band) { this->phyLayer = phy; this->band = band; @@ -29,18 +38,18 @@ LoRaWANNode::LoRaWANNode(PhysicalLayer* phy, const LoRaWANBand_t* band) { this->enableCSMA = false; } -#if !defined(RADIOLIB_EEPROM_UNSUPPORTED) -void LoRaWANNode::wipe() { - Module* mod = this->phyLayer->getMod(); - mod->hal->wipePersistentStorage(); -} - void LoRaWANNode::setCSMA(uint8_t backoffMax, uint8_t difsSlots, bool enableCSMA) { this->backoffMax = backoffMax; this->difsSlots = difsSlots; this->enableCSMA = enableCSMA; } +#if !defined(RADIOLIB_EEPROM_UNSUPPORTED) +void LoRaWANNode::wipe() { + Module* mod = this->phyLayer->getMod(); + mod->hal->wipePersistentStorage(); +} + int16_t LoRaWANNode::restore() { // check the magic value Module* mod = this->phyLayer->getMod(); @@ -314,7 +323,7 @@ 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]); @@ -375,7 +384,6 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe // 1.0 version, just derive the keys LoRaWANNode::hton(&keyDerivationBuff[RADIOLIB_LORAWAN_JOIN_ACCEPT_HOME_NET_ID_POS], this->homeNetId, 3); LoRaWANNode::hton(&keyDerivationBuff[RADIOLIB_LORAWAN_JOIN_ACCEPT_DEV_ADDR_POS], this->devNonce); - keyDerivationBuff[0] = RADIOLIB_LORAWAN_JOIN_ACCEPT_APP_S_KEY; RadioLibAES128Instance.init(nwkKey); RadioLibAES128Instance.encryptECB(keyDerivationBuff, RADIOLIB_AES128_BLOCK_SIZE, this->appSKey); @@ -656,7 +664,6 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf this->isMACPayload = false; } - // check if there are some MAC commands to piggyback (only when piggybacking onto a application-frame) uint8_t foptsLen = 0; size_t foptsBufSize = 0; @@ -804,7 +811,6 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf // encrypt the frame payload processAES(data, len, encKey, &uplinkMsg[RADIOLIB_LORAWAN_FRAME_PAYLOAD_POS(foptsLen)], this->fcntUp, RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK, 0x00, true); - // create blocks for MIC calculation uint8_t block0[RADIOLIB_AES128_BLOCK_SIZE] = { 0 }; block0[RADIOLIB_LORAWAN_BLOCK_MAGIC_POS] = RADIOLIB_LORAWAN_MIC_BLOCK_MAGIC; @@ -838,14 +844,14 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConf LoRaWANNode::hton(&uplinkMsg[uplinkMsgLen - sizeof(uint32_t)], micF); } + RADIOLIB_DEBUG_PRINTLN("uplinkMsg:"); + RADIOLIB_DEBUG_HEXDUMP(uplinkMsg, uplinkMsgLen); + // perform CSMA if enabled. if (enableCSMA) { performCSMA(); } - RADIOLIB_DEBUG_PRINTLN("uplinkMsg:"); - RADIOLIB_DEBUG_HEXDUMP(uplinkMsg, uplinkMsgLen); - // send it (without the MIC calculation blocks) state = this->phyLayer->transmit(&uplinkMsg[RADIOLIB_LORAWAN_FHDR_LEN_START_OFFS], uplinkMsgLen - RADIOLIB_LORAWAN_FHDR_LEN_START_OFFS); @@ -2009,6 +2015,50 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { return(0); } +// 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. +// https://resources.lora-alliance.org/technical-recommendations/tr013-1-0-0-csma +void LoRaWANNode::performCSMA() { + + // Compute initial random back-off. + // When BO is reduced to zero, the function returns and the frame is transmitted. + uint32_t BO = this->phyLayer->random(1, this->backoffMax + 1); + while (BO > 0) { + // DIFS: Check channel for DIFS_slots + bool channelFreeDuringDIFS = true; + for (uint8_t i = 0; i < this->difsSlots; i++) { + if (performCAD()) { + RADIOLIB_DEBUG_PRINTLN("OCCUPIED CHANNEL DURING DIFS"); + channelFreeDuringDIFS = false; + // Channel is occupied during DIFS, hop to another. + this->selectChannels(); + break; + } + } + // Start reducing BO counter if DIFS slot was free. + if (channelFreeDuringDIFS) { + // Continue decrementing BO with per each CAD reporting free channel. + while (BO > 0) { + if (performCAD()) { + RADIOLIB_DEBUG_PRINTLN("OCCUPIED CHANNEL DURING BO"); + // Channel is busy during CAD, hop to another and return to DIFS state again. + this->selectChannels(); + break; // Exit loop. Go back to DIFS state. + } + BO--; // Decrement BO by one if channel is free + } + } + } +} +bool LoRaWANNode::performCAD() { + int16_t state = this->phyLayer->scanChannel(); + if ((state == RADIOLIB_PREAMBLE_DETECTED) || (state == RADIOLIB_LORA_DETECTED)) { + return true; // Channel is busy + } + return false; // Channel is free +} + void LoRaWANNode::processAES(uint8_t* in, size_t len, uint8_t* key, uint8_t* out, uint32_t fcnt, uint8_t dir, uint8_t ctrId, bool counter) { // figure out how many encryption blocks are there size_t numBlocks = len/RADIOLIB_AES128_BLOCK_SIZE; @@ -2076,52 +2126,4 @@ void LoRaWANNode::hton(uint8_t* buff, T val, size_t size) { } } -// 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. -// https://resources.lora-alliance.org/technical-recommendations/tr013-1-0-0-csma -void LoRaWANNode::performCSMA() { - - // Compute initial random back-off. - // When BO is reduced to zero, the function returns and the frame is transmitted. - uint32_t BO = this->phyLayer->random(1, this->backoffMax + 1); - - while (BO > 0) { - // DIFS: Check channel for DIFS_slots - bool channelFreeDuringDIFS = true; - for (uint8_t i = 0; i < this->difsSlots; i++) { - if (performCAD()) { - RADIOLIB_DEBUG_PRINTLN("OCCUPIED CHANNEL DURING DIFS"); - channelFreeDuringDIFS = false; - // Channel is occupied during DIFS, hop to another. - this->selectChannels(); - break; - } - } - - // Start reducing BO counter if DIFS slot was free. - if (channelFreeDuringDIFS) { - // Continue decrementing BO with per each CAD reporting free channel. - while (BO > 0) { - if (performCAD()) { - RADIOLIB_DEBUG_PRINTLN("OCCUPIED CHANNEL DURING BO"); - // Channel is busy during CAD, hop to another and return to DIFS state again. - this->selectChannels(); - break; // Exit loop. Go back to DIFS state. - } - BO--; // Decrement BO by one if channel is free - } - } - } -} - -bool LoRaWANNode::performCAD() { - int16_t state = this->phyLayer->scanChannel(); - - if ((state == RADIOLIB_PREAMBLE_DETECTED) || (state == RADIOLIB_LORA_DETECTED)) { - return true; // Channel is busy - } - return false; // Channel is free -} - -#endif \ No newline at end of file +#endif diff --git a/src/protocols/LoRaWAN/LoRaWAN.h b/src/protocols/LoRaWAN/LoRaWAN.h index ab458b2a7..4d70b3089 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.h +++ b/src/protocols/LoRaWAN/LoRaWAN.h @@ -338,14 +338,16 @@ 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. + /*! + \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. */ + 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; @@ -370,14 +372,6 @@ class LoRaWANNode { int16_t restore(); #endif - /*! - \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. - \param difsSlots Num of CADs to estimate a clear CH. - \param enableCSMA enable/disable CSMA for LoRaWAN. - */ - void setCSMA(uint8_t backoffMax, uint8_t difsSlots, bool enableCSMA = false); - /*! \brief Join network by performing over-the-air activation. By this procedure, the device will perform an exchange with the network server and set all necessary configuration. @@ -500,6 +494,14 @@ class LoRaWANNode { */ 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. + \param difsSlots Num of CADs to estimate a clear CH. + \param enableCSMA enable/disable CSMA for LoRaWAN. + */ + void setCSMA(uint8_t backoffMax, uint8_t difsSlots, bool enableCSMA = false); + #if !defined(RADIOLIB_GODMODE) private: #endif @@ -627,6 +629,12 @@ class LoRaWANNode { // execute mac command, return the number of processed bytes for sequential processing size_t execMacCommand(LoRaWANMacCommand_t* cmd); + + // Performs CSMA as per LoRa Alliance Technical Reccomendation 13 (TR-013). + void performCSMA(); + + // perform a single CAD operation for the under SF/CH combination. Returns either busy or otherwise. + bool performCAD(); // 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); @@ -638,12 +646,6 @@ class LoRaWANNode { // host-to-network conversion method - takes data from host variable and and converts it to network packet endians template static void hton(uint8_t* buff, T val, size_t size = 0); - - // perform a single CAD operation for the under SF/CH combination. Returns either busy or otherwise. - bool performCAD(); - - // Performs CSMA as per LoRa Alliance Technical Reccomendation 13 (TR-013). - void performCSMA(); }; #endif From 5796bbe746f8284c380353d93e9cb261d43e94b5 Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Sun, 5 Nov 2023 00:09:19 +0100 Subject: [PATCH 13/21] [LoRaWAN] fix warnings, remove duplicate function --- src/protocols/LoRaWAN/LoRaWAN.cpp | 28 ++++++++++++++++---------- src/protocols/LoRaWAN/LoRaWANBands.cpp | 10 --------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index afddfab54..1ec6d570e 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -132,12 +132,15 @@ int16_t LoRaWANNode::restore() { int16_t LoRaWANNode::restoreFcntUp() { 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); + 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 buffSize = fcntBuffEnd - fcntBuffStart; + uint8_t fcntBuff[buffSize] = { 0 }; + mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID), fcntBuff, buffSize); // copy the two most significant bytes from the first two bytes - uint8_t bits_30_22 = fcntBuff[0]; - uint8_t bits_22_14 = fcntBuff[1]; + uint32_t bits_30_22 = (uint32_t)fcntBuff[0]; + uint32_t bits_22_14 = (uint32_t)fcntBuff[1]; // the next 7 bits must be retrieved from the byte to which was written most recently // this is the last byte that has its state bit (most significant bit) set equal to its predecessor @@ -149,19 +152,19 @@ int16_t LoRaWANNode::restoreFcntUp() { break; } } - uint8_t bits_14_7 = fcntBuff[idx-1] & 0x7F; + uint32_t bits_14_7 = (uint32_t)fcntBuff[idx-1] & 0x7F; // equally, the last 7 bits must be retrieved from the byte to which was written most recently // this is the last byte that has its state bit (most significant bit) set equal to its predecessor // we find the first byte that has its state bit different, and subtract one idx = 5; state = fcntBuff[idx] >> 7; - for(; idx < 30; idx++) { + for(; idx < buffSize; idx++) { if(fcntBuff[idx] >> 7 != state) { break; } } - uint8_t bits_7_0 = fcntBuff[idx-1] & 0x7F; + uint32_t bits_7_0 = (uint32_t)fcntBuff[idx-1] & 0x7F; this->fcntUp = (bits_30_22 << 22) | (bits_22_14 << 14) | (bits_14_7 << 7) | bits_7_0; @@ -1048,6 +1051,7 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { isConfirmingUp = true; LoRaWANNode::hton(&downlinkMsg[RADIOLIB_LORAWAN_BLOCK_CONF_FCNT_POS], (uint16_t)this->confFcntUp); } + (void)isConfirmingUp; RADIOLIB_DEBUG_PRINTLN("downlinkMsg:"); RADIOLIB_DEBUG_HEXDUMP(downlinkMsg, RADIOLIB_AES128_BLOCK_SIZE + downlinkMsgLen); @@ -1087,7 +1091,7 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { } } else if (fcnt16 <= fcntDownPrev) { uint16_t msb = (fcntDownPrev >> 16) + 1; // assume a rollover - fcnt32 |= (msb << 16); // add back the MSB part + fcnt32 |= ((uint32_t)msb << 16); // add back the MSB part } } @@ -1110,7 +1114,9 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { bool isConfirmedDown = false; if((downlinkMsg[RADIOLIB_LORAWAN_FHDR_LEN_START_OFFS] & 0xFE) == RADIOLIB_LORAWAN_MHDR_MTYPE_CONF_DATA_DOWN) { this->confFcntDown = this->aFcntDown; + isConfirmedDown = true; } + (void)isConfirmedDown; // check the address uint32_t addr = LoRaWANNode::ntoh(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_DEV_ADDR_POS]); @@ -1969,9 +1975,9 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { } break; case(RADIOLIB_LORAWAN_MAC_CMD_ADR_PARAM_SETUP): { - uint8_t limitExp = (cmd->payload[0] & 0xF0) >> 4; - uint8_t delayExp = cmd->payload[0] & 0x0F; - RADIOLIB_DEBUG_PRINTLN("ADR param setup: limitExp = %d, delayExp = %d", limitExp, delayExp); + 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); } break; diff --git a/src/protocols/LoRaWAN/LoRaWANBands.cpp b/src/protocols/LoRaWAN/LoRaWANBands.cpp index cfa01b901..8e36f9c2f 100644 --- a/src/protocols/LoRaWAN/LoRaWANBands.cpp +++ b/src/protocols/LoRaWAN/LoRaWANBands.cpp @@ -2,16 +2,6 @@ #if !defined(RADIOLIB_EXCLUDE_LORAWAN) -uint8_t getDownlinkDataRate(uint8_t uplink, uint8_t offset, uint8_t base, uint8_t min, uint8_t max) { - int8_t dr = uplink - offset + base; - if(dr < min) { - dr = min; - } else if (dr > max) { - dr = max; - } - return(dr); -} - const LoRaWANBand_t EU868 = { .bandType = RADIOLIB_LORAWAN_BAND_DYNAMIC, .payloadLenMax = { 59, 59, 59, 123, 230, 230, 230, 230, 0, 0, 0, 0, 0, 0, 0 }, From 3df866e23dfa52031b5a8f10dbc95cc45507a149 Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Wed, 8 Nov 2023 17:42:05 +0100 Subject: [PATCH 14/21] [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 | 272 ++++++++++++------ src/protocols/LoRaWAN/LoRaWAN.h | 115 +++++--- src/protocols/PhysicalLayer/PhysicalLayer.cpp | 3 +- src/protocols/PhysicalLayer/PhysicalLayer.h | 5 +- 13 files changed, 682 insertions(+), 263 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 50badb088..b5eaaf8c9 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 91b49d716..05198f5ff 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 000000000..6ccd484fb --- /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 000000000..29ec64fbd --- /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 a044e1115..0485fad8a 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 412db3545..28e55b796 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 976e90c57..0c2f9e208 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 e0dffe0c0..a00d6be72 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 fbc3d77fe..4fb28d7bb 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 1ec6d570e..8d61a8616 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) { - return(RADIOLIB_ERR_DATA_RATE_INVALID); + // 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 > this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK].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); } - 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 4d70b3089..5d0eacc15 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 d6c107d57..0bb634f93 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 d7eccc131..741cdb8ce 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 From 84bb8da036935599a5d5c73bc5726a8ccbab29b4 Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Fri, 10 Nov 2023 16:32:29 +0100 Subject: [PATCH 15/21] [LoRaWAN] improve examples --- .../LoRaWAN_End_Device_persistent.ino | 8 -------- .../LoRaWAN_End_Device_reference.ino | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) 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 6ccd484fb..43275a7ca 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 @@ -88,14 +88,6 @@ void setup() { 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); - } } 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 29ec64fbd..e72e977cf 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 @@ -152,7 +152,7 @@ void loop() { String strUp = "Hello World! #" + String(fcntUp); // send a confirmed uplink to port 10 every 64th frame - if(fcntUp / 64 == 0) { + if(fcntUp % 64 == 0) { state = node.uplink(strUp, 10, true); } else { state = node.uplink(strUp, 10); From 966ab6c650de2a8f5f00b5ca0863fc1819c053bd Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Sat, 11 Nov 2023 22:20:36 +0100 Subject: [PATCH 16/21] [LoRaWAN] add new keywords, add debug guard --- keywords.txt | 15 ++++++++++----- src/protocols/LoRaWAN/LoRaWAN.cpp | 12 +++++++----- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/keywords.txt b/keywords.txt index 4cd39fb45..a791ce517 100644 --- a/keywords.txt +++ b/keywords.txt @@ -124,7 +124,6 @@ setSyncWord KEYWORD2 setOutputPower KEYWORD2 setCurrentLimit KEYWORD2 setPreambleLength KEYWORD2 -invertPreamble KEYWORD2 setGain KEYWORD2 getFrequencyError KEYWORD2 getRSSI KEYWORD2 @@ -222,7 +221,6 @@ spectralScanStart KEYWORD2 spectralScanAbort KEYWORD2 spectralScanGetStatus KEYWORD2 spectralScanGetResult KEYWORD2 -setPaConfig KEYWORD2 # nRF24 setIrqAction KEYWORD2 @@ -291,12 +289,18 @@ setModem KEYWORD2 # LoRaWAN wipe KEYWORD2 -restoreOTAA KEYWORD2 +restore KEYWORD2 beginOTAA KEYWORD2 beginABP KEYWORD2 +saveSession KEYWORD2 uplink KEYWORD2 downlink KEYWORD2 -configureChannel KEYWORD2 +sendReceive KEYWORD2 +setDeviceStatus KEYWORD2 +setDatarate KEYWORD2 +setADR KEYWORD2 +selectSubband KEYWORD2 +setCSMA KEYWORD2 ####################################### # Constants (LITERAL1) @@ -408,4 +412,5 @@ RADIOLIB_ERR_COMMAND_QUEUE_EMPTY LITERAL1 RADIOLIB_ERR_COMMAND_QUEUE_ITEM_NOT_FOUND LITERAL1 RADIOLIB_ERR_JOIN_NONCE_INVALID LITERAL1 RADIOLIB_ERR_N_FCNT_DOWN_INVALID LITERAL1 -RADIOLIB_ERR_A_FCNT_DOWN_INVALID LITERAL1 \ No newline at end of file +RADIOLIB_ERR_A_FCNT_DOWN_INVALID LITERAL1 +RADIOLIB_ERR_DATA_RATE_INVALID LITERAL1 \ No newline at end of file diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index 8d61a8616..754cee6e7 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -65,11 +65,13 @@ int16_t LoRaWANNode::restore() { // check the magic value if(mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_MAGIC_ID) != RADIOLIB_LORAWAN_MAGIC) { - 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); + #if defined(RADIOLIB_DEBUG) + 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); + #endif // the magic value is not set, user will have to do perform the join procedure return(RADIOLIB_ERR_NETWORK_NOT_JOINED); } From 140d35af810806fccead2872233a4be504d4122a Mon Sep 17 00:00:00 2001 From: jgromes Date: Sun, 12 Nov 2023 13:27:34 +0100 Subject: [PATCH 17/21] [SX127x] Updated startReceive interface to be more in line with SX126x --- src/modules/SX127x/SX127x.cpp | 21 ++++++++++++--------- src/modules/SX127x/SX127x.h | 7 +++++-- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/modules/SX127x/SX127x.cpp b/src/modules/SX127x/SX127x.cpp index a00d6be72..cab32da9c 100644 --- a/src/modules/SX127x/SX127x.cpp +++ b/src/modules/SX127x/SX127x.cpp @@ -409,13 +409,6 @@ int16_t SX127x::startReceive(uint8_t len, uint8_t mode) { state |= this->mod->SPIsetRegValue(RADIOLIB_SX127X_REG_FIFO_ADDR_PTR, RADIOLIB_SX127X_FIFO_RX_BASE_ADDR_MAX); RADIOLIB_ASSERT(state); - // 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) && (this->spreadingFactor > 6)) { - mode = RADIOLIB_SX127X_RXSINGLE; - state = this->mod->SPIsetRegValue(RADIOLIB_SX127X_REG_SYMB_TIMEOUT_LSB, len); - } - } else if(modem == RADIOLIB_SX127X_FSK_OOK) { // set DIO pin mapping state = this->mod->SPIsetRegValue(RADIOLIB_SX127X_REG_DIO_MAPPING_1, RADIOLIB_SX127X_DIO0_PACK_PAYLOAD_READY, 7, 6); @@ -439,10 +432,20 @@ int16_t SX127x::startReceive(uint8_t len, uint8_t mode) { return(setMode(mode)); } -int16_t SX127x::startReceive(uint32_t mode, uint16_t irqFlags, uint16_t irqMask, size_t len) { +int16_t SX127x::startReceive(uint32_t timeout, uint16_t irqFlags, uint16_t irqMask, size_t len) { (void)irqFlags; (void)irqMask; - return(startReceive((uint8_t)len, (uint8_t)mode)); + uint8_t mode = RADIOLIB_SX127X_RXCONTINUOUS; + if(timeout != 0) { + // for non-zero timeout value, change mode to Rx single and set the timeout + mode = RADIOLIB_SX127X_RXSINGLE; + uint8_t msb_sym = (timeout > 0x3FF) ? 0x3 : (uint8_t)(timeout >> 8); + uint8_t lsb_sym = (timeout > 0x3FF) ? 0xFF : (uint8_t)(timeout & 0xFF); + int16_t state = this->mod->SPIsetRegValue(RADIOLIB_SX127X_REG_MODEM_CONFIG_2, msb_sym, 1, 0); + state |= this->mod->SPIsetRegValue(RADIOLIB_SX127X_REG_SYMB_TIMEOUT_LSB, lsb_sym); + RADIOLIB_ASSERT(state); + } + return(startReceive((uint8_t)len, mode)); } void SX127x::setDio0Action(void (*func)(void), uint32_t dir) { diff --git a/src/modules/SX127x/SX127x.h b/src/modules/SX127x/SX127x.h index 4fb28d7bb..ce82377f6 100644 --- a/src/modules/SX127x/SX127x.h +++ b/src/modules/SX127x/SX127x.h @@ -826,13 +826,16 @@ class SX127x: public PhysicalLayer { /*! \brief Interrupt-driven receive method, implemented for compatibility with PhysicalLayer. - \param mode Receive mode to be used. + \param timeout Receive mode type and/or raw timeout value in symbols. + When set to 0, the timeout will be infinite and the device will remain + in Rx mode until explicitly commanded to stop (Rx continuous mode). + When non-zero (maximum 1023), the device will be set to Rx single mode and timeout will be set. \param irqFlags Ignored. \param irqMask Ignored. \param len Expected length of packet to be received. Required for LoRa spreading factor 6. \returns \ref status_codes */ - int16_t startReceive(uint32_t mode, uint16_t irqFlags, uint16_t irqMask, size_t len); + int16_t startReceive(uint32_t timeout, uint16_t irqFlags, uint16_t irqMask, size_t len); /*! \brief Reads data that was received after calling startReceive method. When the packet length is not known in advance, From c170bc096fd85923b5a28589d62c8a09346f9c63 Mon Sep 17 00:00:00 2001 From: jgromes Date: Sun, 12 Nov 2023 13:28:06 +0100 Subject: [PATCH 18/21] [SX127x] Added public method to convert from bytes to symbols --- src/modules/SX127x/SX127x.cpp | 47 ++++++++++++++++++++++++----------- src/modules/SX127x/SX127x.h | 8 +++++- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/modules/SX127x/SX127x.cpp b/src/modules/SX127x/SX127x.cpp index cab32da9c..703b150f8 100644 --- a/src/modules/SX127x/SX127x.cpp +++ b/src/modules/SX127x/SX127x.cpp @@ -1231,28 +1231,45 @@ int16_t SX127x::variablePacketLengthMode(uint8_t maxLen) { return(SX127x::setPacketMode(RADIOLIB_SX127X_PACKET_VARIABLE, maxLen)); } +float SX127x::getNumSymbols(size_t len) { + // get symbol length in us + float symbolLength = (float) (uint32_t(1) << this->spreadingFactor) / (float) this->bandwidth; + + // get Low Data Rate optimization flag + float de = 0; + if (symbolLength >= 16.0) { + de = 1; + } + + // get explicit/implicit header enabled flag + float ih = (float) this->mod->SPIgetRegValue(RADIOLIB_SX127X_REG_MODEM_CONFIG_1, 0, 0); + + // get CRC enabled flag + float crc = (float) (this->mod->SPIgetRegValue(RADIOLIB_SX127X_REG_MODEM_CONFIG_2, 2, 2) >> 2); + + // get number of preamble symbols + float n_pre = (float) ((this->mod->SPIgetRegValue(RADIOLIB_SX127X_REG_PREAMBLE_MSB) << 8) | this->mod->SPIgetRegValue(RADIOLIB_SX127X_REG_PREAMBLE_LSB)); + + // get number of payload symbols + float n_pay = 8.0 + RADIOLIB_MAX(ceil((8.0 * (float) len - 4.0 * (float) this->spreadingFactor + 28.0 + 16.0 * crc - 20.0 * ih) / (4.0 * (float) this->spreadingFactor - 8.0 * de)) * (float) this->codingRate, 0.0); + + // add 4.25 symbols for the sync + return(n_pre + n_pay + 4.25f); +} + uint32_t SX127x::getTimeOnAir(size_t len) { // check active modem uint8_t modem = getActiveModem(); if (modem == RADIOLIB_SX127X_LORA) { - // Get symbol length in us + // get symbol length in us float symbolLength = (float) (uint32_t(1) << this->spreadingFactor) / (float) this->bandwidth; - // Get Low Data Rate optimization flag - float de = 0; - if (symbolLength >= 16.0) { - de = 1; - } - // Get explicit/implicit header enabled flag - float ih = (float) this->mod->SPIgetRegValue(RADIOLIB_SX127X_REG_MODEM_CONFIG_1, 0, 0); - // Get CRC enabled flag - float crc = (float) (this->mod->SPIgetRegValue(RADIOLIB_SX127X_REG_MODEM_CONFIG_2, 2, 2) >> 2); - // Get number of bits preamble - float n_pre = (float) ((this->mod->SPIgetRegValue(RADIOLIB_SX127X_REG_PREAMBLE_MSB) << 8) | this->mod->SPIgetRegValue(RADIOLIB_SX127X_REG_PREAMBLE_LSB)); - // Get number of bits payload - float n_pay = 8.0 + RADIOLIB_MAX(ceil((8.0 * (float) len - 4.0 * (float) this->spreadingFactor + 28.0 + 16.0 * crc - 20.0 * ih) / (4.0 * (float) this->spreadingFactor - 8.0 * de)) * (float) this->codingRate, 0.0); + + // get number of symbols + float n_sym = getNumSymbols(len); // Get time-on-air in us - return ceil(symbolLength * (n_pre + n_pay + 4.25)) * 1000; + return ceil(symbolLength * n_sym) * 1000; + } else if(modem == RADIOLIB_SX127X_FSK_OOK) { // Get number of bits preamble float n_pre = (float) ((this->mod->SPIgetRegValue(RADIOLIB_SX127X_REG_PREAMBLE_MSB_FSK) << 8) | this->mod->SPIgetRegValue(RADIOLIB_SX127X_REG_PREAMBLE_LSB_FSK)) * 8; diff --git a/src/modules/SX127x/SX127x.h b/src/modules/SX127x/SX127x.h index ce82377f6..fa4ab65ba 100644 --- a/src/modules/SX127x/SX127x.h +++ b/src/modules/SX127x/SX127x.h @@ -818,7 +818,6 @@ 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 */ @@ -1046,6 +1045,13 @@ class SX127x: public PhysicalLayer { */ int16_t variablePacketLengthMode(uint8_t maxLen = RADIOLIB_SX127X_MAX_PACKET_LENGTH_FSK); + /*! + \brief Convert from bytes to LoRa symbols. + \param len Payload length in bytes. + \returns The total number of LoRa symbols, including preamble, sync and possible header. + */ + float getNumSymbols(size_t len); + /*! \brief Get expected time-on-air for a given size of payload. \param len Payload length in bytes. From 9e7978c629dd3f93d23a5be919e3874cd0ed3b2b Mon Sep 17 00:00:00 2001 From: jgromes Date: Sun, 12 Nov 2023 13:28:37 +0100 Subject: [PATCH 19/21] [LoRaWAN] Update start receive for SX127x --- src/protocols/LoRaWAN/LoRaWAN.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index 754cee6e7..fc0b31645 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -958,7 +958,7 @@ int16_t LoRaWANNode::downlinkCommon() { mod->hal->delay(waitLen); // open Rx window by starting receive with specified timeout - state = this->phyLayer->startReceive(timeoutMod, irqFlags, irqMask, timeoutMod); + state = this->phyLayer->startReceive(timeoutMod, irqFlags, irqMask, 0); RADIOLIB_DEBUG_PRINTLN("Opening Rx%d window (%d us timeout)... <-- Rx Delay end ", i+1, timeoutHost); // wait for the timeout to complete (and a small additional delay) From 7002e47e6ebdc53b2308540e4cded4c8b32d5a94 Mon Sep 17 00:00:00 2001 From: jgromes Date: Sun, 12 Nov 2023 13:28:49 +0100 Subject: [PATCH 20/21] Added note about LoRaWAN beta --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6d2904bcd..dbe288bc7 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,8 @@ SX127x, RFM9x, SX126x, RF69, SX1231, CC1101, nRF24L01, RFM2x, Si443x and SX128x * [__POCSAG__](https://www.sigidwiki.com/wiki/POCSAG) using 2-FSK for modules: SX127x, RFM9x, RF69, SX1231, CC1101, nRF24L01, RFM2x and Si443x * [__LoRaWAN__](https://lora-alliance.org/) using LoRa for modules: -SX127x, RFM9x, SX126x and SX128x +SX127x, RFM9x, SX126x and SX128x + * NOTE: LoRaWAN support is currently in beta, feedback via [Issues](https://github.com/jgromes/RadioLib/issues) and [Discussions](https://github.com/jgromes/RadioLib/discussions) is appreciated! ### Supported Arduino platforms: * __Arduino__ From c9eb2f1db0c39d35831c72d98b5398599c994ff7 Mon Sep 17 00:00:00 2001 From: jgromes Date: Sun, 12 Nov 2023 13:32:29 +0100 Subject: [PATCH 21/21] [SX127x] Fixed potential float overflow --- src/modules/SX127x/SX127x.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/SX127x/SX127x.cpp b/src/modules/SX127x/SX127x.cpp index 703b150f8..02a2d3780 100644 --- a/src/modules/SX127x/SX127x.cpp +++ b/src/modules/SX127x/SX127x.cpp @@ -1268,7 +1268,7 @@ uint32_t SX127x::getTimeOnAir(size_t len) { float n_sym = getNumSymbols(len); // Get time-on-air in us - return ceil(symbolLength * n_sym) * 1000; + return ceil((double)symbolLength * (double)n_sym) * 1000; } else if(modem == RADIOLIB_SX127X_FSK_OOK) { // Get number of bits preamble