diff --git a/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino b/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino index a239ee08..a28cad77 100644 --- a/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino +++ b/examples/LoRaWAN/LoRaWAN_End_Device/LoRaWAN_End_Device.ino @@ -133,9 +133,13 @@ void loop() { if(state == RADIOLIB_ERR_NONE) { Serial.println(F("success!")); - // print data of the packet + // print data of the packet (if there are any) Serial.print(F("[LoRaWAN] Data:\t\t")); - Serial.println(strDown); + if(strDown.length() > 0) { + Serial.println(strDown); + } else { + Serial.println(F("")); + } // print RSSI (Received Signal Strength Indicator) Serial.print(F("[LoRaWAN] RSSI:\t\t")); diff --git a/examples/LoRaWAN/LoRaWAN_End_Device_APB/LoRaWAN_End_Device_APB.ino b/examples/LoRaWAN/LoRaWAN_End_Device_APB/LoRaWAN_End_Device_APB.ino index a19effdc..2ecf6569 100644 --- a/examples/LoRaWAN/LoRaWAN_End_Device_APB/LoRaWAN_End_Device_APB.ino +++ b/examples/LoRaWAN/LoRaWAN_End_Device_APB/LoRaWAN_End_Device_APB.ino @@ -128,9 +128,13 @@ void loop() { if(state == RADIOLIB_ERR_NONE) { Serial.println(F("success!")); - // print data of the packet + // print data of the packet (if there are any) Serial.print(F("[LoRaWAN] Data:\t\t")); - Serial.println(strDown); + if(strDown.length() > 0) { + Serial.println(strDown); + } else { + Serial.println(F("")); + } // print RSSI (Received Signal Strength Indicator) Serial.print(F("[LoRaWAN] RSSI:\t\t")); diff --git a/keywords.txt b/keywords.txt index 617a3983..36bfb988 100644 --- a/keywords.txt +++ b/keywords.txt @@ -399,3 +399,5 @@ RADIOLIB_ERR_INVALID_PORT LITERAL1 RADIOLIB_ERR_NO_RX_WINDOW LITERAL1 RADIOLIB_ERR_INVALID_CHANNEL LITERAL1 RADIOLIB_ERR_INVALID_CID LITERAL1 +RADIOLIB_ERR_COMMAND_QUEUE_FULL LITERAL1 +RADIOLIB_ERR_COMMAND_QUEUE_EMPTY LITERAL1 diff --git a/src/TypeDef.h b/src/TypeDef.h index 96058827..11b3f7f6 100644 --- a/src/TypeDef.h +++ b/src/TypeDef.h @@ -523,6 +523,16 @@ */ #define RADIOLIB_ERR_UPLINK_UNAVAILABLE (-1108) +/*! + \brief Unable to push new MAC command because the queue is full. +*/ +#define RADIOLIB_ERR_COMMAND_QUEUE_FULL (-1109) + +/*! + \brief Unable to pop existing MAC command because the queue is empty. +*/ +#define RADIOLIB_ERR_COMMAND_QUEUE_EMPTY (-1110) + /*! \} */ diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index 568abbca..63d4d154 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -250,16 +250,16 @@ int16_t LoRaWANNode::beginOTAA(uint64_t joinEUI, uint64_t devEUI, uint8_t* nwkKe RadioLibAES128Instance.encryptECB(keyDerivationBuff, RADIOLIB_AES128_BLOCK_SIZE, this->nwkSEncKey); //Module::hexdump(this->nwkSEncKey, RADIOLIB_AES128_BLOCK_SIZE); - // send the RekeyInd MAC command + // enqueue the RekeyInd MAC command to be sent in the next uplink this->rev = 1; - uint8_t serverRev = 0xFF; - state = sendMacCommand(RADIOLIB_LORAWAN_MAC_CMD_REKEY_IND, &this->rev, sizeof(uint8_t), &serverRev, sizeof(uint8_t)); + LoRaWANMacCommand_t cmd = { + .cid = RADIOLIB_LORAWAN_MAC_CMD_REKEY_IND, + .len = sizeof(uint8_t), + .payload = { this->rev }, + .repeat = RADIOLIB_LORAWAN_ADR_ACK_LIMIT, + }; + state = pushMacCommand(&cmd, &this->commandsUp); RADIOLIB_ASSERT(state); - - // check the supported server version - if(serverRev != this->rev) { - return(RADIOLIB_ERR_INVALID_REVISION); - } } else { // 1.0 version, just derive the keys @@ -329,10 +329,11 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port) { return(RADIOLIB_ERR_INVALID_PORT); } - // check if there is a MAC command to piggyback - uint8_t foptsLen = 0; - if(this->command) { - foptsLen = 1 + this->command->len; + // check if there are some MAC commands to piggyback + size_t foptsLen = 0; + if(this->commandsUp.numCommands > 0) { + // there are, assume the maximum possible FOpts len for buffer allocation + foptsLen = 15; } // check maximum payload len as defined in phy @@ -361,24 +362,30 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port) { LoRaWANNode::hton(&uplinkMsg[RADIOLIB_LORAWAN_FHDR_DEV_ADDR_POS], this->devAddr); // TODO implement adaptive data rate - uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] = 0x00 | foptsLen; + // foptslen will be added later + uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] = 0x00; // get frame counter from persistent storage uint32_t fcnt = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID) + 1; mod->hal->setPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_FCNT_UP_ID, fcnt); LoRaWANNode::hton(&uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCNT_POS], (uint16_t)fcnt); - // check if there is something in FOpts - if(this->command) { - // append MAC command + // check if we have some MAC command to append + // TODO implement appending multiple MAC commands + LoRaWANMacCommand_t cmd = { 0 }; + if(popMacCommand(&cmd, &this->commandsUp) == RADIOLIB_ERR_NONE) { + // we do, add it to fopts uint8_t foptsBuff[RADIOLIB_AES128_BLOCK_SIZE]; - foptsBuff[0] = this->command->cid; - for(size_t i = 1; i < this->command->len; i++) { - foptsBuff[i] = this->command->payload[i]; + foptsBuff[0] = cmd.cid; + for(size_t i = 1; i < cmd.len; i++) { + foptsBuff[i] = cmd.payload[i]; } + foptsLen = 1 + cmd.len; + uplinkMsgLen = RADIOLIB_LORAWAN_FRAME_LEN(len, foptsLen); + uplinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] |= foptsLen; // encrypt it - processAES(foptsBuff, foptsLen, this->nwkSEncKey, &uplinkMsg[RADIOLIB_LORAWAN_FRAME_PAYLOAD_POS(0)], fcnt, RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK, 0x00, false); + processAES(foptsBuff, foptsLen, this->nwkSEncKey, &uplinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], fcnt, RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK, 0x01, true); } // set the port @@ -435,7 +442,6 @@ int16_t LoRaWANNode::uplink(uint8_t* data, size_t len, uint8_t port) { RADIOLIB_ASSERT(state); // set the timestamp so that we can measure when to start receiving - this->command = NULL; this->rxDelayStart = txStart + timeOnAir; return(RADIOLIB_ERR_NONE); } @@ -656,21 +662,46 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { uint8_t foptsLen = downlinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] & RADIOLIB_LORAWAN_FHDR_FOPTS_LEN_MASK; if(foptsLen > 0) { // there are some Fopts, decrypt them - *len = foptsLen; + uint8_t fopts[RADIOLIB_LORAWAN_FHDR_FOPTS_LEN_MASK]; // according to the specification, the last two arguments should be 0x00 and false, // but that will fail even for LoRaWAN 1.1.0 server - processAES(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], foptsLen, this->nwkSEncKey, data, fcnt, RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, 0x01, true); + processAES(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], (size_t)foptsLen, this->nwkSEncKey, fopts, fcnt, RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, 0x01, true); - #if !defined(RADIOLIB_STATIC_ONLY) - delete[] downlinkMsg; - #endif + //Module::hexdump(fopts, foptsLen); + + // process the MAC command(s) + int8_t remLen = foptsLen; + uint8_t* foptsPtr = fopts; + while(remLen > 0) { + LoRaWANMacCommand_t cmd = { + .cid = *foptsPtr, + .len = remLen - 1, + .payload = { 0 }, + }; + memcpy(cmd.payload, foptsPtr + 1, cmd.len); + + // try to process the mac command + // TODO how to handle incomplete commands? + size_t processedLen = execMacCommand(&cmd) + 1; + + // processing succeeded, move in the buffer to the next command + remLen -= processedLen; + foptsPtr += processedLen; + } + } + + // fopts are processed or not present, check if there is payload + int payLen = downlinkMsgLen - 8 - foptsLen - sizeof(uint32_t); + if(payLen <= 0) { + // no payload + *len = 0; return(RADIOLIB_ERR_NONE); } - // no fopts, just payload - // TODO implement decoding piggybacked Fopts? - *len = downlinkMsgLen; + // there is payload, and so there should be a port too + // TODO pass the port? + *len = payLen - 1; processAES(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], downlinkMsgLen, this->appSKey, data, fcnt, RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, 0x00, true); #if !defined(RADIOLIB_STATIC_ONLY) @@ -680,6 +711,10 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { return(state); } +void LoRaWANNode::setDeviceStatus(uint8_t battLevel) { + this->battLevel = battLevel; +} + void LoRaWANNode::findDataRate(uint8_t dr, DataRate_t* datr, const LoRaWANChannelSpan_t* span) { uint8_t dataRateBand = span->dataRates[dr]; this->dataRate = dr; @@ -905,6 +940,82 @@ int16_t LoRaWANNode::sendMacCommand(uint8_t cid, uint8_t* payload, size_t payloa return(state); } +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)); + /*RADIOLIB_DEBUG_PRINTLN("push MAC CID = %02x, len = %d, payload = %02x %02x %02x %02x %02x, repeat = %d ", + queue->commands[queue->numCommands - 1].cid, + queue->commands[queue->numCommands - 1].len, + queue->commands[queue->numCommands - 1].payload[0], + queue->commands[queue->numCommands - 1].payload[1], + queue->commands[queue->numCommands - 1].payload[2], + queue->commands[queue->numCommands - 1].payload[3], + queue->commands[queue->numCommands - 1].payload[4], + queue->commands[queue->numCommands - 1].repeat);*/ + queue->numCommands++; + + return(RADIOLIB_ERR_NONE); +} + +int16_t LoRaWANNode::popMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQueue_t* queue, bool force) { + if(queue->numCommands == 0) { + return(RADIOLIB_ERR_COMMAND_QUEUE_EMPTY); + } + + if(cmd) { + /*RADIOLIB_DEBUG_PRINTLN("pop MAC CID = %02x, len = %d, payload = %02x %02x %02x %02x %02x, repeat = %d ", + queue->commands[queue->numCommands - 1].cid, + queue->commands[queue->numCommands - 1].len, + queue->commands[queue->numCommands - 1].payload[0], + queue->commands[queue->numCommands - 1].payload[1], + queue->commands[queue->numCommands - 1].payload[2], + queue->commands[queue->numCommands - 1].payload[3], + queue->commands[queue->numCommands - 1].payload[4], + queue->commands[queue->numCommands - 1].repeat);*/ + memcpy(cmd, &queue->commands[queue->numCommands - 1], sizeof(LoRaWANMacCommand_t)); + } + + if((!force) && (queue->commands[queue->numCommands - 1].repeat > 0)) { + queue->commands[queue->numCommands - 1].repeat--; + } else { + queue->commands[queue->numCommands - 1].repeat = 0; + queue->numCommands--; + } + + return(RADIOLIB_ERR_NONE); +} + +size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { + //RADIOLIB_DEBUG_PRINTLN("exe MAC CID = %02x, len = %d", cmd->cid, cmd->len); + + switch(cmd->cid) { + case(RADIOLIB_LORAWAN_MAC_CMD_DEV_STATUS_ANS): { + // set the uplink reply + cmd->len = 2; + cmd->payload[1] = this->battLevel; + int8_t snr = this->phyLayer->getSNR(); + cmd->payload[0] = snr & 0x3F; + + // push it to the uplink queue + pushMacCommand(cmd, &this->commandsUp); + return(0); + } break; + + case(RADIOLIB_LORAWAN_MAC_CMD_REKEY_IND): { + // TODO verify the actual server version here + + // stop sending the ReKey MAC command + popMacCommand(NULL, &this->commandsUp, true); + return(1); + } break; + } + + return(0); +} + 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; diff --git a/src/protocols/LoRaWAN/LoRaWAN.h b/src/protocols/LoRaWAN/LoRaWAN.h index 098d4a57..73ebbf35 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.h +++ b/src/protocols/LoRaWAN/LoRaWAN.h @@ -160,6 +160,9 @@ #define RADIOLIB_LORAWAN_MAC_CMD_DEVICE_TIME_REQ (0x0D) #define RADIOLIB_LORAWAN_MAC_CMD_REJOIN_PARAM_SETUP_ANS (0x0F) +// the length of internal MAC command queue - hopefully this is enough for most use cases +#define RADIOLIB_LORAWAN_MAC_COMMAND_QUEUE_SIZE (8) + /*! \struct LoRaWANChannelSpan_t \brief Structure to save information about LoRaWAN channels. @@ -235,10 +238,27 @@ extern const LoRaWANBand_t AS923; extern const LoRaWANBand_t KR920; extern const LoRaWANBand_t IN865; +/*! + \struct LoRaWANMacCommand_t + \brief Structure to save information about MAC command +*/ struct LoRaWANMacCommand_t { + /*! \brief The command ID */ uint8_t cid; + + /*! \brief Length of the payload */ size_t len; - uint8_t* payload; + + /*! \brief Payload buffer (5 bytes is the longest possible) */ + uint8_t payload[5]; + + /*! \brief Repetition counter (the command will be uplinked repeat + 1 times) */ + uint8_t repeat; +}; + +struct LoRaWANMacCommandQueue_t { + LoRaWANMacCommand_t commands[RADIOLIB_LORAWAN_MAC_COMMAND_QUEUE_SIZE]; + size_t numCommands; }; /*! @@ -337,13 +357,21 @@ class LoRaWANNode { */ int16_t downlink(uint8_t* data, size_t* len); + /*! + \brief Set device status. + \param battLevel Battery level to set. 0 for external power source, 1 for lowest battery, + 254 for highest battery, 255 for unable to measure. + */ + void setDeviceStatus(uint8_t battLevel); + #if !defined(RADIOLIB_GODMODE) private: #endif PhysicalLayer* phyLayer = NULL; const LoRaWANBand_t* band = NULL; - LoRaWANMacCommand_t* command = NULL; + LoRaWANMacCommandQueue_t commandsUp = { .commands = { 0 }, .numCommands = 0 }; + LoRaWANMacCommandQueue_t commandsDown = { .commands = { 0 }, .numCommands = 0 }; // the following is either provided by the network server (OTAA) // or directly entered by the user (ABP) @@ -371,6 +399,9 @@ class LoRaWANNode { // delays between the uplink and RX1/2 windows uint32_t rxDelays[2] = { RADIOLIB_LORAWAN_RECEIVE_DELAY_1_MS, RADIOLIB_LORAWAN_RECEIVE_DELAY_2_MS }; + // device status - battery level + uint8_t battLevel = 0xFF; + // find the first usable data rate in a given channel span void findDataRate(uint8_t dr, DataRate_t* datr, const LoRaWANChannelSpan_t* span); @@ -395,6 +426,15 @@ class LoRaWANNode { // send a MAC command to the network server int16_t sendMacCommand(uint8_t cid, uint8_t* payload, size_t payloadLen, uint8_t* reply, size_t replyLen); + // push MAC command to queue, done by copy + int16_t pushMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQueue_t* queue); + + // pop MAC command from queue, done by copy unless CMD is NULL + int16_t popMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQueue_t* queue, bool force = false); + + // execute mac command, return the number of processed bytes for sequential processing + size_t execMacCommand(LoRaWANMacCommand_t* cmd); + // function to encrypt and decrypt payloads void processAES(uint8_t* in, size_t len, uint8_t* key, uint8_t* out, uint32_t fcnt, uint8_t dir, uint8_t ctrId, bool counter);