[LoRaWAN] Provide support for TSxxx packages (including TS009 reference implementation) (#1528)

* [LoRaWAN] Fix for incorrect use of dutycycle function

* [LoRaWAN] Implement TSxxx package support

And fix an MIC problem for >16-bit downlink FCnt

* Revert unused error code

* [LoRaWAN] Add TS009 example

* [LoRaWAN] Comment a platform dependency in TS009 header

* [LoRaWAN] Make TSxxx example platform independent

* [LoRaWAN] Remove unused variable

* [LoRaWAN] Remove printf from example

* [LoRaWAN] Fix scope of variables

* [LoRaWAN] Remove printf from example

* [LoRaWAN] Fix cppcheck issue

* [LoRaWAN] Feedback improvements
pull/1535/head
StevenCellist 2025-06-21 15:48:30 +02:00 zatwierdzone przez GitHub
rodzic 83fbedb833
commit f2fbd73044
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
7 zmienionych plików z 1011 dodań i 302 usunięć

Wyświetl plik

@ -0,0 +1,239 @@
#include <Arduino.h>
#include <RadioLib.h>
// #include <RadioBoards.h>
#warning "The variables below must match your main code. Please check your radio type!"
extern SX1278 radio; // this can be any LoRaWAN-compatible type (e.g. SX1262)
extern LoRaWANNode node;
extern uint32_t periodicity;
extern bool isConfirmed;
extern bool reply;
extern uint8_t dataUp[255];
extern size_t lenUp;
extern uint8_t fPort;
#define RADIOLIB_LORAWAN_TS009_PACKAGE_VERSION (0x00)
#define RADIOLIB_LORAWAN_TS009_DUT_RESET (0x01)
#define RADIOLIB_LORAWAN_TS009_DUT_JOIN (0x02)
#define RADIOLIB_LORAWAN_TS009_SWITCH_CLASS (0x03)
#define RADIOLIB_LORAWAN_TS009_ADR_BIT_CHANGE (0x04)
#define RADIOLIB_LORAWAN_TS009_REGIONAL_DUTY_CYCLE (0x05)
#define RADIOLIB_LORAWAN_TS009_TX_PERIODICITY_CHANGE (0x06)
#define RADIOLIB_LORAWAN_TS009_TX_FRAMES_CTRL (0x07)
#define RADIOLIB_LORAWAN_TS009_ECHO_PAYLOAD (0x08)
#define RADIOLIB_LORAWAN_TS009_RX_APP_CNT (0x09)
#define RADIOLIB_LORAWAN_TS009_RX_APP_CNT_RESET (0x0A)
#define RADIOLIB_LORAWAN_TS009_LINK_CHECK (0x20)
#define RADIOLIB_LORAWAN_TS009_DEVICE_TIME (0x21)
#define RADIOLIB_LORAWAN_TS009_PING_SLOT_INFO (0x22)
#define RADIOLIB_LORAWAN_TS009_TX_CW (0x7D)
#define RADIOLIB_LORAWAN_TS009_DUT_FPORT224_DISABLE (0x7E)
#define RADIOLIB_LORAWAN_TS009_DUT_VERSIONS (0x7F)
/*!
\brief This function implements the TS009 specification.
To enable this package, add this to your setup:
`node.addAppPackage(RADIOLIB_LORAWAN_PACKAGE_TS009, handleTS009)`
Make sure that all `extern` variables are handled in your user code!
*/
void handleTS009(uint8_t* dataDown, size_t lenDown) {
if(lenDown == 0 || dataDown == NULL) {
return;
}
RADIOLIB_DEBUG_PRINTLN("CID = %02x, len = %d", dataDown[0], lenDown - 1);
switch(dataDown[0]) {
case(RADIOLIB_LORAWAN_TS009_PACKAGE_VERSION): {
lenUp = 3;
dataUp[1] = 5; // PackageIdentifier
dataUp[2] = 1; // PackageVersion
fPort = RADIOLIB_LORAWAN_FPORT_TS009;
RADIOLIB_DEBUG_PRINTLN("PackageIdentifier: %d, PackageVersion: %d", dataUp[1], dataUp[2]);
reply = true;
} break;
case(RADIOLIB_LORAWAN_TS009_DUT_RESET): {
RADIOLIB_DEBUG_PRINTLN("Restarting...");
#warning "Please implement this reset function yourself!"
// the function to reset the MCU is platform-dependent
// for ESP32 for example, this would be:
// ESP.restart();
reply = false;
} break;
case(RADIOLIB_LORAWAN_TS009_DUT_JOIN): {
RADIOLIB_DEBUG_PRINTLN("Reverting to Join state");
node.clearSession();
reply = false;
} break;
case(RADIOLIB_LORAWAN_TS009_SWITCH_CLASS): {
uint8_t classType = dataDown[1];
node.setClass(classType);
RADIOLIB_DEBUG_PRINTLN("Switching to class: %s", classType == 0 ? "A" : (classType == 1 ? "B" : "C"));
reply = false;
} break;
case(RADIOLIB_LORAWAN_TS009_ADR_BIT_CHANGE): {
bool adr = (bool)dataDown[1];
node.setADR(adr);
RADIOLIB_DEBUG_PRINTLN("ADR: %d", adr);
reply = false;
} break;
case(RADIOLIB_LORAWAN_TS009_REGIONAL_DUTY_CYCLE): {
bool dutycycle = (bool)dataDown[1];
node.setDutyCycle(dutycycle, 36000);
RADIOLIB_DEBUG_PRINTLN("Dutycycle: %d", dutycycle);
reply = false;
} break;
case(RADIOLIB_LORAWAN_TS009_TX_PERIODICITY_CHANGE): {
uint32_t defaultIntervalSecs = 30;
uint32_t intervals[11] = {defaultIntervalSecs, 5, 10, 20, 30, 40, 50, 60, 120, 240, 480};
periodicity = intervals[dataDown[1]];
RADIOLIB_DEBUG_PRINTLN("Tx Periodicity: %d", periodicity);
reply = false;
} break;
case(RADIOLIB_LORAWAN_TS009_TX_FRAMES_CTRL): {
switch(dataDown[1]) {
case(0):
// no change
// isConfirmed = isConfirmed;
break;
case(1):
isConfirmed = false;
break;
case(2):
isConfirmed = true;
break;
}
RADIOLIB_DEBUG_PRINTLN("Confirmed: %d", isConfirmed);
reply = false;
} break;
case(RADIOLIB_LORAWAN_TS009_ECHO_PAYLOAD): {
lenUp = lenDown;
for (int i = 1; i < lenDown; i++) {
dataUp[i] = dataDown[i] + 1;
}
fPort = RADIOLIB_LORAWAN_FPORT_TS009;
RADIOLIB_DEBUG_PRINTLN("Echoing payload");
reply = true;
} break;
case(RADIOLIB_LORAWAN_TS009_RX_APP_CNT): {
lenUp = 3;
uint16_t aFcntDown16 = (uint16_t)node.getAFCntDown();
dataUp[1] = aFcntDown16 & 0xFF;
dataUp[2] = aFcntDown16 >> 8;
fPort = RADIOLIB_LORAWAN_FPORT_TS009;
RADIOLIB_DEBUG_PRINTLN("aFCntDown16: %d", aFcntDown16);
reply = true;
} break;
case(RADIOLIB_LORAWAN_TS009_RX_APP_CNT_RESET): {
RADIOLIB_DEBUG_PRINTLN("Resetting Application Frame count");
node.resetFCntDown();
reply = false;
} break;
case(RADIOLIB_LORAWAN_TS009_LINK_CHECK): {
lenUp = 0;
fPort = RADIOLIB_LORAWAN_FPORT_MAC_COMMAND;
node.sendMacCommandReq(RADIOLIB_LORAWAN_MAC_LINK_CHECK);
RADIOLIB_DEBUG_PRINTLN("Requesting LinkCheck");
reply = true;
} break;
case(RADIOLIB_LORAWAN_TS009_DEVICE_TIME): {
lenUp = 0;
fPort = RADIOLIB_LORAWAN_FPORT_MAC_COMMAND;
node.sendMacCommandReq(RADIOLIB_LORAWAN_MAC_DEVICE_TIME);
RADIOLIB_DEBUG_PRINTLN("Requesting DeviceTime");
reply = true;
} break;
case(RADIOLIB_LORAWAN_TS009_PING_SLOT_INFO): {
lenUp = 0;
RADIOLIB_DEBUG_PRINTLN("Requesting PingSlotInfo not implemented");
// send PingSlotInfo MAC command which is not implemented
reply = false;
} break;
case(RADIOLIB_LORAWAN_TS009_TX_CW): {
// not implemented
uint16_t timeout = (dataDown[2] << 8) | dataDown[1];
uint32_t freqRaw = (dataDown[5] << 16) | (dataDown[4] << 8) | (dataDown[3]);
float freq = (float)freqRaw/10000.0;
uint8_t txPower = dataDown[6];
RADIOLIB_DEBUG_PRINTLN("Continuous wave: %7.3f MHz, %d dBm, %d s", freq, txPower, timeout);
radio.setFrequency(freq);
radio.setOutputPower(txPower);
radio.transmitDirect();
delay(timeout * 1000);
radio.standby();
reply = false;
} break;
case(RADIOLIB_LORAWAN_TS009_DUT_FPORT224_DISABLE): {
RADIOLIB_DEBUG_PRINTLN("Disabling FPort 224");
node.removePackage(RADIOLIB_LORAWAN_PACKAGE_TS009);
reply = false;
} break;
case(RADIOLIB_LORAWAN_TS009_DUT_VERSIONS): {
lenUp = 13;
// firmware version - this is RadioLib's version as an example
dataUp[1] = RADIOLIB_VERSION_MAJOR;
dataUp[2] = RADIOLIB_VERSION_MINOR;
dataUp[3] = RADIOLIB_VERSION_PATCH;
dataUp[4] = RADIOLIB_VERSION_EXTRA;
// lorawan version
dataUp[5] = 1;
#if (LORAWAN_VERSION == 1)
dataUp[6] = 1;
dataUp[7] = 0;
#else
dataUp[6] = 0;
dataUp[7] = 4;
#endif
dataUp[8] = 0;
// regional parameters version
dataUp[9] = 1;
dataUp[10] = 0;
dataUp[11] = 4;
dataUp[12] = 0;
fPort = RADIOLIB_LORAWAN_FPORT_TS009;
RADIOLIB_DEBUG_PRINTLN("Requested DUT versions");
reply = true;
} break;
}
// if we must reply, copy the command ID into the uplink buffer
if(reply) {
dataUp[0] = dataDown[0];
}
}

Wyświetl plik

@ -0,0 +1,171 @@
/*
RadioLib LoRaWAN Packages Example
This example shows how the TS009 package can be used
and is the sketch used for passing pre-certification testing.
This scheme can also be used for other future packages
such as TS003/004/005/006/007 which, combined, build FUOTA.
PLEASE NOTE that this is a highly customized sketch with
settings that likely violate laws & regulations, and it is
intended to be used with RF blocking materials and attenuators.
For default module settings, see the wiki page
https://github.com/jgromes/RadioLib/wiki/Default-configuration
For full API reference, see the GitHub Pages
https://jgromes.github.io/RadioLib/
For LoRaWAN details, see the wiki page
https://github.com/jgromes/RadioLib/wiki/LoRaWAN
*/
#include <Arduino.h>
#include <RadioLib.h>
#include "LoRaWAN_TS009.h"
#include "config.h"
#include "lorawan.h"
uint32_t periodicity = uplinkIntervalSeconds;
bool isConfirmed = false;
bool reply = false;
uint8_t fPort = 1;
uint8_t dataUp[255];
uint8_t dataDown[255];
size_t lenUp = 0;
size_t lenDown = 0;
void setup() {
Serial.begin(115200);
delay(3000);
Serial.print(F("Initialise the radio ... "));
int state = radio.begin();
if(state == RADIOLIB_ERR_NONE) {
Serial.println(F("success!"));
} else {
Serial.print(F("failed, code "));
Serial.println(state);
while(true);
}
// setup, restore and join the network
lwBegin();
lwRestore();
lwActivate();
// add TS009 package
node.addAppPackage(RADIOLIB_LORAWAN_PACKAGE_TS009, handleTS009);
// LCTT (TS009 testing) has a huge timing problem on the JoinAccept Rx2 window...
node.scanGuard = 100;
// these settings are totally not recommended
// but unfortunately they are the default settings for TS009 testing
node.setDutyCycle(false);
node.setADR(false);
}
void loop() {
while(!node.isActivated()) {
lwActivate();
// this 5s interval is way too short for normal use!
// but you'd be waiting around for long minutes during TS009 testing otherwise
if(!node.isActivated()) {
delay(5000);
}
node.setDutyCycle(false);
}
int state = RADIOLIB_ERR_NONE;
LoRaWANEvent_t eventUp;
LoRaWANEvent_t eventDown;
uint32_t start = millis();
Serial.println("--------------------");
Serial.println("[LoRaWAN] Sending uplink packet ... ");
if (!reply) {
memset(dataUp, 0, 255);
lenUp = 4;
fPort = 1;
sprintf((char*)dataUp, "%04d", node.getFCntUp());
state = node.sendReceive(dataUp, lenUp, fPort, dataDown, &lenDown, isConfirmed, &eventUp, &eventDown);
} else {
reply = false;
state = node.sendReceive(dataUp, lenUp, fPort, dataDown, &lenDown, isConfirmed, &eventUp, &eventDown);
}
if(state >= RADIOLIB_ERR_NONE) {
Serial.println(F("[LoRaWAN] success!"));
}
if(state > 0) {
// print data of the packet (if there are any)
Serial.print(F("[LoRaWAN] Data:\t\t"));
if(lenDown > 0) {
arrayDump(dataDown, lenDown);
} else {
Serial.println(F("<MAC / package commands only>"));
}
Serial.print(F("[LoRaWAN] FPort:\t"));
Serial.print(eventDown.fPort);
// print RSSI (Received Signal Strength Indicator)
Serial.print(F("[LoRaWAN] RSSI:\t\t"));
Serial.print(radio.getRSSI());
Serial.println(F(" dBm"));
// print SNR (Signal-to-Noise Ratio)
Serial.print(F("[LoRaWAN] SNR:\t\t"));
Serial.print(radio.getSNR());
Serial.println(F(" dB"));
uint8_t margin = 0;
uint8_t gwCnt = 0;
if(node.getMacLinkCheckAns(&margin, &gwCnt) == RADIOLIB_ERR_NONE) {
Serial.print(F("[LoRaWAN] LinkCheck margin:\t"));
Serial.println(margin);
Serial.print(F("[LoRaWAN] LinkCheck count:\t"));
Serial.println(gwCnt);
}
uint32_t networkTime = 0;
uint8_t fracSecond = 0;
if(node.getMacDeviceTimeAns(&networkTime, &fracSecond, true) == RADIOLIB_ERR_NONE) {
Serial.print(F("[LoRaWAN] DeviceTime Unix:\t"));
Serial.println(networkTime);
Serial.print(F("[LoRaWAN] DeviceTime second:\t"));
Serial.print(fracSecond);
Serial.println(F("/256"));
}
} else if(state == 0) {
Serial.println(F("No downlink!"));
} else {
Serial.print(F("failed, code "));
Serial.println(state);
}
uint32_t end = millis();
uint32_t delayDc = node.timeUntilUplink();
uint32_t delayMs = periodicity*1000;
if(delayMs > end - start) {
delayMs -= (end - start);
} else {
delayMs = 1;
}
delayMs += 50;
Serial.print(F("Delay: "));
Serial.print(max(delayDc, delayMs));
Serial.println(F(" ms"));
// wait before sending another packet
delay(max(delayDc, delayMs));
}

Wyświetl plik

@ -0,0 +1,115 @@
#ifndef _CONFIG_H
#define _CONFIG_H
#include <RadioLib.h>
// first you have to set your radio model and pin configuration
// this is provided just as a default example
SX1278 radio = new Module(10, 2, 9, 3);
// if you have RadioBoards (https://github.com/radiolib-org/RadioBoards)
// and are using one of the supported boards, you can do the following:
/*
#define RADIO_BOARD_AUTO
#include <RadioBoards.h>
Radio radio = new RadioModule();
*/
// how often to send an uplink - consider legal & FUP constraints
const uint32_t uplinkIntervalSeconds = 1UL * 60UL; // minutes x seconds
#define LORAWAN_VERSION (0) // use version 1.LORAWAN_VERSION when joining
#define LORAWAN_OTAA (1) // use OTAA (1) or ABP (0)
#if (LORAWAN_OTAA == 1)
// joinEUI - previous versions of LoRaWAN called this AppEUI
// for development purposes you can use all zeros - see wiki for details
#define RADIOLIB_LORAWAN_JOIN_EUI 0x0000000000000000
// the Device EUI & two keys can be generated on the TTN console
#ifndef RADIOLIB_LORAWAN_DEV_EUI // Replace with your Device EUI
#define RADIOLIB_LORAWAN_DEV_EUI 0x----------------
#endif
#ifndef RADIOLIB_LORAWAN_APP_KEY // Replace with your App Key
#define RADIOLIB_LORAWAN_APP_KEY 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--
#endif
// copy over the EUI's & keys in to the something that will not compile if incorrectly formatted
uint64_t joinEUI = RADIOLIB_LORAWAN_JOIN_EUI;
uint64_t devEUI = RADIOLIB_LORAWAN_DEV_EUI;
uint8_t appKey[] = { RADIOLIB_LORAWAN_APP_KEY };
#if (LORAWAN_VERSION == 1)
#ifndef RADIOLIB_LORAWAN_NWK_KEY // Put your Nwk Key here
#define RADIOLIB_LORAWAN_NWK_KEY 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--
#endif
uint8_t nwkKey[] = { RADIOLIB_LORAWAN_NWK_KEY }; // LW v1.1 only
#endif
#else // ABP
#ifndef RADIOLIB_LORAWAN_DEV_ADDR // Replace with your DevAddr
#define RADIOLIB_LORAWAN_DEV_ADDR 0x------
#endif
#ifndef RADIOLIB_LORAWAN_NWKSENC_KEY // Replace with your NwkSEnc Key
#define RADIOLIB_LORAWAN_NWKSENC_KEY 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--
#endif
#ifndef RADIOLIB_LORAWAN_APPS_KEY // Replace with your AppS Key
#define RADIOLIB_LORAWAN_APPS_KEY 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--
#endif
// copy over the keys in to the something that will not compile if incorrectly formatted
uint32_t devAddr = RADIOLIB_LORAWAN_DEV_ADDR;
uint8_t sNwkSEncKey[] = { RADIOLIB_LORAWAN_NWKSENC_KEY };
uint8_t appSKey[] = { RADIOLIB_LORAWAN_APPS_KEY };
#if (LORAWAN_VERSION == 1)
#ifndef RADIOLIB_LORAWAN_FNWKSINT_KEY // Replace with your FNwkSInt Key
#define RADIOLIB_LORAWAN_FNWKSINT_KEY 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--
#endif
#ifndef RADIOLIB_LORAWAN_SNWKSINT_KEY // Replace with your SNwkSInt Key
#define RADIOLIB_LORAWAN_SNWKSINT_KEY 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--, 0x--
#endif
uint8_t fNwkSIntKey[] = { RADIOLIB_LORAWAN_FNWKSINT_KEY }; // LW v1.1 only
uint8_t sNwkSIntKey[] = { RADIOLIB_LORAWAN_SNWKSINT_KEY }; // LW v1.1 only
#endif
#endif // OTAA/ABP
// for the curious, the #ifndef blocks allow for automated testing &/or you can
// put your EUI & keys in to your platformio.ini - see wiki for more tips
// regional choices: EU868, US915, AU915, AS923, IN865, KR920, CN780, CN500
const LoRaWANBand_t Region = EU868;
const uint8_t subBand = 0; // For US915, change this to 2, otherwise leave on 0
// ============================================================================
// Below is to support the sketch - only make changes if the notes say so ...
// create the LoRaWAN node
LoRaWANNode node(&radio, &Region, subBand);
// helper function to display any issues
void debug(bool isFail, const __FlashStringHelper* message, int state, bool Freeze) {
if (isFail) {
Serial.print(message);
Serial.print("(");
Serial.print(state);
Serial.println(")");
while (Freeze);
}
}
// helper function to display a byte array
void arrayDump(uint8_t *buffer, uint16_t len) {
for(uint16_t c = 0; c < len; c++) {
char b = buffer[c];
if(b < 0x10) { Serial.print('0'); }
Serial.print(b, HEX);
}
Serial.println();
}
#endif

Wyświetl plik

@ -0,0 +1,76 @@
#ifndef _LORAWAN_H
#define _LORAWAN_H
#include <RadioLib.h>
#include "config.h"
#warning "You are required to implement persistence here! (ESP32 example provided in comments)"
// #include <Preferences.h>
// Preferences store;
// uint8_t LWnonces[RADIOLIB_LORAWAN_NONCES_BUF_SIZE];
bool lwBegin() {
#if (LORAWAN_OTAA == 1)
#if (LORAWAN_VERSION == 1)
node.beginOTAA(joinEUI, devEUI, nwkKey, appKey);
#else
node.beginOTAA(joinEUI, devEUI, NULL, appKey);
#endif
#else
#if (LORAWAN_VERSION == 1)
node.beginABP(devAddr, fNwkSIntKey, sNwkSIntKey, sNwkSEncKey, appSKey);
#else
node.beginABP(devAddr, NULL, NULL, sNwkSEncKey, appSKey);
#endif
#endif
return(true);
}
int16_t lwRestore() {
int16_t state = RADIOLIB_ERR_UNKNOWN;
// store.begin("radiolib");
// if (store.isKey("nonces")) {
// radio.standby();
// store.getBytes("nonces", LWnonces, RADIOLIB_LORAWAN_NONCES_BUF_SIZE);
// state = node.setBufferNonces(LWnonces);
// }
// store.end();
return(state);
}
void lwActivate() {
int16_t state = RADIOLIB_ERR_NETWORK_NOT_JOINED;
Serial.println(F("[LoRaWAN] Attempting network join ... "));
radio.standby();
#if (LORAWAN_OTAA == 1)
state = node.activateOTAA();
#else
state = node.activateABP();
#endif
if(state == RADIOLIB_LORAWAN_SESSION_RESTORED) {
Serial.println(F("[LoRaWAN] Session restored!"));
return;
}
// store.begin("radiolib");
// uint8_t *persist = node.getBufferNonces();
// store.putBytes("nonces", persist, RADIOLIB_LORAWAN_NONCES_BUF_SIZE);
// store.end();
if(state == RADIOLIB_LORAWAN_NEW_SESSION) {
Serial.println(F("[LoRaWAN] Successfully started new session!"));
return;
}
Serial.print(F("[LoRaWAN] Failed, code "));
Serial.println(state);
}
#endif

Wyświetl plik

@ -549,9 +549,9 @@
#define RADIOLIB_ERR_JOIN_NONCE_INVALID (-1111)
/*!
\brief The downlink frame counter is slightly lower than expected, looks like a replay attack.
\brief The downlink MIC could not be verified (incorrect key or invalid FCnt)
*/
#define RADIOLIB_ERR_DOWNLINK_FCNT_INVALID (-1112)
#define RADIOLIB_ERR_MIC_MISMATCH (-1112)
/*!
\brief Multicast frame counter is invalid (outside bounds).
@ -598,11 +598,6 @@
*/
#define RADIOLIB_ERR_INVALID_MODE (-1121)
/*!
\brief The downlink MIC could not be verified (incorrect key or invalid FCnt)
*/
#define RADIOLIB_ERR_MIC_MISMATCH (-1122)
// LR11x0-specific status codes
/*!

Wyświetl plik

@ -14,6 +14,9 @@ LoRaWANNode::LoRaWANNode(PhysicalLayer* phy, const LoRaWANBand_t* band, uint8_t
this->txPowerMax = this->band->powerMax;
this->subBand = subBand;
memset(this->channelPlan, 0, sizeof(this->channelPlan));
for(int i = 0; i < RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES; i++) {
this->packages[i] = RADIOLIB_LORAWAN_PACKAGE_NONE;
}
}
#if defined(RADIOLIB_BUILD_ARDUINO)
@ -228,10 +231,6 @@ int16_t LoRaWANNode::sendReceive(const uint8_t* dataUp, size_t lenUp, uint8_t fP
LoRaWANNode::clearMacCommands(this->fOptsUp, &this->fOptsUpLen, RADIOLIB_LORAWAN_UPLINK);
return(rxWindow);
}
// a downlink was received, so we can clear the whole MAC uplink buffer
memset(this->fOptsUp, 0, RADIOLIB_LORAWAN_FHDR_FOPTS_MAX_LEN);
this->fOptsUpLen = 0;
state = this->parseDownlink(dataDown, lenDown, rxWindow, eventDown);
RADIOLIB_ASSERT(state);
@ -1143,40 +1142,30 @@ void LoRaWANNode::stopMulticastSession() {
int16_t LoRaWANNode::isValidUplink(size_t len, uint8_t fPort) {
// check destination fPort
switch(fPort) {
case RADIOLIB_LORAWAN_FPORT_MAC_COMMAND: {
// MAC FPort only good if internally overruled (no application payload)
if (!this->isMACPayload) {
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Requested uplink at FPort %d - rejected! This FPort is reserved.", fPort);
return(RADIOLIB_ERR_INVALID_PORT);
}
} break;
case RADIOLIB_LORAWAN_FPORT_TS009: {
// TS009 FPort only good if overruled during verification testing
if(!this->TS009) {
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Requested uplink at FPort %d - rejected! This FPort is not enabled.", fPort);
return(RADIOLIB_ERR_INVALID_PORT);
}
} break;
case RADIOLIB_LORAWAN_FPORT_TS011: {
// TS011 FPort only good if overruled during relay exchange
if(!this->TS011) {
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Requested uplink at FPort %d - rejected! This FPort is not enabled.", fPort);
return(RADIOLIB_ERR_INVALID_PORT);
}
} break;
default: {
if((fPort >= RADIOLIB_LORAWAN_FPORT_PAYLOAD_MIN) && (fPort <= RADIOLIB_LORAWAN_FPORT_PAYLOAD_MAX)) {
// user payload ports, all good
bool ok = false;
if(fPort == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND && this->isMACPayload) {
ok = true;
}
if(fPort >= RADIOLIB_LORAWAN_FPORT_PAYLOAD_MIN && fPort <= RADIOLIB_LORAWAN_FPORT_PAYLOAD_MAX) {
ok = true;
}
if(fPort >= RADIOLIB_LORAWAN_FPORT_RESERVED) {
for(int id = 0; id < RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES; id++) {
if(this->packages[id].enabled && fPort == this->packages[id].packFPort) {
ok = true;
break;
}
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Requested uplink at FPort %d - rejected! This FPort is reserved.", fPort);
} break;
}
}
if(!ok) {
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Requested uplink at FPort %d - rejected! This FPort is reserved.", fPort);
return(RADIOLIB_ERR_INVALID_PORT);
}
// check maximum payload len as defined in band
uint8_t maxPayLen = this->band->payloadLenMax[this->channels[RADIOLIB_LORAWAN_UPLINK].dr];
if(this->TS011) {
if(this->packages[RADIOLIB_LORAWAN_PACKAGE_TS011].enabled) {
maxPayLen = RADIOLIB_MIN(maxPayLen, 222); // payload length is limited to 222 if under repeater
}
@ -1340,9 +1329,16 @@ void LoRaWANNode::composeUplink(const uint8_t* in, uint8_t lenIn, uint8_t* out,
// select encryption key based on the target fPort
uint8_t* encKey = this->appSKey;
if((fPort == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND) || (fPort == RADIOLIB_LORAWAN_FPORT_TS011)) {
if(fPort == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND) {
encKey = this->nwkSEncKey;
}
// check if any of the packages uses this FPort
for(int id = 0; id < RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES; id++) {
if(this->packages[id].enabled && fPort == this->packages[id].packFPort) {
encKey = this->packages[id].isAppPack ? this->appSKey : this->nwkSEncKey;
break;
}
}
// encrypt the frame payload
processAES(in, lenIn, encKey, &out[RADIOLIB_LORAWAN_FRAME_PAYLOAD_POS(this->fOptsUpLen)], this->devAddr, this->fCntUp, RADIOLIB_LORAWAN_UPLINK, 0x00, true);
@ -1496,7 +1492,7 @@ int16_t LoRaWANNode::receiveClassA(uint8_t dir, const LoRaWANChannel_t* dlChanne
// get the maximum allowed Time-on-Air of a packet given the current datarate
uint8_t maxPayLen = this->band->payloadLenMax[dlChannel->dr];
if(this->TS011) {
if(this->packages[RADIOLIB_LORAWAN_PACKAGE_TS011].enabled) {
maxPayLen = RADIOLIB_MIN(maxPayLen, 222); // payload length is limited to 222 if under repeater
}
RadioLibTime_t tMax = this->phyLayer->getTimeOnAir(maxPayLen + 13) / 1000; // mandatory FHDR is 12/13 bytes
@ -1669,7 +1665,7 @@ int16_t LoRaWANNode::receiveClassC(RadioLibTime_t timeout) {
// the specified maximum length M over the data rate used to receive the frame
// SHALL be silently discarded.
uint8_t maxPayLen = this->band->payloadLenMax[this->channels[RADIOLIB_LORAWAN_RX_BC].dr];
if(this->TS011) {
if(this->packages[RADIOLIB_LORAWAN_PACKAGE_TS011].enabled) {
maxPayLen = RADIOLIB_MIN(maxPayLen, 222); // payload length is limited to 222 if under repeater
}
if(this->phyLayer->getPacketLength() > (size_t)(maxPayLen + 13)) { // mandatory FHDR is 12/13 bytes
@ -1779,12 +1775,12 @@ int16_t LoRaWANNode::parseDownlink(uint8_t* data, size_t* len, uint8_t window, L
return(RADIOLIB_ERR_DOWNLINK_MALFORMED);
}
// calculate length of piggy-backed FOpts
uint8_t fOptsPbLen = downlinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] & RADIOLIB_LORAWAN_FHDR_FOPTS_LEN_MASK;
// calculate length of (piggy-backed) FOpts
uint8_t fOptsLen = downlinkMsg[RADIOLIB_LORAWAN_FHDR_FCTRL_POS] & RADIOLIB_LORAWAN_FHDR_FOPTS_LEN_MASK;
// MHDR(1) - DevAddr(4) - FCtrl(1) - FCnt(2) - FOptsPb - Payload - MIC(4)
// MHDR(1) - DevAddr(4) - FCtrl(1) - FCnt(2) - FOpts - Payload - MIC(4)
// potentially also an FPort, will find out next
uint8_t payLen = downlinkMsgLen - 1 - 4 - 1 - 2 - fOptsPbLen - 4;
uint8_t payLen = downlinkMsgLen - 1 - 4 - 1 - 2 - fOptsLen - 4;
// in LoRaWAN v1.1, a frame is a Network frame if there is no Application payload
// i.e.: either no payload at all (empty frame or FOpts only), or MAC only payload
@ -1795,74 +1791,62 @@ int16_t LoRaWANNode::parseDownlink(uint8_t* data, size_t* len, uint8_t window, L
}
if(payLen > 0) {
payLen -= 1; // subtract one as fPort is set
fPort = downlinkMsg[RADIOLIB_LORAWAN_FHDR_FPORT_POS(fOptsPbLen)];
fPort = downlinkMsg[RADIOLIB_LORAWAN_FHDR_FPORT_POS(fOptsLen)];
// check if fPort value is actually allowed
switch(fPort) {
case RADIOLIB_LORAWAN_FPORT_MAC_COMMAND: {
// payload consists of all MAC commands (or is empty)
// check destination fPort
bool ok = false;
// LoRaWAN v1.0.4 only: A Class B/C downlink SHALL NOT transport any MAC command.
// (...) it SHALL silently discard the entire frame.
// However, we also enforce this for LoRaWAN v1.1 (TTS does not allow this anyway).
if(window == RADIOLIB_LORAWAN_RX_BC) {
#if !RADIOLIB_STATIC_ONLY
delete[] downlinkMsg;
#endif
return(RADIOLIB_ERR_DOWNLINK_MALFORMED);
}
} break;
case RADIOLIB_LORAWAN_FPORT_TS009: {
// TS009 FPort only good if overruled during verification testing
if(!this->TS009) {
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Downlink at FPort %d - rejected! This FPort is not enabled.", fPort);
#if !RADIOLIB_STATIC_ONLY
delete[] downlinkMsg;
#endif
return(RADIOLIB_ERR_INVALID_PORT);
}
isAppDownlink = true;
} break;
case RADIOLIB_LORAWAN_FPORT_TS011: {
// TS011 FPort only good if overruled during relay exchange
if(!this->TS011) {
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Downlink at FPort %d - rejected! This FPort is not enabled.", fPort);
#if !RADIOLIB_STATIC_ONLY
delete[] downlinkMsg;
#endif
return(RADIOLIB_ERR_INVALID_PORT);
}
isAppDownlink = true;
} break;
default: {
if((fPort >= RADIOLIB_LORAWAN_FPORT_PAYLOAD_MIN) && (fPort <= RADIOLIB_LORAWAN_FPORT_PAYLOAD_MAX)) {
// payload is user-defined (or empty) - may carry piggybacked MAC commands
isAppDownlink = true;
// LoRaWAN v1.0.4 only: A Class B/C downlink SHALL NOT transport any MAC command.
// (...) it SHALL silently discard the entire frame.
// However, we also enforce this for LoRaWAN v1.1 (TTS does not allow this anyway).
if(fPort == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND && window < RADIOLIB_LORAWAN_RX_BC) {
// payload consists of all MAC commands (or is empty)
ok = true;
}
if(fPort >= RADIOLIB_LORAWAN_FPORT_PAYLOAD_MIN && fPort <= RADIOLIB_LORAWAN_FPORT_PAYLOAD_MAX) {
ok = true;
isAppDownlink = true;
}
// check if any of the packages uses this FPort
if(fPort >= RADIOLIB_LORAWAN_FPORT_RESERVED) {
for(int id = 0; id < RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES; id++) {
if(this->packages[id].enabled && fPort == this->packages[id].packFPort) {
ok = true;
isAppDownlink = this->packages[id].isAppPack;
break;
}
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Downlink at FPort %d - rejected! This FPort is reserved.", fPort);
#if !RADIOLIB_STATIC_ONLY
delete[] downlinkMsg;
#endif
return(RADIOLIB_ERR_INVALID_PORT);
} break;
}
}
if(!ok) {
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Received downlink at FPort %d - rejected! This FPort is reserved.", fPort);
#if !RADIOLIB_STATIC_ONLY
delete[] downlinkMsg;
#endif
return(RADIOLIB_ERR_INVALID_PORT);
}
}
// MAC commands SHALL NOT be present in the payload field and the frame options field simultaneously.
// Should this occur, the end-device SHALL silently discard the frame.
if(fOptsPbLen > 0 && payLen > 0 && fPort == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND) {
#if !RADIOLIB_STATIC_ONLY
delete[] downlinkMsg;
#endif
return(RADIOLIB_ERR_DOWNLINK_MALFORMED);
// handle FOpts in uplink with FPort = 0 (or absent which means 0)
if(fPort == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND) {
if(fOptsLen > 0 && payLen > 0) {
// MAC commands SHALL NOT be present in the payload field and the frame options field simultaneously.
// Should this occur, the end-device SHALL silently discard the frame.
#if !RADIOLIB_STATIC_ONLY
delete[] downlinkMsg;
#endif
return(RADIOLIB_ERR_DOWNLINK_MALFORMED);
}
// if FOpts are in the payload, use this instead
if(payLen > 0) {
fOptsLen = payLen;
}
}
// LoRaWAN v1.0.4 only: A Class B/C downlink SHALL NOT transport any MAC command.
// (...) it SHALL silently discard the entire frame.
// However, we also enforce this for LoRaWAN v1.1 (TTS does not allow this anyway).
if(fOptsPbLen > 0 && window == RADIOLIB_LORAWAN_RX_BC) {
if(fOptsLen > 0 && window == RADIOLIB_LORAWAN_RX_BC) {
#if !RADIOLIB_STATIC_ONLY
delete[] downlinkMsg;
#endif
@ -1886,17 +1870,11 @@ int16_t LoRaWANNode::parseDownlink(uint8_t* data, size_t* len, uint8_t window, L
}
}
// if the downlink FCnt16 value is 'slightly' lower than expected by the device, assume a replay attack
uint16_t devFCnt16 = (uint16_t)devFCnt32;
if(devFCnt16 > 0 && (uint16_t)(devFCnt16 - payFCnt16) < RADIOLIB_LORAWAN_MIN_ROLLOVER_FCNT_GAP) {
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("FCnt rejected: %d -> %d", devFCnt16, payFCnt16);
return(RADIOLIB_ERR_DOWNLINK_FCNT_INVALID);
}
// assume a rollover if the FCnt16 in the payload is smaller than the previous FCnt16 known by device
// assume a rollover if the FCnt16 in the payload is equal to / smaller
// than the previous FCnt16 known by device
// (MAX_FCNT_GAP is deprecated for 1.0.4 / 1.1, TTS and CS both apply a 16-bit rollover)
if(payFCnt16 < devFCnt16) {
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("FCnt rollover: %d -> %d", devFCnt16, payFCnt16);
if(devFCnt32 > 0 && payFCnt16 <= (uint16_t)devFCnt32) {
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("FCnt rollover: %d -> %d", (uint16_t)devFCnt32, payFCnt16);
devFCnt32 += 0x10000; // add 16-bit value
}
devFCnt32 &= ~0xFFFF; // clear lower 16 bits known by device
@ -1928,7 +1906,7 @@ int16_t LoRaWANNode::parseDownlink(uint8_t* data, size_t* len, uint8_t window, L
}
downlinkMsg[RADIOLIB_LORAWAN_BLOCK_DIR_POS] = RADIOLIB_LORAWAN_DOWNLINK;
LoRaWANNode::hton<uint32_t>(&downlinkMsg[RADIOLIB_LORAWAN_BLOCK_DEV_ADDR_POS], addr);
LoRaWANNode::hton<uint16_t>(&downlinkMsg[RADIOLIB_LORAWAN_BLOCK_FCNT_POS], devFCnt32);
LoRaWANNode::hton<uint32_t>(&downlinkMsg[RADIOLIB_LORAWAN_BLOCK_FCNT_POS], devFCnt32);
downlinkMsg[RADIOLIB_LORAWAN_MIC_BLOCK_LEN_POS] = downlinkMsgLen - sizeof(uint32_t);
// check the MIC
@ -1943,8 +1921,15 @@ int16_t LoRaWANNode::parseDownlink(uint8_t* data, size_t* len, uint8_t window, L
#endif
return(RADIOLIB_ERR_MIC_MISMATCH);
}
// save current FCnt to respective frame counter
// all checks passed, so dump its contents and start processing
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Downlink (%sFCntDown = %lu) encoded:",
(this->multicast && window == RADIOLIB_LORAWAN_RX_BC) ? "M" :
(isAppDownlink ? "A" : "N"),
(unsigned long)devFCnt32);
RADIOLIB_DEBUG_PROTOCOL_HEXDUMP(downlinkMsg, RADIOLIB_AES128_BLOCK_SIZE + downlinkMsgLen);
// save new FCnt to respective frame counter
if(this->multicast && window == RADIOLIB_LORAWAN_RX_BC) {
// multicast: McApp downlink
this->mcAFCnt = devFCnt32;
@ -1957,26 +1942,24 @@ int16_t LoRaWANNode::parseDownlink(uint8_t* data, size_t* len, uint8_t window, L
}
}
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Downlink (%sFCntDown = %lu) encoded:",
(this->multicast && window == RADIOLIB_LORAWAN_RX_BC) ? "M" :
(isAppDownlink ? "A" : "N"),
(unsigned long)devFCnt32);
RADIOLIB_DEBUG_PROTOCOL_HEXDUMP(downlinkMsg, RADIOLIB_AES128_BLOCK_SIZE + downlinkMsgLen);
// 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;
}
// if this downlink is on FPort 0, the FOptsLen is the length of the payload
// in any other case, the payload (length) is user accessible
uint8_t fOptsLen = fOptsPbLen;
if(fPort == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND && payLen > 0) {
fOptsLen = payLen;
} else {
*len = payLen;
// do some housekeeping for normal Class A downlinks (not allowed for Class B/C)
if(window < RADIOLIB_LORAWAN_RX_BC) {
// if this is a confirmed frame, save the downlink number (only app frames can be confirmed)
if((downlinkMsg[RADIOLIB_LORAWAN_FHDR_LEN_START_OFFS] & 0xFE) == RADIOLIB_LORAWAN_MHDR_MTYPE_CONF_DATA_DOWN) {
this->confFCntDown = this->aFCntDown;
isConfirmedDown = true;
}
// a Class A downlink was received, so restart the ADR counter with the next uplink
this->adrFCnt = this->getFCntUp() + 1;
// a Class A downlink was received, so we can clear the MAC uplink and downlink buffers
memset(this->fOptsUp, 0, RADIOLIB_LORAWAN_FHDR_FOPTS_MAX_LEN);
this->fOptsUpLen = 0;
memset(this->fOptsDown, 0, RADIOLIB_LORAWAN_FHDR_FOPTS_MAX_LEN);
this->fOptsDownLen = 0;
}
#if !RADIOLIB_STATIC_ONLY
@ -1985,26 +1968,162 @@ int16_t LoRaWANNode::parseDownlink(uint8_t* data, size_t* len, uint8_t window, L
uint8_t fOpts[RADIOLIB_STATIC_ARRAY_SIZE];
#endif
// figure out if the payload should end up in user data or internal FOpts buffer
uint8_t* dest;
if(fPort == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND) {
dest = fOpts;
} else {
dest = data;
// decrypt any FOpts on FPort = 0, in which case FOptsLen is the length of the payload
if(fPort == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND && payLen > 0) {
payLen = 0;
processAES(&downlinkMsg[RADIOLIB_LORAWAN_FRAME_PAYLOAD_POS(0)], fOptsLen, this->nwkSEncKey, fOpts, addr, devFCnt32, RADIOLIB_LORAWAN_DOWNLINK, 0x00, true);
// decrypt any piggy-backed FOpts
} else if(fOptsLen > 0) {
// the decryption depends on the LoRaWAN version
if(this->rev == 1) {
// in LoRaWAN v1.1, the piggy-backed FOpts are encrypted using the NwkSEncKey
uint8_t ctrId = 0x01 + isAppDownlink; // see LoRaWAN v1.1 errata
processAES(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], (size_t)fOptsLen, this->nwkSEncKey, fOpts, this->devAddr, devFCnt32, RADIOLIB_LORAWAN_DOWNLINK, ctrId, true);
} else {
// in LoRaWAN v1.0.x, the piggy-backed FOpts are unencrypted
memcpy(fOpts, &downlinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], (size_t)fOptsLen);
}
}
// process any FOpts
if(fOptsLen > 0) {
uint8_t* mPtr = fOpts;
uint8_t procLen = 0;
uint8_t fOptsRe[RADIOLIB_LORAWAN_MAX_DOWNLINK_SIZE] = { 0 };
uint8_t fOptsReLen = 0;
// indication whether LinkAdr MAC command has been processed
bool mAdr = false;
while(procLen < fOptsLen) {
uint8_t cid = *mPtr; // MAC id is the first byte
// fetch length of MAC downlink command and uplink response
uint8_t fLen = 1;
uint8_t fLenRe = 1;
state = this->getMacLen(cid, &fLen, RADIOLIB_LORAWAN_DOWNLINK, true, mPtr + 1);
if(state != RADIOLIB_ERR_NONE) {
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("WARNING: Unknown MAC CID %02x", cid);
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("WARNING: Skipping remaining MAC commands");
fOptsLen = procLen; // truncate to last processed MAC command
break;
}
(void)this->getMacLen(cid, &fLenRe, RADIOLIB_LORAWAN_UPLINK, true);
// check whether the complete payload is present
if(procLen + fLen > fOptsLen) {
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("WARNING: Incomplete MAC command %02x (%d bytes, expected %d)", cid, fOptsLen - procLen, fLen);
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("WARNING: Skipping remaining MAC commands");
fOptsLen = procLen; // truncate to last processed MAC command
break;
}
bool reply = false;
// if this is a LinkAdr MAC command, pre-process contiguous commands into one atomic block
if(cid == RADIOLIB_LORAWAN_MAC_LINK_ADR) {
// if there was any LinkAdr command before, set NACK and continue without processing
if(mAdr) {
reply = true;
fOptsRe[fOptsReLen + 1] = 0x00;
// if this is the first LinkAdr command, do some special treatment:
} else {
mAdr = true;
uint8_t fAdrLen = 5;
uint8_t mAdrOpt[14] = { 0 };
// retrieve all contiguous LinkAdr commands
while(procLen + fLen + fAdrLen < fOptsLen + 1 && *(mPtr + fLen) == RADIOLIB_LORAWAN_MAC_LINK_ADR) {
fLen += 5; // ADR command is 5 bytes
fLenRe += 2; // ADR response is 2 bytes
}
// pre-process them into a single complete channel mask (stored in mAdrOpt)
LoRaWANNode::preprocessMacLinkAdr(mPtr, fLen, mAdrOpt);
// execute like a normal MAC command (but pointing to mAdrOpt instead)
reply = this->execMacCommand(cid, mAdrOpt, 14, &fOptsRe[fOptsReLen + 1]);
// in LoRaWAN v1.0.x, all ACK bytes should have equal status - fix in post-processing
if(this->rev == 0) {
LoRaWANNode::postprocessMacLinkAdr(&fOptsRe[fOptsReLen], fLen);
// in LoRaWAN v1.1, just provide one ACK, so no post-processing but cut off reply length
} else {
fLenRe = 2;
}
}
// MAC command other than LinkAdr, just process the payload
} else {
reply = this->execMacCommand(cid, mPtr + 1, fLen - 1, &fOptsRe[fOptsReLen + 1]);
}
// if there is a reply, only add it to the reply if maximum payload size allows
if(reply && (fOptsReLen + fLenRe <= this->band->payloadLenMax[this->channels[RADIOLIB_LORAWAN_UPLINK].dr])) {
fOptsRe[fOptsReLen] = cid;
fOptsReLen += fLenRe;
}
procLen += fLen;
mPtr += fLen;
}
// remove all MAC commands except those whose payload can be requested by the user
// (which are LinkCheck and DeviceTime)
LoRaWANNode::clearMacCommands(fOpts, &fOptsLen, RADIOLIB_LORAWAN_DOWNLINK);
memcpy(this->fOptsDown, fOpts, fOptsLen);
this->fOptsDownLen = fOptsLen;
// if fOptsLen for the next uplink is larger than can be piggybacked onto an uplink, send separate uplink
if(fOptsReLen > RADIOLIB_LORAWAN_FHDR_FOPTS_MAX_LEN) {
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("! Sending MAC-only uplink (%d bytes):", fOptsReLen);
RADIOLIB_DEBUG_PROTOCOL_HEXDUMP(fOptsRe, fOptsReLen);
this->isMACPayload = true;
// temporarily lift dutyCycle restrictions to allow immediate MAC response
bool prevDC = this->dutyCycleEnabled;
this->dutyCycleEnabled = false;
this->sendReceive(fOptsRe, fOptsReLen, RADIOLIB_LORAWAN_FPORT_MAC_COMMAND);
this->dutyCycleEnabled = prevDC;
} else { // fOptsReLen <= 15
memcpy(this->fOptsUp, fOptsRe, fOptsReLen);
this->fOptsUpLen = fOptsReLen;
}
}
// figure out which key to use to decrypt the payload
uint8_t* encKey = this->appSKey;
if((fPort == RADIOLIB_LORAWAN_FPORT_MAC_COMMAND) || (fPort == RADIOLIB_LORAWAN_FPORT_TS011)) {
encKey = this->nwkSEncKey;
}
if(this->multicast && window == RADIOLIB_LORAWAN_RX_BC) {
encKey = this->mcAppSKey;
}
for(int id = 0; id < RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES; id++) {
if(this->packages[id].enabled && fPort == this->packages[id].packFPort) {
encKey = this->packages[id].isAppPack ? this->appSKey : this->nwkSEncKey;
break;
}
}
// decrypt the frame payload
processAES(&downlinkMsg[RADIOLIB_LORAWAN_FRAME_PAYLOAD_POS(fOptsPbLen)], payLen, encKey, dest, addr, devFCnt32, RADIOLIB_LORAWAN_DOWNLINK, 0x00, true);
// by default, the data and length are user-accessible
processAES(&downlinkMsg[RADIOLIB_LORAWAN_FRAME_PAYLOAD_POS(fOptsLen)], payLen, encKey, data, addr, devFCnt32, RADIOLIB_LORAWAN_DOWNLINK, 0x00, true);
*len = payLen;
// however, if this frame belongs to a package, redirect instead and 'hide' contents from the user
// just to be sure that it doesn't get re-interpreted...
for(int id = 0; id < RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES; id++) {
if(this->packages[id].enabled && fPort == this->packages[id].packFPort) {
this->packages[id].callback(data, *len);
memset(data, 0, *len);
*len = 0;
}
}
// pass the event info if requested
if(event) {
event->dir = RADIOLIB_LORAWAN_DOWNLINK;
@ -2019,151 +2138,6 @@ int16_t LoRaWANNode::parseDownlink(uint8_t* data, size_t* len, uint8_t window, L
event->multicast = (bool)this->multicast;
}
// for RxBC downlinks, return already, we aren't allowed to do any FOpts stuff
if(window == RADIOLIB_LORAWAN_RX_BC) {
#if !RADIOLIB_STATIC_ONLY
delete[] fOpts;
delete[] downlinkMsg;
#endif
return(RADIOLIB_ERR_NONE);
}
// a downlink was received, so restart the ADR counter with the next uplink
this->adrFCnt = this->getFCntUp() + 1;
// decrypt any piggy-backed FOpts
if(fOptsPbLen > 0) {
// the decryption depends on the LoRaWAN version
if(this->rev == 1) {
// in LoRaWAN v1.1, the piggy-backed FOpts are encrypted using the NwkSEncKey
uint8_t ctrId = 0x01 + isAppDownlink; // see LoRaWAN v1.1 errata
processAES(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], (size_t)fOptsPbLen, this->nwkSEncKey, fOpts, this->devAddr, devFCnt32, RADIOLIB_LORAWAN_DOWNLINK, ctrId, true);
} else {
// in LoRaWAN v1.0.x, the piggy-backed FOpts are unencrypted
memcpy(fOpts, &downlinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], (size_t)fOptsPbLen);
}
}
// clear the previous MAC commands, if any
memset(this->fOptsDown, 0, RADIOLIB_LORAWAN_FHDR_FOPTS_MAX_LEN);
// process FOpts (if there are any)
uint8_t cid;
uint8_t fLen = 1;
uint8_t* mPtr = fOpts;
uint8_t procLen = 0;
uint8_t fOptsRe[RADIOLIB_LORAWAN_MAX_DOWNLINK_SIZE] = { 0 };
uint8_t fOptsReLen = 0;
// indication whether LinkAdr MAC command has been processed
bool mAdr = false;
while(procLen < fOptsLen) {
cid = *mPtr; // MAC id is the first byte
// fetch length of MAC downlink payload
state = this->getMacLen(cid, &fLen, RADIOLIB_LORAWAN_DOWNLINK, true, mPtr + 1);
if(state != RADIOLIB_ERR_NONE) {
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("WARNING: Unknown MAC CID %02x", cid);
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("WARNING: Skipping remaining MAC commands");
fOptsLen = procLen; // truncate to last processed MAC command
break;
}
// already fetch length of MAC answer payload (if any), include CID
uint8_t fLenRe = 0;
(void)this->getMacLen(cid, &fLenRe, RADIOLIB_LORAWAN_UPLINK, true);
// don't care about return value: the previous getMacLen() would have failed anyway
if(procLen + fLen > fOptsLen) {
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("WARNING: Incomplete MAC command %02x (%d bytes, expected %d)", cid, fOptsLen - procLen, fLen);
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("WARNING: Skipping remaining MAC commands");
fOptsLen = procLen; // truncate to last processed MAC command
break;
}
bool reply = false;
// if this is a LinkAdr MAC command, pre-process contiguous commands into one atomic block
if(cid == RADIOLIB_LORAWAN_MAC_LINK_ADR) {
// if there was any LinkAdr command before, set NACK and continue without processing
if(mAdr) {
reply = true;
fOptsRe[fOptsReLen + 1] = 0x00;
// if this is the first LinkAdr command, do some special treatment:
} else {
mAdr = true;
uint8_t fAdrLen = 5;
uint8_t mAdrOpt[14] = { 0 };
// retrieve all contiguous LinkAdr commands
while(procLen + fLen + fAdrLen < fOptsLen + 1 && *(mPtr + fLen) == RADIOLIB_LORAWAN_MAC_LINK_ADR) {
fLen += 5; // ADR command is 5 bytes
fLenRe += 2; // ADR response is 2 bytes
}
// pre-process them into a single complete channel mask (stored in mAdrOpt)
LoRaWANNode::preprocessMacLinkAdr(mPtr, fLen, mAdrOpt);
// execute like a normal MAC command (but pointing to mAdrOpt instead)
reply = this->execMacCommand(cid, mAdrOpt, 14, &fOptsRe[fOptsReLen + 1]);
// in LoRaWAN v1.0.x, all ACK bytes should have equal status - fix in post-processing
if(this->rev == 0) {
LoRaWANNode::postprocessMacLinkAdr(&fOptsRe[fOptsReLen], fLen);
// in LoRaWAN v1.1, just provide one ACK, so no post-processing but cut off reply length
} else {
fLenRe = 2;
}
}
// MAC command other than LinkAdr, just process the payload
} else {
reply = this->execMacCommand(cid, mPtr + 1, fLen - 1, &fOptsRe[fOptsReLen + 1]);
}
// if there is a reply, only add it to the reply if maximum payload size allows
if(reply && (fOptsReLen + fLenRe <= this->band->payloadLenMax[this->channels[RADIOLIB_LORAWAN_UPLINK].dr])) {
fOptsRe[fOptsReLen] = cid;
fOptsReLen += fLenRe;
}
procLen += fLen;
mPtr += fLen;
}
// remove all MAC commands except those whose payload can be requested by the user
// (which are LinkCheck and DeviceTime)
if(fOptsLen > 0) {
LoRaWANNode::clearMacCommands(fOpts, &fOptsLen, RADIOLIB_LORAWAN_DOWNLINK);
memcpy(this->fOptsDown, fOpts, fOptsLen);
}
this->fOptsDownLen = fOptsLen;
// if fOptsLen for the next uplink is larger than can be piggybacked onto an uplink, send separate uplink
if(fOptsReLen > RADIOLIB_LORAWAN_FHDR_FOPTS_MAX_LEN) {
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Uplink MAC-only payload (%d bytes):", fOptsReLen);
RADIOLIB_DEBUG_PROTOCOL_HEXDUMP(fOptsRe, fOptsReLen);
this->isMACPayload = true;
// temporarily lift dutyCycle restrictions to allow immediate MAC response
bool prevDC = this->dutyCycleEnabled;
this->dutyCycleEnabled = false;
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Sending MAC-only uplink .. ");
this->sendReceive(fOptsRe, fOptsReLen, RADIOLIB_LORAWAN_FPORT_MAC_COMMAND);
this->dutyCycleEnabled = prevDC;
} else { // fOptsReLen <= 15
memcpy(this->fOptsUp, fOptsRe, fOptsReLen);
this->fOptsUpLen = fOptsReLen;
}
#if !RADIOLIB_STATIC_ONLY
delete[] fOpts;
delete[] downlinkMsg;
@ -3101,6 +3075,7 @@ void LoRaWANNode::setDutyCycle(bool enable, RadioLibTime_t msPerHour) {
this->dutyCycleEnabled = enable;
if(!enable) {
this->dutyCycle = 0;
return;
}
if(msPerHour == 0) {
this->dutyCycle = this->band->dutyCycle;
@ -3662,7 +3637,7 @@ uint8_t LoRaWANNode::getMaxPayloadLen() {
uint8_t minLen = 0;
uint8_t maxLen = this->band->payloadLenMax[this->channels[RADIOLIB_LORAWAN_UPLINK].dr];
if(this->TS011) {
if(this->packages[RADIOLIB_LORAWAN_PACKAGE_TS011].enabled) {
maxLen = RADIOLIB_MIN(maxLen, 222); // payload length is limited to N=222 if under repeater
}
maxLen += 13; // mandatory FHDR is 12/13 bytes
@ -3697,6 +3672,58 @@ void LoRaWANNode::setSleepFunction(SleepCb_t cb) {
this->sleepCb = cb;
}
int16_t LoRaWANNode::addAppPackage(uint8_t packageId, PackageCb_t callback) {
if(packageId >= RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES) {
return(RADIOLIB_ERR_INVALID_MODE);
}
return(this->addAppPackage(packageId, callback, PackageTable[packageId].packFPort));
}
int16_t LoRaWANNode::addAppPackage(uint8_t packageId, PackageCb_t callback, uint8_t fPort) {
if(packageId >= RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES) {
return(RADIOLIB_ERR_INVALID_MODE);
}
if(PackageTable[packageId].isAppPack == false) {
return(RADIOLIB_ERR_INVALID_MODE);
}
if(PackageTable[packageId].fixedFPort && fPort != PackageTable[packageId].packFPort) {
return(RADIOLIB_ERR_INVALID_PORT);
}
if(callback == NULL) {
return(RADIOLIB_ERR_NULL_POINTER);
}
this->packages[packageId] = PackageTable[packageId];
this->packages[packageId].packFPort = fPort;
this->packages[packageId].callback = callback;
this->packages[packageId].enabled = true;
return(RADIOLIB_ERR_NONE);
}
int16_t LoRaWANNode::addNwkPackage(uint8_t packageId, PackageCb_t callback) {
if(packageId >= RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES) {
return(RADIOLIB_ERR_INVALID_MODE);
}
if(PackageTable[packageId].isAppPack == true) {
return(RADIOLIB_ERR_INVALID_MODE);
}
if(callback == NULL) {
return(RADIOLIB_ERR_NULL_POINTER);
}
this->packages[packageId] = PackageTable[packageId];
this->packages[packageId].callback = callback;
this->packages[packageId].enabled = true;
return(RADIOLIB_ERR_NONE);
}
void LoRaWANNode::removePackage(uint8_t packageId) {
// silently ignore, assume that the user supplies decent index
if(packageId >= RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES) {
return;
}
this->packages[packageId].enabled = false;
return;
}
int16_t LoRaWANNode::findDataRate(uint8_t dr, DataRate_t* dataRate) {
int16_t state = RADIOLIB_ERR_NONE;

Wyświetl plik

@ -44,8 +44,6 @@
#define RADIOLIB_LORAWAN_FPORT_MAC_COMMAND (0x00 << 0) // 7 0 payload contains MAC commands only
#define RADIOLIB_LORAWAN_FPORT_PAYLOAD_MIN (0x01 << 0) // 7 0 start of user-allowed fPort range
#define RADIOLIB_LORAWAN_FPORT_PAYLOAD_MAX (0xDF << 0) // 7 0 end of user-allowed fPort range
#define RADIOLIB_LORAWAN_FPORT_TS009 (0xE0 << 0) // 7 0 fPort used for TS009 testing
#define RADIOLIB_LORAWAN_FPORT_TS011 (0xE2 << 0) // 7 0 fPort used for TS011 Forwarding
#define RADIOLIB_LORAWAN_FPORT_RESERVED (0xE0 << 0) // 7 0 fPort values equal to and larger than this are reserved
// data rate encoding
@ -97,9 +95,6 @@
#define RADIOLIB_LORAWAN_REJOIN_MAX_COUNT_N (10) // send rejoin request 16384 uplinks
#define RADIOLIB_LORAWAN_REJOIN_MAX_TIME_N (15) // once every year, not actually implemented
// developer recommended default setting (not specified)
#define RADIOLIB_LORAWAN_MIN_ROLLOVER_FCNT_GAP (16384) // equal to deprecated MaxFCntGap
// join request message layout
#define RADIOLIB_LORAWAN_JOIN_REQUEST_LEN (23)
#define RADIOLIB_LORAWAN_JOIN_REQUEST_JOIN_EUI_POS (1)
@ -202,6 +197,27 @@
#define RADIOLIB_LORAWAN_MAC_DEVICE_MODE (0x20)
#define RADIOLIB_LORAWAN_MAC_PROPRIETARY (0x80)
// number of supported LoRaWAN TS packages
#define RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES (7)
// Package ID numbering as per TS008 (TS011 ID is made up)
#define RADIOLIB_LORAWAN_PACKAGE_TS007 (0)
#define RADIOLIB_LORAWAN_PACKAGE_TS003 (1)
#define RADIOLIB_LORAWAN_PACKAGE_TS005 (2)
#define RADIOLIB_LORAWAN_PACKAGE_TS004 (3)
#define RADIOLIB_LORAWAN_PACKAGE_TS006 (4)
#define RADIOLIB_LORAWAN_PACKAGE_TS009 (5)
#define RADIOLIB_LORAWAN_PACKAGE_TS011 (6)
// Package FPort (specified or recommended)
#define RADIOLIB_LORAWAN_FPORT_TS007 (225)
#define RADIOLIB_LORAWAN_FPORT_TS003 (202)
#define RADIOLIB_LORAWAN_FPORT_TS005 (200)
#define RADIOLIB_LORAWAN_FPORT_TS004 (201)
#define RADIOLIB_LORAWAN_FPORT_TS006 (203)
#define RADIOLIB_LORAWAN_FPORT_TS009 (224)
#define RADIOLIB_LORAWAN_FPORT_TS011 (226)
// the length of internal MAC command queue - hopefully this is enough for most use cases
#define RADIOLIB_LORAWAN_MAC_COMMAND_QUEUE_SIZE (9)
@ -261,6 +277,45 @@ constexpr LoRaWANMacCommand_t MacTable[RADIOLIB_LORAWAN_NUM_MAC_COMMANDS] = {
{ RADIOLIB_LORAWAN_MAC_PROPRIETARY, 5, 0, false, true },
};
/*! \brief A user-supplied callback for LoRaWAN Application Packages (TSxxx) */
typedef void (*PackageCb_t)(uint8_t* dataDown, size_t lenDown);
/*!
\struct LoRaWANPackage_t
\brief LoRaWAN Packages structure (for TSxxx documents).
*/
struct LoRaWANPackage_t {
/*! \brief Package ID as per TS008 */
uint8_t packId;
/*! \brief Package default FPort (specified or recommended) */
uint8_t packFPort;
/*! \brief Whether the package runs through the Application layer */
bool isAppPack;
/*! \brief Whether the FPort value is fixed or may be modified */
bool fixedFPort;
/*! \brief Whether the package is currently in use */
bool enabled;
/*! \brief User-provided callback for handling package downlinks */
PackageCb_t callback;
};
constexpr LoRaWANPackage_t PackageTable[RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES] = {
{ RADIOLIB_LORAWAN_PACKAGE_TS007, RADIOLIB_LORAWAN_FPORT_TS007, true, false, false },
{ RADIOLIB_LORAWAN_PACKAGE_TS003, RADIOLIB_LORAWAN_FPORT_TS003, true, true, false },
{ RADIOLIB_LORAWAN_PACKAGE_TS005, RADIOLIB_LORAWAN_FPORT_TS005, true, true, false },
{ RADIOLIB_LORAWAN_PACKAGE_TS004, RADIOLIB_LORAWAN_FPORT_TS004, true, true, false },
{ RADIOLIB_LORAWAN_PACKAGE_TS006, RADIOLIB_LORAWAN_FPORT_TS006, true, true, false },
{ RADIOLIB_LORAWAN_PACKAGE_TS009, RADIOLIB_LORAWAN_FPORT_TS009, true, false, false },
{ RADIOLIB_LORAWAN_PACKAGE_TS011, RADIOLIB_LORAWAN_FPORT_TS011, false, false, false }
};
#define RADIOLIB_LORAWAN_PACKAGE_NONE { .packId = 0, .packFPort = 0, .isAppPack = false, .fixedFPort = false, .enabled = false, .callback = NULL }
#define RADIOLIB_LORAWAN_NONCES_VERSION_VAL (0x0001)
enum LoRaWANSchemeBase_t {
@ -886,11 +941,37 @@ class LoRaWANNode {
*/
void setSleepFunction(SleepCb_t cb);
/*!
\brief TS009 Protocol Specification Verification switch
(allows FPort 224 and cuts off uplink payload instead of rejecting if maximum length exceeded).
/*!
\brief Add a LoRaWAN Application Package as defined in one of the TSxxx documents.
Any downlinks that occur on the corresponding FPort will be redirected to
a supplied callback that implements this package. These downlink contents will be
hidden from the user as the downlink buffer will be empty and the length zero.
The package may need to overrule the behaviour of your device - refer to the examples.
Advanced users only!
\param packageId The ID of the package (one of RADIOLIB_LORAWAN_PACKAGE_TSxxx).
\param callback The downlink handler for this package of type (uint8_t* dataDown, size_t lenDown).
\returns \ref status_codes
*/
bool TS009 = false;
int16_t addAppPackage(uint8_t packageId, PackageCb_t callback);
/*!
\brief Add a LoRaWAN Application Package as defined in one of the TSxxx documents.
Any downlinks that occur on the corresponding FPort will be redirected to
a supplied callback that implements this package. These downlink contents will be
hidden from the user as the downlink buffer will be empty and the length zero.
The package may need to overrule the behaviour of your device - refer to the examples.
Advanced users only!
\param packageId The ID of the package (one of RADIOLIB_LORAWAN_PACKAGE_TSxxx).
\param callback The downlink handler for this package of type (uint8_t* dataDown, size_t lenDown).
\param fPort A custom FPort for packages that have a default FPort < 224.
\returns \ref status_codes
*/
int16_t addAppPackage(uint8_t packageId, PackageCb_t callback, uint8_t fPort);
/*!
\brief Disable a package that was previously added.
*/
void removePackage(uint8_t packageId);
/*!
\brief Rx window padding in milliseconds
@ -983,6 +1064,9 @@ class LoRaWANNode {
uint32_t mcAFCnt = 0;
uint32_t mcAFCntMax = 0;
// enabled TS packages
LoRaWANPackage_t packages[RADIOLIB_LORAWAN_NUM_SUPPORTED_PACKAGES];
// enable/disable CSMA for LoRaWAN
bool csmaEnabled = false;
@ -1032,9 +1116,7 @@ class LoRaWANNode {
// save the selected sub-band in case this must be restored in ADR control
uint8_t subBand = 0;
// allow port 226 for devices implementing TS011
bool TS011 = false;
// user-provided sleep callback
SleepCb_t sleepCb = nullptr;
// this will reset the device credentials, so the device starts completely new
@ -1079,6 +1161,10 @@ class LoRaWANNode {
// extract downlink payload and process MAC commands
int16_t parseDownlink(uint8_t* data, size_t* len, uint8_t window, LoRaWANEvent_t* event = NULL);
// add a LoRaWAN package that runs through the network layer
// (not available to users, they are only allowed to add application packages)
int16_t addNwkPackage(uint8_t packageId, PackageCb_t callback);
// execute mac command, return the number of processed bytes for sequential processing
bool execMacCommand(uint8_t cid, uint8_t* optIn, uint8_t lenIn);
bool execMacCommand(uint8_t cid, uint8_t* optIn, uint8_t lenIn, uint8_t* optOut);