RadioLibSmol/src/protocols/LoRaWAN/LoRaWAN.cpp
2024-01-08 22:33:34 +01:00

2791 lines
113 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "LoRaWAN.h"
#include <string.h>
#if !RADIOLIB_EXCLUDE_LORAWAN
#if defined(RADIOLIB_EEPROM_UNSUPPORTED)
#warning "Persistent storage not supported!"
#endif
// 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 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;
this->rx2 = this->band->rx2;
this->dutyCycle = this->band->dutyCycle;
if(this->dutyCycle > 0) {
this->dutyCycleEnabled = true;
}
this->dwellTimeUp = this->band->dwellTimeUp;
if(this->dwellTimeUp > 0) {
this->dwellTimeEnabledUp = true;
}
this->dwellTimeDn = this->band->dwellTimeDn;
if(this->dwellTimeDn) {
this->dwellTimeEnabledDn = true;
}
this->txPowerMax = this->band->powerMax;
this->txPowerCur = 0; // start at 0 offset = full power
this->difsSlots = 2;
this->backoffMax = 6;
this->enableCSMA = false;
}
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();
}
// TODO do not return status code, but return LoRaWAN mode (OTAA, ABP, none)
int16_t LoRaWANNode::restore() {
// if already joined, ignore
if(this->activeMode != RADIOLIB_LORAWAN_MODE_NONE) {
return(this->activeMode);
}
Module* mod = this->phyLayer->getMod();
uint8_t nvm_table_version = mod->hal->getPersistentParameter<uint8_t>(RADIOLIB_EEPROM_TABLE_VERSION_ID);
// if (RADIOLIB_EEPROM_LORAWAN_TABLE_VERSION > nvm_table_version) {
// // set default values for variables that are new or something
// }
(void)nvm_table_version;
// check the magic value
uint8_t lwMode = mod->hal->getPersistentParameter<uint8_t>(RADIOLIB_EEPROM_LORAWAN_MODE_ID);
if(lwMode == RADIOLIB_LORAWAN_MODE_NONE) {
#if RADIOLIB_DEBUG
RADIOLIB_DEBUG_PRINTLN("magic id not set (no saved session)");
RADIOLIB_DEBUG_PRINTLN("first 16 bytes of NVM:");
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);
}
// pull all authentication keys from persistent storage
this->devAddr = mod->hal->getPersistentParameter<uint32_t>(RADIOLIB_EEPROM_LORAWAN_DEV_ADDR_ID);
mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_APP_S_KEY_ID), this->appSKey, RADIOLIB_AES128_BLOCK_SIZE);
mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_FNWK_SINT_KEY_ID), this->fNwkSIntKey, RADIOLIB_AES128_BLOCK_SIZE);
mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_SNWK_SINT_KEY_ID), this->sNwkSIntKey, RADIOLIB_AES128_BLOCK_SIZE);
mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_NWK_SENC_KEY_ID), this->nwkSEncKey, RADIOLIB_AES128_BLOCK_SIZE);
// get session parameters
this->rev = mod->hal->getPersistentParameter<uint8_t>(RADIOLIB_EEPROM_LORAWAN_VERSION_ID);
RADIOLIB_DEBUG_PRINTLN("LoRaWAN session: v1.%d", this->rev);
this->devNonce = mod->hal->getPersistentParameter<uint16_t>(RADIOLIB_EEPROM_LORAWAN_DEV_NONCE_ID);
this->joinNonce = mod->hal->getPersistentParameter<uint32_t>(RADIOLIB_EEPROM_LORAWAN_JOIN_NONCE_ID);
this->aFcntDown = mod->hal->getPersistentParameter<uint32_t>(RADIOLIB_EEPROM_LORAWAN_A_FCNT_DOWN_ID);
this->nFcntDown = mod->hal->getPersistentParameter<uint32_t>(RADIOLIB_EEPROM_LORAWAN_N_FCNT_DOWN_ID);
this->confFcntUp = mod->hal->getPersistentParameter<uint32_t>(RADIOLIB_EEPROM_LORAWAN_CONF_FCNT_UP_ID);
this->confFcntDown = mod->hal->getPersistentParameter<uint32_t>(RADIOLIB_EEPROM_LORAWAN_CONF_FCNT_DOWN_ID);
this->adrFcnt = mod->hal->getPersistentParameter<uint32_t>(RADIOLIB_EEPROM_LORAWAN_ADR_FCNT_ID);
// fcntUp is stored in highly efficient wear-leveling system, so parse it
this->restoreFcntUp();
// get the defined channels
int16_t state = this->restoreChannels();
RADIOLIB_ASSERT(state);
// get MAC state
LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 };
cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR;
cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn;
mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_LINK_ADR_ID), cmd.payload, cmd.len);
execMacCommand(&cmd, false);
cmd.cid = RADIOLIB_LORAWAN_MAC_DUTY_CYCLE;
cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_DUTY_CYCLE].lenDn;
mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_DUTY_CYCLE_ID), cmd.payload, cmd.len);
execMacCommand(&cmd, false);
cmd.cid = RADIOLIB_LORAWAN_MAC_RX_PARAM_SETUP;
cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_RX_PARAM_SETUP].lenDn;
mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_RX_PARAM_SETUP_ID), cmd.payload, cmd.len);
execMacCommand(&cmd, false);
cmd.cid = RADIOLIB_LORAWAN_MAC_RX_TIMING_SETUP;
cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_RX_TIMING_SETUP].lenDn;
mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_RX_TIMING_SETUP_ID), cmd.payload, cmd.len);
execMacCommand(&cmd, false);
cmd.cid = RADIOLIB_LORAWAN_MAC_TX_PARAM_SETUP;
cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_TX_PARAM_SETUP].lenDn;
mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_TX_PARAM_SETUP_ID), cmd.payload, cmd.len);
execMacCommand(&cmd, false);
cmd.cid = RADIOLIB_LORAWAN_MAC_ADR_PARAM_SETUP;
cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_ADR_PARAM_SETUP].lenDn;
mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_ADR_PARAM_SETUP_ID), cmd.payload, cmd.len);
execMacCommand(&cmd, false);
cmd.cid = RADIOLIB_LORAWAN_MAC_REJOIN_PARAM_SETUP;
cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_REJOIN_PARAM_SETUP].lenDn;
mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_REJOIN_PARAM_SETUP_ID), cmd.payload, cmd.len);
execMacCommand(&cmd, false);
uint8_t queueBuff[sizeof(LoRaWANMacCommandQueue_t)] = { 0 };
mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_MAC_QUEUE_UL_ID), queueBuff, sizeof(LoRaWANMacCommandQueue_t));
memcpy(&this->commandsUp, queueBuff, sizeof(LoRaWANMacCommandQueue_t));
RADIOLIB_DEBUG_PRINTLN("Number of MAC commands: %d", this->commandsUp.numCommands);
state = this->setPhyProperties();
RADIOLIB_ASSERT(state);
// full session is restored, so set joined flag to whichever mode is restored
this->activeMode = lwMode;
return(this->activeMode);
}
int16_t LoRaWANNode::restoreFcntUp() {
Module* mod = this->phyLayer->getMod();
uint8_t fcntBuffStart = mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_FCNT_UP_ID);
uint8_t fcntBuffEnd = mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_FCNT_UP_ID + 1);
uint8_t buffSize = fcntBuffEnd - fcntBuffStart;
#if RADIOLIB_STATIC_ONLY
uint8_t fcntBuff[RADIOLIB_STATIC_ARRAY_SIZE];
#else
uint8_t* fcntBuff = new uint8_t[buffSize];
#endif
mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_FCNT_UP_ID), fcntBuff, buffSize);
// copy the two most significant bytes from the first two bytes
uint32_t bits_30_22 = (uint32_t)fcntBuff[0];
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
// 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;
}
}
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 < buffSize; idx++) {
if(fcntBuff[idx] >> 7 != state) {
break;
}
}
uint32_t bits_7_0 = (uint32_t)fcntBuff[idx-1] & 0x7F;
#if !RADIOLIB_STATIC_ONLY
delete[] fcntBuff;
#endif
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() {
// first do the default channels
this->setupChannels(nullptr);
Module* mod = this->phyLayer->getMod();
uint8_t bufferZeroes[5] = { 0 };
if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) {
uint8_t numBytesUp = RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS * MacTable[RADIOLIB_LORAWAN_MAC_NEW_CHANNEL].lenDn;
uint8_t bufferUp[numBytesUp] = { 0 };
mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_UL_CHANNELS_ID), bufferUp, numBytesUp);
LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 };
cmd.cid = RADIOLIB_LORAWAN_MAC_NEW_CHANNEL;
for(int i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) {
cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_NEW_CHANNEL].lenDn;
memcpy(cmd.payload, &(bufferUp[i * cmd.len]), cmd.len);
if(memcmp(cmd.payload, bufferZeroes, cmd.len) != 0) { // only execute if it is not all zeroes
(void)execMacCommand(&cmd, false);
}
}
uint8_t numBytesDn = RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS * MacTable[RADIOLIB_LORAWAN_MAC_DL_CHANNEL].lenDn;
uint8_t bufferDn[numBytesDn] = { 0 };
mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_DL_CHANNELS_ID), bufferDn, numBytesDn);
cmd.cid = RADIOLIB_LORAWAN_MAC_DL_CHANNEL;
for(int i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) {
cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_DL_CHANNEL].lenDn;
memcpy(cmd.payload, &bufferDn[i * cmd.len], cmd.len);
if(memcmp(cmd.payload, bufferZeroes, cmd.len) != 0) { // only execute if it is not all zeroes
(void)execMacCommand(&cmd, false);
}
}
} else {
uint8_t numBytes = 8 * MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn;
uint8_t buffer[numBytes] = { 0 };
mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_UL_CHANNELS_ID), buffer, numBytes);
LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 };
cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR;
cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn;
for(int i = 0; i < 8; i++) {
memcpy(cmd.payload, &buffer[i * cmd.len], cmd.len);
// there COULD, according to spec, be an all zeroes ADR command - meh
if(memcmp(cmd.payload, bufferZeroes, cmd.len) != 0) {
execMacCommand(&cmd, false);
}
}
}
return(RADIOLIB_ERR_NONE);
}
#endif
void LoRaWANNode::beginCommon() {
LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 };
cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR;
cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn;
cmd.payload[0] = (this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] << 4);
cmd.payload[0] |= 0; // default to max Tx Power
cmd.payload[3] |= (1 << 7); // set the RFU bit, which means that the channel mask gets ignored
(void)execMacCommand(&cmd);
cmd.cid = RADIOLIB_LORAWAN_MAC_DUTY_CYCLE;
cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_DUTY_CYCLE].lenDn;
uint8_t maxDCyclePower;
switch(this->band->dutyCycle) {
case(0):
maxDCyclePower = 0;
break;
case(3600):
maxDCyclePower = 10;
break;
case(36000):
maxDCyclePower = 7;
break;
default:
maxDCyclePower = 0;
break;
}
cmd.payload[0] = maxDCyclePower;
(void)execMacCommand(&cmd);
cmd.cid = RADIOLIB_LORAWAN_MAC_RX_PARAM_SETUP;
cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_RX_PARAM_SETUP].lenDn;
cmd.payload[0] = (RADIOLIB_LORAWAN_RX1_DR_OFFSET << 4);
cmd.payload[0] |= this->rx2.drMax; // may be set by user, otherwise band's default upon initialization
uint32_t rx2Freq = uint32_t(this->rx2.freq * 10000);
LoRaWANNode::hton<uint32_t>(&cmd.payload[1], rx2Freq, 3);
(void)execMacCommand(&cmd);
cmd.cid = RADIOLIB_LORAWAN_MAC_RX_TIMING_SETUP;
cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_RX_TIMING_SETUP].lenDn;
cmd.payload[0] = (RADIOLIB_LORAWAN_RECEIVE_DELAY_1_MS / 1000);
(void)execMacCommand(&cmd);
cmd.cid = RADIOLIB_LORAWAN_MAC_TX_PARAM_SETUP;
cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_TX_PARAM_SETUP].lenDn;
cmd.payload[0] = (this->band->dwellTimeDn > 0 ? 1 : 0) << 5;
cmd.payload[0] |= (this->band->dwellTimeUp > 0 ? 1 : 0) << 4;
uint8_t maxEIRPRaw;
switch(this->band->powerMax) {
case(12):
maxEIRPRaw = 2;
break;
case(14):
maxEIRPRaw = 4;
break;
case(16):
maxEIRPRaw = 5;
break;
case(19): // this option does not exist for the TxParamSetupReq but will be caught during execution
maxEIRPRaw = 7;
break;
case(30):
maxEIRPRaw = 13;
break;
default:
maxEIRPRaw = 2;
break;
}
cmd.payload[0] |= maxEIRPRaw;
(void)execMacCommand(&cmd);
cmd.cid = RADIOLIB_LORAWAN_MAC_ADR_PARAM_SETUP;
cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_ADR_PARAM_SETUP].lenDn;
cmd.payload[0] = (RADIOLIB_LORAWAN_ADR_ACK_LIMIT_EXP << 4);
cmd.payload[0] |= RADIOLIB_LORAWAN_ADR_ACK_DELAY_EXP;
(void)execMacCommand(&cmd);
cmd.cid = RADIOLIB_LORAWAN_MAC_REJOIN_PARAM_SETUP;
cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_REJOIN_PARAM_SETUP].lenDn;
cmd.payload[0] = (RADIOLIB_LORAWAN_REJOIN_MAX_TIME_N << 4);
cmd.payload[0] |= RADIOLIB_LORAWAN_REJOIN_MAX_COUNT_N;
(void)execMacCommand(&cmd);
}
int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKey, uint8_t* appKey, uint8_t joinDr, bool force) {
// check if we actually need to send the join request
Module* mod = this->phyLayer->getMod();
#if !defined(RADIOLIB_EEPROM_UNSUPPORTED)
uint16_t checkSum = 0;
checkSum ^= checkSum16((uint8_t*)joinEUI, 8);
checkSum ^= checkSum16((uint8_t*)devEUI, 8);
checkSum ^= checkSum16(nwkKey, 16);
checkSum ^= checkSum16(appKey, 16);
bool validCheckSum = mod->hal->getPersistentParameter<uint16_t>(RADIOLIB_EEPROM_LORAWAN_CHECKSUM_ID) == checkSum;
bool validMode = mod->hal->getPersistentParameter<uint16_t>(RADIOLIB_EEPROM_LORAWAN_MODE_ID) == RADIOLIB_LORAWAN_MODE_OTAA;
if(!force && validCheckSum && validMode) {
// the device has joined already, we can just pull the data from persistent storage
RADIOLIB_DEBUG_PRINTLN("Found existing session; restoring...");
return(this->restore());
} else {
#if RADIOLIB_DEBUG
RADIOLIB_DEBUG_PRINTLN("Failed to restore session (checksum: %d, mode: %d)", validCheckSum, validMode);
RADIOLIB_DEBUG_PRINTLN("First 16 bytes of NVM:");
uint8_t nvmBuff[16];
mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(0), nvmBuff, 16);
RADIOLIB_DEBUG_HEXDUMP(nvmBuff, 16);
RADIOLIB_DEBUG_PRINTLN("Wiping EEPROM and starting a clean session");
#endif
this->wipe();
}
#else
(void)force;
#endif
int16_t state = RADIOLIB_ERR_NONE;
// setup uplink/downlink frequencies and datarates
state = this->selectChannelsJR(this->devNonce, joinDr);
RADIOLIB_ASSERT(state);
// setup all MAC properties to default values
this->beginCommon();
// set the physical layer configuration
state = this->setPhyProperties();
RADIOLIB_ASSERT(state);
// configure for uplink with default configuration
state = this->configureChannel(RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK);
RADIOLIB_ASSERT(state);
// 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];
// set the packet fields
joinRequestMsg[0] = RADIOLIB_LORAWAN_MHDR_MTYPE_JOIN_REQUEST | RADIOLIB_LORAWAN_MHDR_MAJOR_R1;
LoRaWANNode::hton<uint64_t>(&joinRequestMsg[RADIOLIB_LORAWAN_JOIN_REQUEST_JOIN_EUI_POS], joinEUI);
LoRaWANNode::hton<uint64_t>(&joinRequestMsg[RADIOLIB_LORAWAN_JOIN_REQUEST_DEV_EUI_POS], devEUI);
LoRaWANNode::hton<uint16_t>(&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);
LoRaWANNode::hton<uint32_t>(&joinRequestMsg[RADIOLIB_LORAWAN_JOIN_REQUEST_LEN - sizeof(uint32_t)], mic);
// 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
this->rxDelays[0] = RADIOLIB_LORAWAN_JOIN_ACCEPT_DELAY_1_MS;
this->rxDelays[1] = RADIOLIB_LORAWAN_JOIN_ACCEPT_DELAY_2_MS;
// 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];
// check received length
size_t lenRx = this->phyLayer->getPacketLength(true);
if((lenRx != RADIOLIB_LORAWAN_JOIN_ACCEPT_MAX_LEN) && (lenRx != RADIOLIB_LORAWAN_JOIN_ACCEPT_MAX_LEN - RADIOLIB_LORAWAN_JOIN_ACCEPT_CFLIST_LEN)) {
RADIOLIB_DEBUG_PRINTLN("joinAccept reply length mismatch, expected %luB got %luB", RADIOLIB_LORAWAN_JOIN_ACCEPT_MAX_LEN, lenRx);
return(RADIOLIB_ERR_DOWNLINK_MALFORMED);
}
// read the packet
state = this->phyLayer->readData(joinAcceptMsgEnc, lenRx);
// downlink frames are sent without CRC, which will raise error on SX127x
// we can ignore that error
if(state != RADIOLIB_ERR_LORA_HEADER_DAMAGED) {
RADIOLIB_ASSERT(state);
}
// check reply message type
if((joinAcceptMsgEnc[0] & RADIOLIB_LORAWAN_MHDR_MTYPE_MASK) != RADIOLIB_LORAWAN_MHDR_MTYPE_JOIN_ACCEPT) {
RADIOLIB_DEBUG_PRINTLN("joinAccept reply message type invalid, expected 0x%02x got 0x%02x", RADIOLIB_LORAWAN_MHDR_MTYPE_JOIN_ACCEPT, joinAcceptMsgEnc[0]);
return(RADIOLIB_ERR_DOWNLINK_MALFORMED);
}
// decrypt the join accept message
// this is done by encrypting again in ECB mode
// the first byte is the MAC header which is not encrypted
uint8_t joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_MAX_LEN];
joinAcceptMsg[0] = joinAcceptMsgEnc[0];
RadioLibAES128Instance.init(nwkKey);
RadioLibAES128Instance.encryptECB(&joinAcceptMsgEnc[1], RADIOLIB_LORAWAN_JOIN_ACCEPT_MAX_LEN - 1, &joinAcceptMsg[1]);
RADIOLIB_DEBUG_PRINTLN("joinAcceptMsg:");
RADIOLIB_DEBUG_HEXDUMP(joinAcceptMsg, lenRx);
// get current JoinNonce from downlink and previous JoinNonce from persistent storage
uint32_t joinNonceNew = LoRaWANNode::ntoh<uint32_t>(&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;
this->homeNetId = LoRaWANNode::ntoh<uint32_t>(&joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_HOME_NET_ID_POS], 3);
this->devAddr = LoRaWANNode::ntoh<uint32_t>(&joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_DEV_ADDR_POS]);
// check LoRaWAN revision (the MIC verification depends on this)
uint8_t dlSettings = joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_DL_SETTINGS_POS];
this->rev = (dlSettings & RADIOLIB_LORAWAN_JOIN_ACCEPT_R_1_1) >> 7;
RADIOLIB_DEBUG_PRINTLN("LoRaWAN revision: 1.%d", this->rev);
// 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;
LoRaWANNode::hton<uint64_t>(&keyDerivationBuff[1], devEUI);
RadioLibAES128Instance.init(nwkKey);
RadioLibAES128Instance.encryptECB(keyDerivationBuff, RADIOLIB_AES128_BLOCK_SIZE, this->jSIntKey);
// prepare the buffer for MIC calculation
uint8_t micBuff[3*RADIOLIB_AES128_BLOCK_SIZE] = { 0 };
micBuff[0] = RADIOLIB_LORAWAN_JOIN_REQUEST_TYPE;
LoRaWANNode::hton<uint64_t>(&micBuff[1], joinEUI);
LoRaWANNode::hton<uint16_t>(&micBuff[9], this->devNonce);
memcpy(&micBuff[11], joinAcceptMsg, lenRx);
if(!verifyMIC(micBuff, lenRx + 11, this->jSIntKey)) {
return(RADIOLIB_ERR_CRC_MISMATCH);
}
} else {
// 1.0 version
if(!verifyMIC(joinAcceptMsg, lenRx, nwkKey)) {
return(RADIOLIB_ERR_CRC_MISMATCH);
}
}
LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 };
cmd.cid = RADIOLIB_LORAWAN_MAC_RX_PARAM_SETUP;
cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_RX_PARAM_SETUP].lenDn;
cmd.payload[0] = dlSettings & 0x7F;
uint32_t rx2Freq = uint32_t(this->rx2.freq * 10000); // default Rx2 frequency
LoRaWANNode::hton<uint32_t>(&cmd.payload[1], rx2Freq, 3);
(void)execMacCommand(&cmd);
cmd.cid = RADIOLIB_LORAWAN_MAC_RX_TIMING_SETUP;
cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_RX_TIMING_SETUP].lenDn;
cmd.payload[0] = joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_RX_DELAY_POS];
(void)execMacCommand(&cmd);
// process CFlist if present
if(lenRx == RADIOLIB_LORAWAN_JOIN_ACCEPT_MAX_LEN) {
uint8_t cfList[RADIOLIB_LORAWAN_JOIN_ACCEPT_CFLIST_LEN] = { 0 };
memcpy(&cfList[0], &joinAcceptMsg[RADIOLIB_LORAWAN_JOIN_ACCEPT_CFLIST_POS], RADIOLIB_LORAWAN_JOIN_ACCEPT_CFLIST_LEN);
this->setupChannels(cfList);
} else {
this->setupChannels(nullptr);
}
// prepare buffer for key derivation
uint8_t keyDerivationBuff[RADIOLIB_AES128_BLOCK_SIZE] = { 0 };
LoRaWANNode::hton<uint32_t>(&keyDerivationBuff[RADIOLIB_LORAWAN_JOIN_ACCEPT_JOIN_NONCE_POS], joinNonce, 3);
// check protocol version (1.0 vs 1.1)
if(this->rev == 1) {
// 1.1 version, derive the keys
LoRaWANNode::hton<uint64_t>(&keyDerivationBuff[RADIOLIB_LORAWAN_JOIN_ACCEPT_JOIN_EUI_POS], joinEUI);
LoRaWANNode::hton<uint16_t>(&keyDerivationBuff[RADIOLIB_LORAWAN_JOIN_ACCEPT_DEV_NONCE_POS], this->devNonce);
keyDerivationBuff[0] = RADIOLIB_LORAWAN_JOIN_ACCEPT_APP_S_KEY;
RadioLibAES128Instance.init(appKey);
RadioLibAES128Instance.encryptECB(keyDerivationBuff, RADIOLIB_AES128_BLOCK_SIZE, this->appSKey);
keyDerivationBuff[0] = RADIOLIB_LORAWAN_JOIN_ACCEPT_F_NWK_S_INT_KEY;
RadioLibAES128Instance.init(nwkKey);
RadioLibAES128Instance.encryptECB(keyDerivationBuff, RADIOLIB_AES128_BLOCK_SIZE, this->fNwkSIntKey);
keyDerivationBuff[0] = RADIOLIB_LORAWAN_JOIN_ACCEPT_S_NWK_S_INT_KEY;
RadioLibAES128Instance.init(nwkKey);
RadioLibAES128Instance.encryptECB(keyDerivationBuff, RADIOLIB_AES128_BLOCK_SIZE, this->sNwkSIntKey);
keyDerivationBuff[0] = RADIOLIB_LORAWAN_JOIN_ACCEPT_NWK_S_ENC_KEY;
RadioLibAES128Instance.init(nwkKey);
RadioLibAES128Instance.encryptECB(keyDerivationBuff, RADIOLIB_AES128_BLOCK_SIZE, this->nwkSEncKey);
// enqueue the RekeyInd MAC command to be sent in the next uplink
LoRaWANMacCommand_t cmd = {
.cid = RADIOLIB_LORAWAN_MAC_REKEY,
.payload = { this->rev },
.len = sizeof(uint8_t),
.repeat = 0x01 << RADIOLIB_LORAWAN_ADR_ACK_LIMIT_EXP,
};
state = pushMacCommand(&cmd, &this->commandsUp);
RADIOLIB_ASSERT(state);
} else {
// 1.0 version, just derive the keys
LoRaWANNode::hton<uint32_t>(&keyDerivationBuff[RADIOLIB_LORAWAN_JOIN_ACCEPT_HOME_NET_ID_POS], this->homeNetId, 3);
LoRaWANNode::hton<uint16_t>(&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);
keyDerivationBuff[0] = RADIOLIB_LORAWAN_JOIN_ACCEPT_F_NWK_S_INT_KEY;
RadioLibAES128Instance.init(nwkKey);
RadioLibAES128Instance.encryptECB(keyDerivationBuff, RADIOLIB_AES128_BLOCK_SIZE, this->fNwkSIntKey);
memcpy(this->sNwkSIntKey, this->fNwkSIntKey, RADIOLIB_AES128_KEY_SIZE);
memcpy(this->nwkSEncKey, this->fNwkSIntKey, RADIOLIB_AES128_KEY_SIZE);
}
// 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 activation keys checksum, device address & keys as well as JoinAccept values; these are only ever set when joining
mod->hal->setPersistentParameter<uint16_t>(RADIOLIB_EEPROM_LORAWAN_CHECKSUM_ID, checkSum);
mod->hal->setPersistentParameter<uint32_t>(RADIOLIB_EEPROM_LORAWAN_DEV_ADDR_ID, this->devAddr);
mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_APP_S_KEY_ID), this->appSKey, RADIOLIB_AES128_BLOCK_SIZE);
mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_FNWK_SINT_KEY_ID), this->fNwkSIntKey, RADIOLIB_AES128_BLOCK_SIZE);
mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_SNWK_SINT_KEY_ID), this->sNwkSIntKey, RADIOLIB_AES128_BLOCK_SIZE);
mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_NWK_SENC_KEY_ID), this->nwkSEncKey, RADIOLIB_AES128_BLOCK_SIZE);
// save join-request parameters
mod->hal->setPersistentParameter<uint32_t>(RADIOLIB_EEPROM_LORAWAN_HOME_NET_ID, this->homeNetId);
mod->hal->setPersistentParameter<uint32_t>(RADIOLIB_EEPROM_LORAWAN_JOIN_NONCE_ID, this->joinNonce);
this->saveSession();
// everything written to NVM, write current table version to persistent storage and set mode
mod->hal->setPersistentParameter<uint16_t>(RADIOLIB_EEPROM_TABLE_VERSION_ID, RADIOLIB_EEPROM_TABLE_VERSION);
mod->hal->setPersistentParameter<uint16_t>(RADIOLIB_EEPROM_LORAWAN_MODE_ID, RADIOLIB_LORAWAN_MODE_OTAA);
#endif
this->activeMode = RADIOLIB_LORAWAN_MODE_OTAA;
return(RADIOLIB_ERR_NONE);
}
int16_t LoRaWANNode::beginABP(uint32_t addr, uint8_t* nwkSKey, uint8_t* appSKey, uint8_t* fNwkSIntKey, uint8_t* sNwkSIntKey, bool force) {
Module* mod = this->phyLayer->getMod();
#if !defined(RADIOLIB_EEPROM_UNSUPPORTED)
// check if we actually need to restart from a clean session
uint16_t checkSum = 0;
checkSum ^= checkSum16((uint8_t*)addr, 4);
checkSum ^= checkSum16(nwkSKey, 16);
checkSum ^= checkSum16(appSKey, 16);
if(fNwkSIntKey) { checkSum ^= checkSum16(fNwkSIntKey, 16); }
if(sNwkSIntKey) { checkSum ^= checkSum16(sNwkSIntKey, 16); }
bool validCheckSum = mod->hal->getPersistentParameter<uint16_t>(RADIOLIB_EEPROM_LORAWAN_CHECKSUM_ID) == checkSum;
bool validMode = mod->hal->getPersistentParameter<uint16_t>(RADIOLIB_EEPROM_LORAWAN_MODE_ID) == RADIOLIB_LORAWAN_MODE_ABP;
if(!force && validCheckSum && validMode) {
// the device has joined already, we can just pull the data from persistent storage
RADIOLIB_DEBUG_PRINTLN("Found existing session; restoring...");
return(this->restore());
} else {
#if RADIOLIB_DEBUG
RADIOLIB_DEBUG_PRINTLN("Failed to restore session (checksum: %d, mode: %d)", validCheckSum, validMode);
RADIOLIB_DEBUG_PRINTLN("First 16 bytes of NVM:");
uint8_t nvmBuff[16];
mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(0), nvmBuff, 16);
RADIOLIB_DEBUG_HEXDUMP(nvmBuff, 16);
RADIOLIB_DEBUG_PRINTLN("Wiping EEPROM and starting a clean session");
#endif
this->wipe();
}
#else
(void)force;
#endif
this->devAddr = addr;
memcpy(this->appSKey, appSKey, RADIOLIB_AES128_KEY_SIZE);
memcpy(this->nwkSEncKey, nwkSKey, RADIOLIB_AES128_KEY_SIZE);
if(fNwkSIntKey) {
this->rev = 1;
memcpy(this->fNwkSIntKey, fNwkSIntKey, RADIOLIB_AES128_KEY_SIZE);
} else {
memcpy(this->fNwkSIntKey, nwkSKey, RADIOLIB_AES128_KEY_SIZE);
}
if(sNwkSIntKey) {
memcpy(this->sNwkSIntKey, sNwkSIntKey, RADIOLIB_AES128_KEY_SIZE);
}
int16_t state = RADIOLIB_ERR_NONE;
// calculate initial datarate - in case of fixed bands, this requires a subband to be selected
// downlink datarate is calculated using a specific uplink channel, so don't care here
this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = (this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][0].drMax + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][0].drMin) / 2;
// setup all MAC properties to default values
this->beginCommon();
// set the physical layer configuration
state = this->setPhyProperties();
RADIOLIB_ASSERT(state);
#if !defined(RADIOLIB_EEPROM_UNSUPPORTED)
// save the activation keys checksum, device address & keys
mod->hal->setPersistentParameter<uint16_t>(RADIOLIB_EEPROM_LORAWAN_CHECKSUM_ID, checkSum);
mod->hal->setPersistentParameter<uint32_t>(RADIOLIB_EEPROM_LORAWAN_DEV_ADDR_ID, this->devAddr);
mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_APP_S_KEY_ID), this->appSKey, RADIOLIB_AES128_BLOCK_SIZE);
mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_FNWK_SINT_KEY_ID), this->fNwkSIntKey, RADIOLIB_AES128_BLOCK_SIZE);
mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_SNWK_SINT_KEY_ID), this->sNwkSIntKey, RADIOLIB_AES128_BLOCK_SIZE);
mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_NWK_SENC_KEY_ID), this->nwkSEncKey, RADIOLIB_AES128_BLOCK_SIZE);
// save all new frame counters
this->saveSession();
// everything written to NVM, write current table version to persistent storage and set mode
mod->hal->setPersistentParameter<uint16_t>(RADIOLIB_EEPROM_TABLE_VERSION_ID, RADIOLIB_EEPROM_TABLE_VERSION);
mod->hal->setPersistentParameter<uint16_t>(RADIOLIB_EEPROM_LORAWAN_MODE_ID, RADIOLIB_LORAWAN_MODE_ABP);
#endif
this->activeMode = RADIOLIB_LORAWAN_MODE_ABP;
return(RADIOLIB_ERR_NONE);
}
bool LoRaWANNode::isJoined() {
return(this->activeMode != RADIOLIB_LORAWAN_MODE_NONE);
}
#if !defined(RADIOLIB_EEPROM_UNSUPPORTED)
int16_t LoRaWANNode::saveSession() {
Module* mod = this->phyLayer->getMod();
// store session configuration (MAC commands)
if(mod->hal->getPersistentParameter<uint8_t>(RADIOLIB_EEPROM_LORAWAN_VERSION_ID) != this->rev)
mod->hal->setPersistentParameter<uint8_t>(RADIOLIB_EEPROM_LORAWAN_VERSION_ID, this->rev);
if(mod->hal->getPersistentParameter<uint16_t>(RADIOLIB_EEPROM_LORAWAN_DEV_NONCE_ID) != this->devNonce)
mod->hal->setPersistentParameter<uint16_t>(RADIOLIB_EEPROM_LORAWAN_DEV_NONCE_ID, this->devNonce);
// store all frame counters
if(mod->hal->getPersistentParameter<uint32_t>(RADIOLIB_EEPROM_LORAWAN_A_FCNT_DOWN_ID) != this->aFcntDown)
mod->hal->setPersistentParameter<uint32_t>(RADIOLIB_EEPROM_LORAWAN_A_FCNT_DOWN_ID, this->aFcntDown);
if(mod->hal->getPersistentParameter<uint32_t>(RADIOLIB_EEPROM_LORAWAN_N_FCNT_DOWN_ID) != this->nFcntDown)
mod->hal->setPersistentParameter<uint32_t>(RADIOLIB_EEPROM_LORAWAN_N_FCNT_DOWN_ID, this->nFcntDown);
if(mod->hal->getPersistentParameter<uint32_t>(RADIOLIB_EEPROM_LORAWAN_CONF_FCNT_UP_ID) != this->confFcntUp)
mod->hal->setPersistentParameter<uint32_t>(RADIOLIB_EEPROM_LORAWAN_CONF_FCNT_UP_ID, this->confFcntUp);
if(mod->hal->getPersistentParameter<uint32_t>(RADIOLIB_EEPROM_LORAWAN_CONF_FCNT_DOWN_ID) != this->confFcntDown)
mod->hal->setPersistentParameter<uint32_t>(RADIOLIB_EEPROM_LORAWAN_CONF_FCNT_DOWN_ID, this->confFcntDown);
if(mod->hal->getPersistentParameter<uint32_t>(RADIOLIB_EEPROM_LORAWAN_ADR_FCNT_ID) != this->adrFcnt)
mod->hal->setPersistentParameter<uint32_t>(RADIOLIB_EEPROM_LORAWAN_ADR_FCNT_ID, this->adrFcnt);
// fcntUp is saved using highly efficient wear-leveling as this is by far going to be written most often
this->saveFcntUp();
// if there is, or was, any MAC command in the queue, overwrite with the current MAC queue
uint8_t queueBuff[sizeof(LoRaWANMacCommandQueue_t)] = { 0 };
mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_MAC_QUEUE_UL_ID), queueBuff, sizeof(LoRaWANMacCommandQueue_t));
LoRaWANMacCommandQueue_t cmdTemp;
memcpy(&cmdTemp, queueBuff, sizeof(LoRaWANMacCommandQueue_t));
if(this->commandsUp.numCommands > 0 || cmdTemp.numCommands > 0) {
memcpy(queueBuff, &this->commandsUp, sizeof(LoRaWANMacCommandQueue_t));
mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_MAC_QUEUE_UL_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_EEPROM_LORAWAN_FCNT_UP_ID), fcntBuff, 30);
// we discard the first two bits - your flash will likely be far dead by the time you reach 2^30 uplinks
// the first two bytes of the remaining 30 bytes are stored straight into storage without additional wear leveling
// because they hardly ever change
uint8_t bits_30_22 = (uint8_t)(this->fcntUp >> 22);
if(fcntBuff[0] != bits_30_22)
mod->hal->setPersistentParameter<uint8_t>(RADIOLIB_EEPROM_LORAWAN_FCNT_UP_ID, bits_30_22, 0);
uint8_t bits_22_14 = (uint8_t)(this->fcntUp >> 14);
if(fcntBuff[1] != bits_22_14)
mod->hal->setPersistentParameter<uint8_t>(RADIOLIB_EEPROM_LORAWAN_FCNT_UP_ID, bits_22_14, 1);
// the next 7 bits are stored into one of few indices
// this index is indicated by the first byte that has its state (most significant bit) different from its predecessor
// 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;
}
}
// 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<uint8_t>(RADIOLIB_EEPROM_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<uint8_t>(RADIOLIB_EEPROM_LORAWAN_FCNT_UP_ID, bits_7_0, idx);
return(RADIOLIB_ERR_NONE);
}
#endif // RADIOLIB_EEPROM_UNSUPPORTED
#if defined(RADIOLIB_BUILD_ARDUINO)
int16_t LoRaWANNode::uplink(String& str, uint8_t port, bool isConfirmed, LoRaWANEvent_t* event) {
return(this->uplink(str.c_str(), port, isConfirmed, event));
}
#endif
int16_t LoRaWANNode::uplink(const char* str, uint8_t port, bool isConfirmed, LoRaWANEvent_t* event) {
return(this->uplink((uint8_t*)str, strlen(str), port, isConfirmed, event));
}
int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port, bool isConfirmed, LoRaWANEvent_t* event) {
Module* mod = this->phyLayer->getMod();
// 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);
}
// if adhering to dutyCycle and the time since last uplink + interval has not elapsed, return an error
if(this->dutyCycleEnabled && this->rxDelayStart + dutyCycleInterval(this->dutyCycle, this->lastToA) > mod->hal->millis()) {
return(RADIOLIB_ERR_UPLINK_UNAVAILABLE);
}
// check destination port
if(port > 0xDF) {
return(RADIOLIB_ERR_INVALID_PORT);
}
// port 0 is only allowed for MAC-only payloads
if(port == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND) {
if (!this->isMACPayload) {
return(RADIOLIB_ERR_INVALID_PORT);
}
// if this is MAC only payload, continue and reset for next uplink
this->isMACPayload = false;
}
int16_t state = RADIOLIB_ERR_NONE;
// 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 = this->commandsUp.len;
foptsBufSize = 15;
}
// check maximum payload len as defined in phy
if(len > this->band->payloadLenMax[this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]]) {
// len = this->band->payloadLenMax[this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK]];
return(RADIOLIB_ERR_PACKET_TOO_LONG);
}
// increase frame counter by one
this->fcntUp += 1;
bool adrAckReq = false;
if(this->adrEnabled) {
// check if we need to do ADR stuff
uint32_t adrLimit = 0x01 << this->adrLimitExp;
uint32_t adrDelay = 0x01 << this->adrDelayExp;
if((this->fcntUp - this->adrFcnt) >= adrLimit) {
adrAckReq = true;
}
// if we hit the Limit + Delay, try one of three, in order:
// set TxPower to max, set DR to min, enable all default channels
if ((this->fcntUp - this->adrFcnt) == (adrLimit + adrDelay)) {
uint8_t adrStage = 1;
while(adrStage != 0) {
switch(adrStage) {
case(1): {
// if the TxPower field has some offset, remove it and switch to maximum power
if(this->txPowerCur > 0) {
// set the maximum power supported by both the module and the band
state = this->setTxPower(this->txPowerMax, true);
if(state == RADIOLIB_ERR_NONE) {
this->txPowerCur = 0;
adrStage = 0; // successfully did some ADR stuff
}
}
if(adrStage == 1) { // if nothing succeeded, proceed to stage 2
adrStage = 2;
}
}
break;
case(2): {
// try to decrease the datarate
if(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] > 0) {
if(this->setDatarate(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] - 1, true) == RADIOLIB_ERR_NONE) {
adrStage = 0; // successfully did some ADR stuff
}
}
if(adrStage == 2) { // if nothing succeeded, proceed to stage 3
adrStage = 3;
}
}
break;
case(3): {
if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) {
this->setupChannels(nullptr); // revert to default frequencies
} else {
// if a subband was selected by user, go back to its default state
// hopefully it'll help something, but probably not; at least we tried..
if(this->selectedSubband >= 0) {
this->selectSubband(this->selectedSubband);
}
}
adrStage = 0; // nothing else to do, so end the cycle
}
break;
}
}
// we tried something to improve the range, so increase the ADR frame counter by 'ADR delay'
this->adrFcnt += adrDelay;
}
}
// configure for uplink
this->selectChannels();
state = this->configureChannel(RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK);
RADIOLIB_ASSERT(state);
// if dwell time is imposed, calculated expected time on air and cancel if exceeds
if(this->dwellTimeEnabledUp && this->phyLayer->getTimeOnAir(RADIOLIB_LORAWAN_FRAME_LEN(len, foptsLen) - 16)/1000 > this->dwellTimeUp) {
return(RADIOLIB_ERR_PACKET_TOO_LONG);
}
// build the uplink message
// the first 16 bytes are reserved for MIC calculation blocks
size_t uplinkMsgLen = RADIOLIB_LORAWAN_FRAME_LEN(len, foptsBufSize);
#if RADIOLIB_STATIC_ONLY
uint8_t uplinkMsg[RADIOLIB_STATIC_ARRAY_SIZE];
#else
uint8_t* uplinkMsg = new uint8_t[uplinkMsgLen];
#endif
// set the packet fields
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<uint32_t>(&uplinkMsg[RADIOLIB_LORAWAN_FHDR_DEV_ADDR_POS], this->devAddr);
// length of fopts will be added later
uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] = 0x00;
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;
}
}
// if the saved confirm-fcnt is set, set the ACK bit
bool isConfirmingDown = false;
if(this->confFcntDown != RADIOLIB_LORAWAN_FCNT_NONE) {
isConfirmingDown = true;
uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] |= RADIOLIB_LORAWAN_FCTRL_ACK;
}
LoRaWANNode::hton<uint16_t>(&uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCNT_POS], (uint16_t)this->fcntUp);
// check if we have some MAC commands to append
if(foptsLen > 0) {
// assume maximum possible buffer size
uint8_t foptsBuff[15];
uint8_t* foptsPtr = foptsBuff;
// append all MAC replies into fopts buffer
int16_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);
}
}
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], this->fcntUp, RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK, 0x01, true);
}
// set the port
uplinkMsg[RADIOLIB_LORAWAN_FHDR_FPORT_POS(foptsLen)] = port;
// select encryption key based on the target port
uint8_t* encKey = this->appSKey;
if(port == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND) {
encKey = this->nwkSEncKey;
}
// 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;
block0[RADIOLIB_LORAWAN_BLOCK_DIR_POS] = RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK;
LoRaWANNode::hton<uint32_t>(&block0[RADIOLIB_LORAWAN_BLOCK_DEV_ADDR_POS], this->devAddr);
LoRaWANNode::hton<uint32_t>(&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 };
memcpy(block1, block0, RADIOLIB_AES128_BLOCK_SIZE);
if(this->confFcntDown != RADIOLIB_LORAWAN_FCNT_NONE) {
LoRaWANNode::hton<uint16_t>(&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;
RADIOLIB_DEBUG_PRINTLN("uplinkMsg pre-MIC:");
RADIOLIB_DEBUG_HEXDUMP(uplinkMsg, uplinkMsgLen);
// calculate authentication codes
memcpy(uplinkMsg, block1, RADIOLIB_AES128_BLOCK_SIZE);
uint32_t micS = this->generateMIC(uplinkMsg, uplinkMsgLen - sizeof(uint32_t), this->sNwkSIntKey);
memcpy(uplinkMsg, block0, RADIOLIB_AES128_BLOCK_SIZE);
uint32_t micF = this->generateMIC(uplinkMsg, uplinkMsgLen - sizeof(uint32_t), this->fNwkSIntKey);
// check LoRaWAN revision
if(this->rev == 1) {
uint32_t mic = ((uint32_t)(micF & 0x0000FF00) << 16) | ((uint32_t)(micF & 0x0000000FF) << 16) | ((uint32_t)(micS & 0x0000FF00) >> 0) | ((uint32_t)(micS & 0x0000000FF) >> 0);
LoRaWANNode::hton<uint32_t>(&uplinkMsg[uplinkMsgLen - sizeof(uint32_t)], mic);
} else {
LoRaWANNode::hton<uint32_t>(&uplinkMsg[uplinkMsgLen - sizeof(uint32_t)], micF);
}
RADIOLIB_DEBUG_PRINTLN("uplinkMsg:");
RADIOLIB_DEBUG_HEXDUMP(uplinkMsg, uplinkMsgLen);
// perform CSMA if enabled.
if (enableCSMA) {
performCSMA();
}
// 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);
// 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");
// calculate Time on Air of this uplink in milliseconds
this->lastToA = this->phyLayer->getTimeOnAir(uplinkMsgLen - RADIOLIB_LORAWAN_FHDR_LEN_START_OFFS) / 1000;
#if !RADIOLIB_STATIC_ONLY
delete[] uplinkMsg;
#endif
RADIOLIB_ASSERT(state);
// the downlink confirmation was acknowledged, so clear the counter value
this->confFcntDown = RADIOLIB_LORAWAN_FCNT_NONE;
// pass the extra info if requested
if(event) {
event->dir = RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK;
event->confirmed = isConfirmed;
event->confirming = isConfirmingDown;
event->datarate = this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK];
event->freq = currentChannels[event->dir].freq;
event->power = this->txPowerMax - this->txPowerCur * 2;
event->fcnt = this->fcntUp;
event->port = port;
}
return(RADIOLIB_ERR_NONE);
}
int16_t LoRaWANNode::downlinkCommon() {
Module* mod = this->phyLayer->getMod();
const uint32_t scanGuard = 10;
// 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);
}
// configure for downlink
int16_t state = this->configureChannel(RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK);
RADIOLIB_ASSERT(state);
// downlink messages are sent with inverted IQ
if(!this->FSK) {
state = this->phyLayer->invertIQ(true);
RADIOLIB_ASSERT(state);
}
// create the masks that are required for receiving downlinks
uint16_t irqFlags = 0x0000;
uint16_t irqMask = 0x0000;
this->phyLayer->irqRxDoneRxTimeout(irqFlags, irqMask);
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(timeoutHost);
// 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);
if(waitLen > scanGuard) {
waitLen -= scanGuard;
}
mod->hal->delay(waitLen);
// 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");
// check if the IRQ bit for Rx Timeout is set
if(!this->phyLayer->isRxTimeout()) {
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);
DataRate_t dataRate;
findDataRate(this->rx2.drMax, &dataRate);
state = this->phyLayer->setDataRate(dataRate);
RADIOLIB_ASSERT(state);
}
}
// 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()) {
this->phyLayer->standby(); // TODO check: this should be done automagically due to RxSingle?
if(!this->FSK) {
this->phyLayer->invertIQ(false);
}
return(RADIOLIB_ERR_RX_TIMEOUT);
}
// wait for the DIO to fire indicating a downlink is received
while(!downlinkAction) {
mod->hal->yield();
}
// we have a message, clear actions, go to standby and reset the IQ inversion
this->phyLayer->standby(); // TODO check: this should be done automagically due to RxSingle?
this->phyLayer->clearPacketReceivedAction();
if(!this->FSK) {
state = this->phyLayer->invertIQ(false);
RADIOLIB_ASSERT(state);
}
return(RADIOLIB_ERR_NONE);
}
#if defined(RADIOLIB_BUILD_ARDUINO)
int16_t LoRaWANNode::downlink(String& str, LoRaWANEvent_t* event) {
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, event);
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, LoRaWANEvent_t* event) {
// 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);
// check the minimum required frame length
// an extra byte is subtracted because downlink frames may not have a port
if(downlinkMsgLen < RADIOLIB_LORAWAN_FRAME_LEN(0, 0) - 1 - RADIOLIB_AES128_BLOCK_SIZE) {
RADIOLIB_DEBUG_PRINTLN("Downlink message too short (%lu bytes)", downlinkMsgLen);
return(RADIOLIB_ERR_DOWNLINK_MALFORMED);
}
// build the buffer for the downlink message
// the first 16 bytes are reserved for MIC calculation block
#if !RADIOLIB_STATIC_ONLY
uint8_t* downlinkMsg = new uint8_t[RADIOLIB_AES128_BLOCK_SIZE + downlinkMsgLen];
#else
uint8_t downlinkMsg[RADIOLIB_STATIC_ARRAY_SIZE];
#endif
// set the MIC calculation block
memset(downlinkMsg, 0x00, RADIOLIB_AES128_BLOCK_SIZE);
downlinkMsg[RADIOLIB_LORAWAN_BLOCK_MAGIC_POS] = RADIOLIB_LORAWAN_MIC_BLOCK_MAGIC;
LoRaWANNode::hton<uint32_t>(&downlinkMsg[RADIOLIB_LORAWAN_BLOCK_DEV_ADDR_POS], this->devAddr);
downlinkMsg[RADIOLIB_LORAWAN_BLOCK_DIR_POS] = RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK;
downlinkMsg[RADIOLIB_LORAWAN_MIC_BLOCK_LEN_POS] = downlinkMsgLen - sizeof(uint32_t);
// read the data
state = this->phyLayer->readData(&downlinkMsg[RADIOLIB_AES128_BLOCK_SIZE], downlinkMsgLen);
// downlink frames are sent without CRC, which will raise error on SX127x
// we can ignore that error
if(state == RADIOLIB_ERR_LORA_HEADER_DAMAGED) {
state = RADIOLIB_ERR_NONE;
}
if(state != RADIOLIB_ERR_NONE) {
#if !RADIOLIB_STATIC_ONLY
delete[] downlinkMsg;
#endif
return(state);
}
// get the frame counter and set it to the MIC calculation block
uint16_t fcnt16 = LoRaWANNode::ntoh<uint16_t>(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FCNT_POS]);
LoRaWANNode::hton<uint16_t>(&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
bool isConfirmingUp = false;
if((downlinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] & RADIOLIB_LORAWAN_FCTRL_ACK) && (this->rev == 1)) {
isConfirmingUp = true;
LoRaWANNode::hton<uint16_t>(&downlinkMsg[RADIOLIB_LORAWAN_BLOCK_CONF_FCNT_POS], (uint16_t)this->confFcntUp);
}
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);
RADIOLIB_DEBUG_PRINTLN("FOpts: %02X", downlinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS]);
// in LoRaWAN v1.1, a frame can be a network frame if there is no Application payload
// i.e., no payload at all (empty frame or FOpts only), or MAC only payload (FPort = 0)
// TODO "NFCntDown is used for MAC communication on port 0 and when the FPort field is missing"
// so what about empty frames for ACK? Per TS008, these should be Application downlinks
bool isAppDownlink = true;
if(payLen <= 0) {
if(this->rev == 1) {
isAppDownlink = false;
}
}
else if(downlinkMsg[RADIOLIB_LORAWAN_FHDR_FPORT_POS(foptsLen)] == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND) {
foptsLen = payLen - 1;
if(this->rev == 1) {
isAppDownlink = false;
}
}
RADIOLIB_DEBUG_PRINTLN("FOptsLen: %d", foptsLen);
// check the FcntDown value (Network or Application)
uint32_t fcntDownPrev = 0;
if (isAppDownlink) {
fcntDownPrev = this->aFcntDown;
} else {
fcntDownPrev = this->nFcntDown;
}
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 !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 |= ((uint32_t)msb << 16); // add back the MSB part
}
}
// check the MIC
if(!verifyMIC(downlinkMsg, RADIOLIB_AES128_BLOCK_SIZE + downlinkMsgLen, this->sNwkSIntKey)) {
#if !RADIOLIB_STATIC_ONLY
delete[] downlinkMsg;
#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;
isConfirmedDown = true;
}
// check the address
uint32_t addr = LoRaWANNode::ntoh<uint32_t>(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_DEV_ADDR_POS]);
if(addr != this->devAddr) {
RADIOLIB_DEBUG_PRINTLN("Device address mismatch, expected 0x%08X, got 0x%08X", this->devAddr, addr);
#if !RADIOLIB_STATIC_ONLY
delete[] downlinkMsg;
#endif
return(RADIOLIB_ERR_DOWNLINK_MALFORMED);
}
// process FOpts (if there are any)
if(foptsLen > 0) {
// there are some Fopts, decrypt them
uint8_t fopts[RADIOLIB_MAX(RADIOLIB_LORAWAN_FHDR_FOPTS_LEN_MASK, (int)foptsLen)];
// TODO it COULD be the case that the assumed FCnt rollover is incorrect, if possible figure out a way to catch this and retry with just fcnt16
// if there are <= 15 bytes of FOpts, they are in the FHDR, otherwise they are in the payload
// in case of the latter, process AES is if it were a normal payload but using the NwkSEncKey
if(foptsLen <= RADIOLIB_LORAWAN_FHDR_FOPTS_LEN_MASK) {
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);
} else {
processAES(&downlinkMsg[RADIOLIB_LORAWAN_FRAME_PAYLOAD_POS(0)], (size_t)foptsLen, this->nwkSEncKey, fopts, fcnt32, RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, 0x00, true);
}
RADIOLIB_DEBUG_PRINTLN("fopts:");
RADIOLIB_DEBUG_HEXDUMP(fopts, foptsLen);
// process the MAC command(s)
int8_t remLen = foptsLen;
uint8_t* foptsPtr = fopts;
while(remLen > 0) {
uint8_t cid = *foptsPtr;
uint8_t macLen = getMacPayloadLength(cid);
if(macLen + 1 > remLen)
break;
LoRaWANMacCommand_t cmd = {
.cid = cid,
.payload = { 0 },
.len = macLen,
.repeat = 0,
};
memcpy(cmd.payload, foptsPtr + 1, macLen);
RADIOLIB_DEBUG_PRINTLN("[%02X]: %02X %02X %02X %02X %02X (%d)",
cmd.cid, cmd.payload[0], cmd.payload[1], cmd.payload[2], cmd.payload[3], cmd.payload[4], cmd.len);
// process the MAC command
bool sendUp = execMacCommand(&cmd);
if(sendUp) {
pushMacCommand(&cmd, &this->commandsUp);
}
// processing succeeded, move in the buffer to the next command
remLen -= (macLen + 1);
foptsPtr += (macLen + 1);
RADIOLIB_DEBUG_PRINTLN("Processed: %d, remaining: %d", (macLen + 1), remLen);
}
RADIOLIB_DEBUG_PRINTLN("MAC response:");
for (int i = 0; i < this->commandsUp.numCommands; i++) {
RADIOLIB_DEBUG_HEXDUMP(&(this->commandsUp.commands[i].cid), sizeof(LoRaWANMacCommand_t));
}
// if FOptsLen for the next uplink is larger than can be piggybacked onto an uplink, send separate uplink
if(this->commandsUp.len > 15) {
size_t foptsBufSize = this->commandsUp.len;
#if RADIOLIB_STATIC_ONLY
uint8_t foptsBuff[RADIOLIB_STATIC_ARRAY_SIZE];
#else
uint8_t* foptsBuff = new uint8_t[foptsBufSize];
#endif
uint8_t* foptsPtr = foptsBuff;
// append all MAC replies into fopts buffer
int16_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);
}
}
this->isMACPayload = true;
// temporarily lift dutyCycle restrictions to allow immediate MAC response
bool prevDC = this->dutyCycleEnabled;
this->dutyCycleEnabled = false;
RADIOLIB_DEBUG_PRINTLN("Sending MAC-only uplink .. ");
state = this->uplink(foptsBuff, foptsBufSize, RADIOLIB_LORAWAN_FPORT_MAC_COMMAND);
RADIOLIB_DEBUG_PRINTLN(" .. state: %d", state);
this->dutyCycleEnabled = prevDC;
#if !RADIOLIB_STATIC_ONLY
delete[] foptsBuff;
#endif
#if RADIOLIB_STATIC_ONLY
uint8_t strDown[RADIOLIB_STATIC_ARRAY_SIZE];
#else
uint8_t* strDown = new uint8_t[this->band->payloadLenMax[this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK]]];
#endif
size_t lenDown = 0;
RADIOLIB_DEBUG_PRINTLN("Receiving after MAC-only uplink .. ");
state = this->downlink(strDown, &lenDown);
RADIOLIB_DEBUG_PRINTLN(" .. state: %d", state);
#if !RADIOLIB_STATIC_ONLY
delete[] strDown;
#endif
RADIOLIB_ASSERT(state);
}
}
// a downlink was received, so reset the ADR counter to this uplink's fcnt
this->adrFcnt = this->fcntUp;
// pass the extra info if requested
if(event) {
event->dir = RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK;
event->confirmed = isConfirmedDown;
event->confirming = isConfirmingUp;
event->datarate = this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK];
event->freq = currentChannels[event->dir].freq;
event->power = this->txPowerMax - this->txPowerCur * 2;
event->fcnt = isAppDownlink ? this->aFcntDown : this->nFcntDown;
event->port = downlinkMsg[RADIOLIB_LORAWAN_FHDR_FPORT_POS(foptsLen)];
}
// process Application payload (if there is any)
if(payLen <= 0 || foptsLen > RADIOLIB_LORAWAN_FHDR_FOPTS_MAX_LEN) {
// no payload
*len = 0;
#if !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;
// 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
processAES(&downlinkMsg[RADIOLIB_LORAWAN_FRAME_PAYLOAD_POS(foptsLen)], payLen - 1, this->appSKey, data, fcnt32, RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, 0x00, true);
#if !RADIOLIB_STATIC_ONLY
delete[] downlinkMsg;
#endif
return(RADIOLIB_ERR_NONE);
}
#if defined(RADIOLIB_BUILD_ARDUINO)
int16_t LoRaWANNode::sendReceive(String& strUp, uint8_t port, String& strDown, bool isConfirmed, LoRaWANEvent_t* eventUp, LoRaWANEvent_t* eventDown) {
// send the uplink
int16_t state = this->uplink(strUp, port, isConfirmed, eventUp);
RADIOLIB_ASSERT(state);
// wait for the downlink
state = this->downlink(strDown, eventDown);
return(state);
}
#endif
int16_t LoRaWANNode::sendReceive(const char* strUp, uint8_t port, uint8_t* dataDown, size_t* lenDown, bool isConfirmed, LoRaWANEvent_t* eventUp, LoRaWANEvent_t* eventDown) {
// send the uplink
int16_t state = this->uplink(strUp, port, isConfirmed, eventUp);
RADIOLIB_ASSERT(state);
// wait for the downlink
state = this->downlink(dataDown, lenDown, eventDown);
return(state);
}
int16_t LoRaWANNode::sendReceive(uint8_t* dataUp, size_t lenUp, uint8_t port, uint8_t* dataDown, size_t* lenDown, bool isConfirmed, LoRaWANEvent_t* eventUp, LoRaWANEvent_t* eventDown) {
// send the uplink
int16_t state = this->uplink(dataUp, lenUp, port, isConfirmed, eventUp);
RADIOLIB_ASSERT(state);
// wait for the downlink
state = this->downlink(dataDown, lenDown, eventDown);
return(state);
}
void LoRaWANNode::setDeviceStatus(uint8_t battLevel) {
this->battLevel = battLevel;
}
uint32_t LoRaWANNode::getFcntUp() {
return(this->fcntUp);
}
uint32_t LoRaWANNode::getNFcntDown() {
return(this->nFcntDown);
}
uint32_t LoRaWANNode::getAFcntDown() {
return(this->aFcntDown);
}
void LoRaWANNode::resetFcntDown() {
this->nFcntDown = 0;
this->aFcntDown = 0;
}
uint32_t LoRaWANNode::generateMIC(uint8_t* msg, size_t len, uint8_t* key) {
if((msg == NULL) || (len == 0)) {
return(0);
}
RadioLibAES128Instance.init(key);
uint8_t cmac[RADIOLIB_AES128_BLOCK_SIZE];
RadioLibAES128Instance.generateCMAC(msg, len, cmac);
return(((uint32_t)cmac[0]) | ((uint32_t)cmac[1] << 8) | ((uint32_t)cmac[2] << 16) | ((uint32_t)cmac[3]) << 24);
}
bool LoRaWANNode::verifyMIC(uint8_t* msg, size_t len, uint8_t* key) {
if((msg == NULL) || (len < sizeof(uint32_t))) {
return(0);
}
// extract MIC from the message
uint32_t micReceived = LoRaWANNode::ntoh<uint32_t>(&msg[len - sizeof(uint32_t)]);
// calculate the expected value and compare
uint32_t micCalculated = generateMIC(msg, len - sizeof(uint32_t), key);
if(micCalculated != micReceived) {
RADIOLIB_DEBUG_PRINTLN("MIC mismatch, expected %08x, got %08x", micCalculated, micReceived);
return(false);
}
return(true);
}
int16_t LoRaWANNode::setPhyProperties() {
// set the physical layer configuration
int8_t pwr = this->txPowerMax - this->txPowerCur * 2;
int16_t state = RADIOLIB_ERR_INVALID_OUTPUT_POWER;
while(state == RADIOLIB_ERR_INVALID_OUTPUT_POWER) {
// go from the highest power and lower it until we hit one supported by the module
state = this->phyLayer->setOutputPower(pwr--);
}
RADIOLIB_ASSERT(state);
uint8_t syncWord[3] = { 0 };
uint8_t syncWordLen = 0;
size_t preLen = 0;
if(this->FSK) {
preLen = 8*RADIOLIB_LORAWAN_GFSK_PREAMBLE_LEN;
syncWord[0] = (uint8_t)(RADIOLIB_LORAWAN_GFSK_SYNC_WORD >> 16);
syncWord[1] = (uint8_t)(RADIOLIB_LORAWAN_GFSK_SYNC_WORD >> 8);
syncWord[2] = (uint8_t)RADIOLIB_LORAWAN_GFSK_SYNC_WORD;
syncWordLen = 3;
} else {
preLen = RADIOLIB_LORAWAN_LORA_PREAMBLE_LEN;
syncWord[0] = RADIOLIB_LORAWAN_LORA_SYNC_WORD;
syncWordLen = 1;
}
state = this->phyLayer->setSyncWord(syncWord, syncWordLen);
RADIOLIB_ASSERT(state);
state = this->phyLayer->setPreambleLength(preLen);
return(state);
}
int16_t LoRaWANNode::setupChannels(uint8_t* cfList) {
RADIOLIB_DEBUG_PRINTLN("Setting up channels");
// in case of frequency list-type band, copy the default TX channels into the available channels, with RX1 = TX
if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) {
RADIOLIB_DEBUG_PRINTLN("Dynamic band");
size_t num = 0;
// copy the default defined channels into the first slots
for(; num < 3 && this->band->txFreqs[num].enabled; num++) {
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num] = this->band->txFreqs[num];
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][num] = this->band->txFreqs[num];
RADIOLIB_DEBUG_PRINTLN("Channel UL/DL %d frequency = %f MHz", this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].idx, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].freq);
}
// if there is a cflist present, parse its frequencies into the next five slots, with datarate range copied from default channel 0
if(cfList != nullptr) {
RADIOLIB_DEBUG_PRINTLN("CFList present");
LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 };
cmd.cid = RADIOLIB_LORAWAN_MAC_NEW_CHANNEL;
cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_NEW_CHANNEL].lenDn;
// datarate range for all new channels is equal to the default channels
cmd.payload[4] = (this->band->txFreqs[0].drMax << 4) | this->band->txFreqs[0].drMin;
for(uint8_t i = 0; i < 5; i++, num++) {
cmd.payload[0] = num;
memcpy(&cmd.payload[1], &cfList[i*3], 3);
(void)execMacCommand(&cmd);
}
}
for(; num < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; num++) {
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num] = RADIOLIB_LORAWAN_CHANNEL_NONE;
}
} else { // RADIOLIB_LORAWAN_BAND_FIXED
if(cfList != nullptr) {
RADIOLIB_DEBUG_PRINTLN("CFList present");
LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 };
cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR;
cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn;
cmd.payload[0] = 0xFF; // same datarate and payload
// in case of mask-type bands, copy those frequencies that are masked true into the available TX channels
size_t numChMasks = 3 + this->band->numTxSpans; // 4 masks for bands with 2 spans, 5 spans for bands with 1 span
for(size_t chMaskCntl = 0; chMaskCntl < numChMasks; chMaskCntl++) {
cmd.payload[3] = chMaskCntl << 4; // NbTrans = 0 -> keep the same
memcpy(&cmd.payload[1], &cfList[chMaskCntl*2], 2);
(void)execMacCommand(&cmd);
}
}
}
for (int i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) {
RADIOLIB_DEBUG_PRINTLN("UL: %d %d %5.2f (%d - %d) | DL: %d %d %5.2f (%d - %d)",
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx,
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled,
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].freq,
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].drMin,
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].drMax,
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].idx,
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].enabled,
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].freq,
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].drMin,
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][i].drMax
);
}
return(RADIOLIB_ERR_NONE);
}
int16_t LoRaWANNode::selectSubband(uint8_t idx) {
int16_t state = this->selectSubband((idx - 1) * 8, idx * 8 - 1);
return(state);
}
int16_t LoRaWANNode::selectSubband(uint8_t startChannel, uint8_t endChannel) {
if(this->activeMode != RADIOLIB_LORAWAN_MODE_NONE) {
RADIOLIB_DEBUG_PRINTLN("There is already an active session - cannot change subband");
return(RADIOLIB_ERR_INVALID_CHANNEL);
}
if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) {
RADIOLIB_DEBUG_PRINTLN("This is a dynamic band plan which does not support subbands");
return(RADIOLIB_ERR_INVALID_CHANNEL);
}
this->selectedSubband = startChannel % 8; // save selected subband - assumed a block of 8 channels
uint8_t numChannels = endChannel - startChannel + 1;
if(startChannel > this->band->txSpans[0].numChannels) {
RADIOLIB_DEBUG_PRINTLN("There are only %d channels available in this band", this->band->txSpans[0].numChannels);
return(RADIOLIB_ERR_INVALID_CHANNEL);
}
if(startChannel + numChannels > this->band->txSpans[0].numChannels) {
numChannels = this->band->txSpans[0].numChannels - startChannel;
RADIOLIB_DEBUG_PRINTLN("Could only select %d channels due to end of band", numChannels);
}
if(numChannels > RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS) {
numChannels = RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS;
RADIOLIB_DEBUG_PRINTLN("Could only select %d channels due to specified limit", numChannels);
}
LoRaWANChannel_t chnl;
for(size_t chNum = 0; chNum < numChannels; chNum++) {
chnl.enabled = true;
chnl.idx = startChannel + chNum;
chnl.freq = this->band->txSpans[0].freqStart + chnl.idx*this->band->txSpans[0].freqStep;
chnl.drMin = this->band->txSpans[0].drMin;
chnl.drMax = this->band->txSpans[0].drMax;
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chNum] = chnl;
// downlink channel is dynamically calculated on each uplink in selectChannels()
RADIOLIB_DEBUG_PRINTLN("Channel UL %d frequency = %f MHz", chNum, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chNum].freq);
}
return(RADIOLIB_ERR_NONE);
}
int16_t LoRaWANNode::selectChannelsJR(uint16_t devNonce, uint8_t joinDr) {
LoRaWANChannel_t channelUp;
LoRaWANChannel_t channelDown;
uint8_t drUp;
uint8_t drDown;
if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) {
// count the number of available channels for a join-request (default channels + join-request channels)
uint8_t numJRChannels = 0;
for(size_t i = 0; i < 3; i++) {
if(this->band->txFreqs[i].enabled) {
numJRChannels++;
}
if(this->band->txJoinReq[i].enabled) {
numJRChannels++;
}
}
// cycle through the available channels (seed with devNonce)
uint8_t channelId = devNonce % numJRChannels;
// find the channel whose index is selected
for(size_t i = 0; i < 3; i++) {
if(this->band->txFreqs[i].idx == channelId) {
channelUp = this->band->txFreqs[i];
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(joinDr != RADIOLIB_LORAWAN_DATA_RATE_UNUSED) {
if(joinDr >= channelUp.drMin && joinDr <= channelUp.drMax) {
drUp = joinDr;
} else {
RADIOLIB_DEBUG_PRINTLN("Datarate %d is not valid (min: %d, max %d) - using default", joinDr, channelUp.drMin, channelUp.drMax);
joinDr = RADIOLIB_LORAWAN_DATA_RATE_UNUSED;
}
}
if(joinDr == RADIOLIB_LORAWAN_DATA_RATE_UNUSED) {
drUp = int((channelUp.drMax + channelUp.drMin) / 2);
}
// derive the downlink channel and datarate from the uplink channel and datarate
channelDown = channelUp;
drDown = getDownlinkDataRate(drUp, this->rx1DrOffset, this->band->rx1DataRateBase, channelDown.drMin, channelDown.drMax);
} else { // RADIOLIB_LORAWAN_BAND_FIXED
uint8_t spanID = 0;
uint8_t channelID = 0;
uint8_t numEnabledChannels = 0;
// if there are any predefined channels because user selected a subband, select one of these channels
for(; numEnabledChannels < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; numEnabledChannels++) {
if(this->availableChannels[numEnabledChannels][RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK].enabled == false) {
break;
}
}
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;
}
// 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] = channelDown;
this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = drUp;
this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = drDown;
return(RADIOLIB_ERR_NONE);
}
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;
}
}
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] = this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][channelID];
if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) {
// for dynamic bands, the downlink channel is the one matched to the uplink channel
this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][channelID];
} else { // RADIOLIB_LORAWAN_BAND_FIXED
// for fixed bands, the downlink channel is the uplink channel ID `modulo` number of downlink channels
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;
}
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, bool saveToEeprom) {
// scan through all enabled channels and check if the requested datarate is available
bool isValidDR = false;
for(size_t i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) {
LoRaWANChannel_t *chnl = &(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i]);
if(chnl->enabled) {
if(drUp > chnl->drMin && drUp < chnl->drMax) {
isValidDR = true;
break;
}
}
}
if(!isValidDR) {
RADIOLIB_DEBUG_PRINTLN("No defined channel allows datarate %d", drUp);
return(RADIOLIB_ERR_DATA_RATE_INVALID);
}
LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 };
cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR;
cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn;
cmd.payload[0] = (drUp << 4);
cmd.payload[0] |= 0x0F; // keep Tx Power the same
cmd.payload[3] |= (1 << 7); // set the RFU bit, which means that the channel mask gets ignored
cmd.payload[3] |= 0; // keep NbTrans the same
(void)execMacCommand(&cmd, saveToEeprom);
// check if ACK is set for Tx Power
if((cmd.payload[0] >> 1) != 1) {
return(RADIOLIB_ERR_DATA_RATE_INVALID);
}
return(RADIOLIB_ERR_NONE);
}
void LoRaWANNode::setADR(bool enable) {
this->adrEnabled = enable;
}
void LoRaWANNode::setDutyCycle(bool enable, uint32_t msPerHour) {
this->dutyCycleEnabled = true;
if(msPerHour <= 0) {
this->dutyCycle = this->band->dutyCycle;
} else {
this->dutyCycle = msPerHour;
}
}
// given an airtime in milliseconds, calculate the minimum uplink interval
// to adhere to a given dutyCycle
uint32_t LoRaWANNode::dutyCycleInterval(uint32_t msPerHour, uint32_t airtime) {
if(msPerHour == 0 || airtime == 0) {
return(0);
}
uint32_t oneHourInMs = 60 * 60 * 1000;
float numPackets = msPerHour / airtime;
uint32_t delayMs = oneHourInMs / numPackets + 1; // + 1 to prevent rounding problems
return(delayMs);
}
uint32_t LoRaWANNode::timeUntilUplink() {
Module* mod = this->phyLayer->getMod();
uint32_t nextUplink = this->rxDelayStart + dutyCycleInterval(this->dutyCycle, this->lastToA);
if(mod->hal->millis() > nextUplink){
return(0);
}
return(nextUplink - mod->hal->millis() + 1);
}
void LoRaWANNode::setDwellTime(bool enable, uint32_t msPerUplink) {
this->dwellTimeEnabledUp = enable;
if(msPerUplink <= 0) {
this->dwellTimeUp = this->band->dwellTimeUp;
} else {
this->dwellTimeUp = msPerUplink;
}
}
uint8_t LoRaWANNode::maxPayloadDwellTime() {
// configure current datarate
DataRate_t dr;
findDataRate(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK], &dr);
(void)this->phyLayer->setDataRate(dr);
uint8_t minPayLen = 0;
uint8_t maxPayLen = 255;
uint8_t payLen = (minPayLen + maxPayLen) / 2;
// do some binary search to find maximum allowed payload length
while(payLen != minPayLen && payLen != maxPayLen) {
if(this->phyLayer->getTimeOnAir(payLen) > this->dwellTimeUp) {
maxPayLen = payLen;
} else {
minPayLen = payLen;
}
payLen = (minPayLen + maxPayLen) / 2;
}
return(payLen - 13); // fixed 13-byte header
}
int16_t LoRaWANNode::setTxPower(int8_t txPower, bool saveToEeprom) {
// only allow values within the band's (or MAC state) maximum
if(txPower > this->txPowerMax) {
return(RADIOLIB_ERR_INVALID_OUTPUT_POWER);
}
// Tx Power is set in steps of two
// the selected value is rounded down to nearest multiple of two away from txPowerMax
// e.g. on EU868, max is 16; if 13 is selected then we set to 12
uint8_t txPowerNew = (this->txPowerMax - txPower) / 2 + 1;
LoRaWANMacCommand_t cmd = { 0, 0, 0, 0 };
cmd.cid = RADIOLIB_LORAWAN_MAC_LINK_ADR;
cmd.len = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn;
cmd.payload[0] = 0xF0; // keep datarate the same
cmd.payload[0] |= txPowerNew; // set the Tx Power
cmd.payload[3] |= (1 << 7); // set the RFU bit, which means that the channel mask gets ignored
cmd.payload[3] |= 0; // keep NbTrans the same
(void)execMacCommand(&cmd, saveToEeprom);
// check if ACK is set for Tx Power
if((cmd.payload[0] >> 2) != 1) {
return(RADIOLIB_ERR_INVALID_OUTPUT_POWER);
}
return(RADIOLIB_ERR_NONE);
}
int16_t LoRaWANNode::findDataRate(uint8_t dr, DataRate_t* dataRate) {
uint8_t dataRateBand = this->band->dataRates[dr];
if(dataRateBand & RADIOLIB_LORAWAN_DATA_RATE_FSK_50_K) {
dataRate->fsk.bitRate = 50;
dataRate->fsk.freqDev = 25;
} else {
uint8_t bw = dataRateBand & 0x0C;
switch(bw) {
case(RADIOLIB_LORAWAN_DATA_RATE_BW_125_KHZ):
dataRate->lora.bandwidth = 125.0;
break;
case(RADIOLIB_LORAWAN_DATA_RATE_BW_250_KHZ):
dataRate->lora.bandwidth = 250.0;
break;
case(RADIOLIB_LORAWAN_DATA_RATE_BW_500_KHZ):
dataRate->lora.bandwidth = 500.0;
break;
default:
dataRate->lora.bandwidth = 125.0;
}
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(RADIOLIB_ERR_NONE);
}
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);
int state = this->phyLayer->setFrequency(this->currentChannels[dir].freq);
RADIOLIB_ASSERT(state);
// 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);
}
return(state);
}
bool LoRaWANNode::sendMacCommandReq(uint8_t cid) {
bool valid = false;
for(size_t i = 0; i < RADIOLIB_LORAWAN_NUM_MAC_COMMANDS; i++) {
if(MacTable[i].cid == cid) {
valid = MacTable[i].user;
}
}
if(!valid)
return(false);
LoRaWANMacCommand_t cmd = { cid, 0, 0, 0 };
pushMacCommand(&cmd, &this->commandsUp);
return(true);
}
int16_t LoRaWANNode::pushMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQueue_t* queue) {
if(queue->numCommands >= RADIOLIB_LORAWAN_MAC_COMMAND_QUEUE_SIZE) {
return(RADIOLIB_ERR_COMMAND_QUEUE_FULL);
}
memcpy(&queue->commands[queue->numCommands], cmd, sizeof(LoRaWANMacCommand_t));
queue->numCommands++;
queue->len += 1 + cmd->len; // 1 byte for command ID, len bytes for payload
return(RADIOLIB_ERR_NONE);
}
int16_t LoRaWANNode::deleteMacCommand(uint8_t cid, LoRaWANMacCommandQueue_t* queue, uint8_t payload[5]) {
if(queue->numCommands == 0) {
return(RADIOLIB_ERR_COMMAND_QUEUE_EMPTY);
}
for(size_t index = 0; index < queue->numCommands; index++) {
if(queue->commands[index].cid == cid) {
// if a pointer to a payload is supplied, copy the command's payload over
if(payload) {
memcpy(payload, queue->commands[index].payload, queue->commands[index].len);
}
queue->len -= (1 + queue->commands[index].len); // 1 byte for command ID, len for payload
// move all subsequent commands one forward in the queue
if(index < RADIOLIB_LORAWAN_MAC_COMMAND_QUEUE_SIZE - 1) {
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));
queue->numCommands--;
return(RADIOLIB_ERR_NONE);
}
}
return(RADIOLIB_ERR_COMMAND_QUEUE_ITEM_NOT_FOUND);
}
bool LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd, bool saveToEeprom) {
RADIOLIB_DEBUG_PRINTLN("exe MAC CID = %02x, len = %d", cmd->cid, cmd->len);
Module* mod = this->phyLayer->getMod();
#if defined(RADIOLIB_EEPROM_UNSUPPORTED)
(void)saveToEeprom;
(void)mod;
#endif
if(cmd->cid >= RADIOLIB_LORAWAN_MAC_PROPRIETARY) {
// TODO call user-provided callback for proprietary MAC commands?
return(false);
}
switch(cmd->cid) {
case(RADIOLIB_LORAWAN_MAC_RESET): {
// get the server version
uint8_t srvVersion = cmd->payload[0];
RADIOLIB_DEBUG_PRINTLN("Server version: 1.%d", srvVersion);
if(srvVersion == this->rev) {
// valid server version, stop sending the ResetInd MAC command
deleteMacCommand(RADIOLIB_LORAWAN_MAC_RESET, &this->commandsUp);
}
return(false);
} break;
case(RADIOLIB_LORAWAN_MAC_LINK_CHECK): {
pushMacCommand(cmd, &this->commandsDown);
return(false);
} break;
case(RADIOLIB_LORAWAN_MAC_LINK_ADR): {
// get the ADR configuration
// per spec, all these configuration should only be set if all ACKs are set, otherwise retain previous state
// but we don't bother and try to set each individual command
uint8_t drUp = (cmd->payload[0] & 0xF0) >> 4;
uint8_t txPower = cmd->payload[0] & 0x0F;
uint16_t chMask = LoRaWANNode::ntoh<uint16_t>(&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", drUp, txPower, chMask, chMaskCntl, nbTrans);
// apply the configuration
uint8_t drAck = 0;
if(drUp == 0x0F) { // keep the same
drAck = 1;
// replace the 'placeholder' with the current actual value for saving
cmd->payload[0] = (cmd->payload[0] & 0x0F) | (this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] << 4);
} else if (this->band->dataRates[drUp] != RADIOLIB_LORAWAN_DATA_RATE_UNUSED) {
uint8_t drDown = getDownlinkDataRate(drUp, this->rx1DrOffset, this->band->rx1DataRateBase,
this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK].drMin, this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK].drMax);
this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK] = drUp;
this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK] = drDown;
drAck = 1;
}
// try to apply the power configuration
uint8_t pwrAck = 0;
if(txPower == 0x0F) {
pwrAck = 1;
// replace the 'placeholder' with the current actual value for saving
cmd->payload[0] = (cmd->payload[0] & 0xF0) | this->txPowerCur;
} else {
int8_t pwr = this->txPowerMax - 2*txPower;
int16_t state = RADIOLIB_ERR_INVALID_OUTPUT_POWER;
while(state == RADIOLIB_ERR_INVALID_OUTPUT_POWER) {
// go from the highest power and lower it until we hit one supported by the module
state = this->phyLayer->setOutputPower(pwr--);
}
// only acknowledge if the requested datarate was succesfully configured
if(state == RADIOLIB_ERR_NONE) {
pwrAck = 1;
this->txPowerCur = txPower;
}
}
bool isSuccessive = false;
uint8_t chMaskAck = 1;
// only apply channel mask when the RFU bit is not set
// (which is set on the internal MAC command when creating new session)
if((cmd->payload[3] >> 7) == 0) {
if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) {
for(size_t i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) {
if(chMaskCntl == 0) {
// if chMaskCntl == 0, apply the mask by looking at each channel bit
RADIOLIB_DEBUG_PRINTLN("ADR channel %d: %d --> %d", this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled, (chMask >> i) & 0x01);
if(chMask & (1UL << i)) {
// if it should be enabled but is not currently defined, stop immediately
if(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx == RADIOLIB_LORAWAN_CHANNEL_INDEX_NONE) {
chMaskAck = 0;
break;
}
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = true;
} else {
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = false;
}
} else if(chMaskCntl == 6) {
// if chMaskCntl == 6, enable all defined channels
if(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].idx != RADIOLIB_LORAWAN_CHANNEL_INDEX_NONE) {
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = true;
}
}
}
} else { // RADIOLIB_LORAWAN_BAND_FIXED
// delete any prior ADR responses from the uplink queue, but do not care if none is present yet
int16_t state = deleteMacCommand(RADIOLIB_LORAWAN_MAC_LINK_ADR, &this->commandsUp);
if(state == RADIOLIB_ERR_NONE) {
isSuccessive = true; // if we found an ADR Ans in the uplink MAC queue, this is a successive ADR MAC request
}
RADIOLIB_DEBUG_PRINTLN("mask[%d] = 0x%04x", chMaskCntl, chMask);
uint8_t num = 0;
uint8_t chNum = chMaskCntl*16;
uint8_t chSpan = 0;
for(size_t i = 0; i < RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS; i++) {
RADIOLIB_DEBUG_PRINTLN("chNum: %d, chSpan: %d, i: %d, mask: %d", chNum, chSpan, i, chMask & (1UL << i));
// if we must roll over to next span, reset chNum and move to next channel span
if(chNum >= this->band->txSpans[chSpan].numChannels) {
chNum = 0;
chSpan++;
}
if(chMask & (1UL << i)) {
if(chSpan >= this->band->numTxSpans) {
RADIOLIB_DEBUG_PRINTLN("channel bitmask overrun!");
return(RADIOLIB_ERR_UNKNOWN);
}
LoRaWANChannel_t chnl;
chnl.enabled = true;
chnl.idx = chMaskCntl*16 + i;
chnl.freq = this->band->txSpans[chSpan].freqStart + chNum*this->band->txSpans[chSpan].freqStep;
chnl.drMin = this->band->txSpans[chSpan].drMin;
chnl.drMax = this->band->txSpans[chSpan].drMax;
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num] = chnl;
// downlink channels are dynamically calculated on each uplink in selectChannels()
RADIOLIB_DEBUG_PRINTLN("Channel UL %d (%d) frequency = %f MHz", num, chnl.idx, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].freq);
num++;
}
chNum++;
}
}
}
if(nbTrans == 0) { // keep the same
cmd->payload[3] = (cmd->payload[3] & 0xF0) | this->nbTrans; // set current number of retransmissions for saving
} else {
this->nbTrans = nbTrans;
}
#if !defined(RADIOLIB_EEPROM_UNSUPPORTED)
if(saveToEeprom) {
uint8_t payLen = MacTable[RADIOLIB_LORAWAN_MAC_LINK_ADR].lenDn;
if(this->band->bandType == RADIOLIB_LORAWAN_BAND_DYNAMIC) {
// if RFU bit is set, this is just a change in Datarate or TxPower, so read ADR command and overwrite first byte
if((cmd->payload[3] >> 7) == 1) {
mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_LINK_ADR_ID) + 1, &(cmd->payload[1]), 3);
}
// save to the single ADR MAC location
mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_LINK_ADR_ID), &(cmd->payload[0]), payLen);
} else {
// read how many ADR masks are already stored
uint8_t macNumADR = mod->hal->getPersistentParameter<uint8_t>(RADIOLIB_EEPROM_LORAWAN_NUM_ADR_MASKS_ID);
// if RFU bit is set, this is just a change in Datarate or TxPower, so read ADR command and overwrite first byte
if((cmd->payload[3] >> 7) == 1) {
mod->hal->readPersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_UL_CHANNELS_ID) + macNumADR * payLen + 1, &(cmd->payload[1]), 3);
} else {
if(isSuccessive) {
// saved another ADR mask, so increase counter
mod->hal->setPersistentParameter<uint8_t>(RADIOLIB_EEPROM_LORAWAN_NUM_ADR_MASKS_ID, macNumADR + 1);
} else {
// this is the first ADR mask in this downlink, so (re)set counter to 1
mod->hal->setPersistentParameter<uint8_t>(RADIOLIB_EEPROM_LORAWAN_NUM_ADR_MASKS_ID, 1);
}
}
// save to the uplink channel location, to the macNumADR-th slot of 4 bytes
mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_UL_CHANNELS_ID) + macNumADR * payLen, &(cmd->payload[0]), payLen);
}
}
#endif
// send the reply
cmd->len = 1;
cmd->payload[0] = (pwrAck << 2) | (drAck << 1) | (chMaskAck << 0);
RADIOLIB_DEBUG_PRINTLN("ADR ANS: status = 0x%02x", cmd->payload[0]);
return(true);
} break;
case(RADIOLIB_LORAWAN_MAC_DUTY_CYCLE): {
uint8_t maxDutyCycle = cmd->payload[0] & 0x0F;
RADIOLIB_DEBUG_PRINTLN("Max duty cycle: 1/2^%d", maxDutyCycle);
if(maxDutyCycle == 0) {
this->dutyCycle = this->band->dutyCycle;
} else {
this->dutyCycle = 60 * 60 * 1000 / (1 << maxDutyCycle);
}
#if !defined(RADIOLIB_EEPROM_UNSUPPORTED)
if(saveToEeprom) {
mod->hal->setPersistentParameter<uint8_t>(RADIOLIB_EEPROM_LORAWAN_DUTY_CYCLE_ID, cmd->payload[0]);
}
#endif
cmd->len = 0;
return(true);
} break;
case(RADIOLIB_LORAWAN_MAC_RX_PARAM_SETUP): {
// get the configuration
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<uint32_t>(&cmd->payload[1], 3);
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
uint8_t chanAck = 0;
if(this->phyLayer->setFrequency(this->rx2.freq) == RADIOLIB_ERR_NONE) {
chanAck = 1;
this->phyLayer->setFrequency(this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK].freq);
}
#if !defined(RADIOLIB_EEPROM_UNSUPPORTED)
if(saveToEeprom) {
uint8_t payLen = MacTable[RADIOLIB_LORAWAN_MAC_RX_PARAM_SETUP].lenDn;
mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_RX_PARAM_SETUP_ID), &(cmd->payload[0]), payLen);
}
#endif
// TODO this should be sent repeatedly until the next downlink
cmd->len = 1;
cmd->payload[0] = (rx1OffsAck << 2) | (rx2Ack << 1) | (chanAck << 0);
RADIOLIB_DEBUG_PRINTLN("Rx param ANS: status = 0x%02x", cmd->payload[0]);
return(true);
} break;
case(RADIOLIB_LORAWAN_MAC_DEV_STATUS): {
// set the uplink reply
cmd->len = 2;
cmd->payload[1] = this->battLevel;
int8_t snr = this->phyLayer->getSNR();
cmd->payload[0] = snr & 0x3F;
RADIOLIB_DEBUG_PRINTLN("DevStatus ANS: status = 0x%02x%02x", cmd->payload[0], cmd->payload[1]);
return(true);
} break;
case(RADIOLIB_LORAWAN_MAC_NEW_CHANNEL): {
// get the configuration
uint8_t chIndex = cmd->payload[0];
uint32_t freqRaw = LoRaWANNode::ntoh<uint32_t>(&cmd->payload[1], 3);
float freq = (float)freqRaw/10000.0;
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;
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chIndex].enabled = true;
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chIndex].idx = chIndex;
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chIndex].freq = freq;
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chIndex].drMin = minDr;
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chIndex].drMax = maxDr;
// downlink channel is identical to uplink channel
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][chIndex] = this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chIndex];
newChAck = 1;
// check if the frequency is possible
if(this->phyLayer->setFrequency(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chIndex].freq) == RADIOLIB_ERR_NONE) {
freqAck = 1;
this->phyLayer->setFrequency(this->currentChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK].freq);
}
RADIOLIB_DEBUG_PRINTLN("UL: %d %d %5.2f (%d - %d) | DL: %d %d %5.2f (%d - %d)",
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chIndex].idx,
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chIndex].enabled,
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chIndex].freq,
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chIndex].drMin,
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][chIndex].drMax,
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][chIndex].idx,
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][chIndex].enabled,
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][chIndex].freq,
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][chIndex].drMin,
this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][chIndex].drMax
);
#if !defined(RADIOLIB_EEPROM_UNSUPPORTED)
if(saveToEeprom) {
// save to uplink channels location, to the chIndex-th slot of 5 bytes
uint8_t payLen = MacTable[RADIOLIB_LORAWAN_MAC_NEW_CHANNEL].lenDn;
RADIOLIB_DEBUG_PRINTLN("Saving channel:");
RADIOLIB_DEBUG_HEXDUMP(&(cmd->payload[0]), payLen);
mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_UL_CHANNELS_ID) + chIndex * payLen, &(cmd->payload[0]), payLen);
}
#endif
// send the reply
cmd->len = 1;
cmd->payload[0] = (newChAck << 1) | (freqAck << 0);
return(true);
} break;
case(RADIOLIB_LORAWAN_MAC_DL_CHANNEL): {
// get the configuration
uint8_t chIndex = cmd->payload[0];
uint32_t freqRaw = LoRaWANNode::ntoh<uint32_t>(&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;
}
}
}
#if !defined(RADIOLIB_EEPROM_UNSUPPORTED)
if(saveToEeprom) {
// save to downlink channels location, to the chIndex-th slot of 4 bytes
uint8_t payLen = MacTable[RADIOLIB_LORAWAN_MAC_DL_CHANNEL].lenDn;
mod->hal->writePersistentStorage(mod->hal->getPersistentAddr(RADIOLIB_EEPROM_LORAWAN_DL_CHANNELS_ID) + chIndex * payLen, &(cmd->payload[0]), payLen);
}
#endif
// TODO send this repeatedly until a downlink is received
cmd->len = 1;
cmd->payload[0] = (freqUlAck << 1) | (freqDlAck << 0);
return(true);
} break;
case(RADIOLIB_LORAWAN_MAC_RX_TIMING_SETUP): {
// get the configuration
uint8_t delay = cmd->payload[0] & 0x0F;
RADIOLIB_DEBUG_PRINTLN("RX timing: delay = %d sec", delay);
// apply the configuration
if(delay == 0) {
delay = 1;
}
this->rxDelays[0] = delay * 1000;
this->rxDelays[1] = this->rxDelays[0] + 1000;
#if !defined(RADIOLIB_EEPROM_UNSUPPORTED)
if(saveToEeprom) {
mod->hal->setPersistentParameter<uint8_t>(RADIOLIB_EEPROM_LORAWAN_RX_TIMING_SETUP_ID, cmd->payload[0]);
}
#endif
// send the reply
cmd->len = 0;
// TODO send this repeatedly until a downlink is received
return(true);
} break;
case(RADIOLIB_LORAWAN_MAC_TX_PARAM_SETUP): {
uint8_t dlDwell = (cmd->payload[0] & 0x20) >> 5;
uint8_t ulDwell = (cmd->payload[0] & 0x10) >> 4;
uint8_t maxEirpRaw = cmd->payload[0] & 0x0F;
// who the f came up with this ...
const uint8_t eirpEncoding[] = { 8, 10, 12, 13, 14, 16, 18, 20, 21, 24, 26, 27, 29, 30, 33, 36 };
this->txPowerMax = eirpEncoding[maxEirpRaw];
RADIOLIB_DEBUG_PRINTLN("TX timing: dlDwell = %d, ulDwell = %d, maxEirp = %d dBm", dlDwell, ulDwell, this->txPowerMax);
this->dwellTimeEnabledUp = ulDwell ? true : false;
this->dwellTimeUp = ulDwell ? RADIOLIB_LORAWAN_DWELL_TIME : 0;
this->dwellTimeEnabledDn = dlDwell ? true : false;
this->dwellTimeDn = dlDwell ? RADIOLIB_LORAWAN_DWELL_TIME : 0;
#if !defined(RADIOLIB_EEPROM_UNSUPPORTED)
if(saveToEeprom) {
mod->hal->setPersistentParameter<uint8_t>(RADIOLIB_EEPROM_LORAWAN_TX_PARAM_SETUP_ID, cmd->payload[0]);
}
#endif
cmd->len = 0;
return(true);
} break;
case(RADIOLIB_LORAWAN_MAC_REKEY): {
// get the server version
uint8_t srvVersion = cmd->payload[0];
RADIOLIB_DEBUG_PRINTLN("Server version: 1.%d", srvVersion);
if((srvVersion > 0) && (srvVersion <= this->rev)) {
// valid server version, stop sending the ReKey MAC command
deleteMacCommand(RADIOLIB_LORAWAN_MAC_REKEY, &this->commandsUp);
}
return(false);
} break;
case(RADIOLIB_LORAWAN_MAC_ADR_PARAM_SETUP): {
this->adrLimitExp = (cmd->payload[0] & 0xF0) >> 4;
this->adrDelayExp = cmd->payload[0] & 0x0F;
RADIOLIB_DEBUG_PRINTLN("ADR param setup: limitExp = %d, delayExp = %d", this->adrLimitExp, this->adrDelayExp);
#if !defined(RADIOLIB_EEPROM_UNSUPPORTED)
if(saveToEeprom) {
mod->hal->setPersistentParameter<uint8_t>(RADIOLIB_EEPROM_LORAWAN_ADR_PARAM_SETUP_ID, cmd->payload[0]);
}
#endif
cmd->len = 0;
return(true);
} break;
case(RADIOLIB_LORAWAN_MAC_DEVICE_TIME): {
pushMacCommand(cmd, &this->commandsDown);
return(false);
} break;
case(RADIOLIB_LORAWAN_MAC_FORCE_REJOIN): {
// TODO implement this
uint16_t rejoinReq = LoRaWANNode::ntoh<uint16_t>(&cmd->payload[0]);
uint8_t period = (rejoinReq & 0x3800) >> 11;
uint8_t maxRetries = (rejoinReq & 0x0700) >> 8;
uint8_t rejoinType = (rejoinReq & 0x0070) >> 4;
uint8_t dr = rejoinReq & 0x000F;
RADIOLIB_DEBUG_PRINTLN("Force rejoin: period = %d, maxRetries = %d, rejoinType = %d, dr = %d", period, maxRetries, rejoinType, dr);
(void)period;
(void)maxRetries;
(void)rejoinType;
(void)dr;
return(false);
} break;
case(RADIOLIB_LORAWAN_MAC_REJOIN_PARAM_SETUP): {
// TODO implement this
uint8_t maxTime = (cmd->payload[0] & 0xF0) >> 4;
uint8_t maxCount = cmd->payload[0] & 0x0F;
RADIOLIB_DEBUG_PRINTLN("Rejoin setup: maxTime = %d, maxCount = %d", maxTime, maxCount);
#if !defined(RADIOLIB_EEPROM_UNSUPPORTED)
if(saveToEeprom) {
mod->hal->setPersistentParameter<uint8_t>(RADIOLIB_EEPROM_LORAWAN_REJOIN_PARAM_SETUP_ID, cmd->payload[0]);
}
#endif
cmd->len = 0;
cmd->payload[0] = (1 << 1) | 1;
(void)maxTime;
(void)maxCount;
return(true);
} break;
}
return(false);
}
uint8_t LoRaWANNode::getMacPayloadLength(uint8_t cid) {
for (LoRaWANMacSpec_t entry : MacTable) {
if (entry.cid == cid) {
return entry.lenDn;
}
}
// no idea about the length
return 0;
}
int16_t LoRaWANNode::getMacLinkCheckAns(uint8_t* margin, uint8_t* gwCnt) {
uint8_t payload[5];
int16_t state = deleteMacCommand(RADIOLIB_LORAWAN_LINK_CHECK_REQ, &this->commandsDown, payload);
RADIOLIB_ASSERT(state);
if(margin) { *margin = payload[0]; }
if(gwCnt) { *gwCnt = payload[1]; }
// RADIOLIB_DEBUG_PRINTLN("Link check: margin = %d dB, gwCnt = %d", margin, gwCnt);
return(RADIOLIB_ERR_NONE);
}
int16_t LoRaWANNode::getMacDeviceTimeAns(uint32_t* gpsEpoch, uint8_t* fraction, bool returnUnix) {
uint8_t payload[5];
int16_t state = deleteMacCommand(RADIOLIB_LORAWAN_MAC_DEVICE_TIME, &this->commandsDown, payload);
RADIOLIB_ASSERT(state);
if(gpsEpoch) {
*gpsEpoch = LoRaWANNode::ntoh<uint32_t>(&payload[0]);
if(returnUnix) {
uint32_t unixOffset = 315964800;
*gpsEpoch += unixOffset;
}
}
if(fraction) { *fraction = payload[4]; }
// RADIOLIB_DEBUG_PRINTLN("Network time: gpsEpoch = %d s, delayExp = %f", gpsEpoch, (float)(*fraction)/256.0f);
return(RADIOLIB_ERR_NONE);
}
// 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;
if(len % RADIOLIB_AES128_BLOCK_SIZE) {
numBlocks++;
}
// generate the encryption blocks
uint8_t encBuffer[RADIOLIB_AES128_BLOCK_SIZE] = { 0 };
uint8_t encBlock[RADIOLIB_AES128_BLOCK_SIZE] = { 0 };
encBlock[RADIOLIB_LORAWAN_BLOCK_MAGIC_POS] = RADIOLIB_LORAWAN_ENC_BLOCK_MAGIC;
encBlock[RADIOLIB_LORAWAN_ENC_BLOCK_COUNTER_ID_POS] = ctrId;
encBlock[RADIOLIB_LORAWAN_BLOCK_DIR_POS] = dir;
LoRaWANNode::hton<uint32_t>(&encBlock[RADIOLIB_LORAWAN_BLOCK_DEV_ADDR_POS], this->devAddr);
LoRaWANNode::hton<uint32_t>(&encBlock[RADIOLIB_LORAWAN_BLOCK_FCNT_POS], fcnt);
// now encrypt the input
// 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;
}
// encrypt the buffer
RadioLibAES128Instance.init(key);
RadioLibAES128Instance.encryptECB(encBlock, RADIOLIB_AES128_BLOCK_SIZE, encBuffer);
// now xor the buffer with the input
size_t xorLen = remLen;
if(xorLen > RADIOLIB_AES128_BLOCK_SIZE) {
xorLen = RADIOLIB_AES128_BLOCK_SIZE;
}
for(uint8_t j = 0; j < xorLen; j++) {
out[i*RADIOLIB_AES128_BLOCK_SIZE + j] = in[i*RADIOLIB_AES128_BLOCK_SIZE + j] ^ encBuffer[j];
}
remLen -= xorLen;
}
}
uint16_t LoRaWANNode::checkSum16(uint8_t *key, uint8_t keyLen) {
uint16_t buf16[RADIOLIB_AES128_KEY_SIZE/2];
uint8_t bufLen = keyLen / 2;
memcpy(buf16, key, bufLen);
uint16_t checkSum = 0;
for(int i = 0; i < bufLen; i++) {
checkSum ^= buf16[i];
}
return(checkSum);
}
template<typename T>
T LoRaWANNode::ntoh(uint8_t* buff, size_t size) {
uint8_t* buffPtr = buff;
size_t targetSize = sizeof(T);
if(size != 0) {
targetSize = size;
}
T res = 0;
for(size_t i = 0; i < targetSize; i++) {
res |= (uint32_t)(*(buffPtr++)) << 8*i;
}
return(res);
}
template<typename T>
void LoRaWANNode::hton(uint8_t* buff, T val, size_t size) {
uint8_t* buffPtr = buff;
size_t targetSize = sizeof(T);
if(size != 0) {
targetSize = size;
}
for(size_t i = 0; i < targetSize; i++) {
*(buffPtr++) = val >> 8*i;
}
}
#endif