[LoRaWAN] Implement Class C (including multicast) (#1504)

* [SX126x] Add unified RxContinuous timeout

* [SX127x] Add unified RxContinuous timeout

* [SX128x] Add unified RxContinuous timeout

* [LR11x0] Add unified RxContinuous timeout

* [LoRaWAN] Implement Class C

* [LoRaWAN] Fix cppcheck issue

* [LoRaWAN] Add a few comments

* [LoRaWAN] Add a few more comments

* [LoRaWAN] Improve debug line

* [LoRaWAN] Prohibit class switching before join

* [LoRaWAN] Fix Class C downlink details

* [LoRaWAN] Implement Class C multicast

* Update keywords.txt

* [LoRaWAN] Fix error codes in examples

* [LoRaWAN] Declare Multicast key arguments `const`

* [LoRaWAN] Small fixes for Class C

* [LoRaWAN] Small fixes for Class C

* [LoRaWAN] Fix Class C feedback
lr2021
StevenCellist 2025-05-29 21:08:24 +02:00 zatwierdzone przez GitHub
rodzic 764eeabd3a
commit 49d340e667
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
11 zmienionych plików z 611 dodań i 307 usunięć

Wyświetl plik

@ -72,8 +72,8 @@ String stateDecode(const int16_t result) {
return "ERR_PACKET_TOO_LONG";
case RADIOLIB_ERR_RX_TIMEOUT:
return "ERR_RX_TIMEOUT";
case RADIOLIB_ERR_CRC_MISMATCH:
return "ERR_CRC_MISMATCH";
case RADIOLIB_ERR_MIC_MISMATCH:
return "ERR_MIC_MISMATCH";
case RADIOLIB_ERR_INVALID_BANDWIDTH:
return "ERR_INVALID_BANDWIDTH";
case RADIOLIB_ERR_INVALID_SPREADING_FACTOR:
@ -104,10 +104,6 @@ String stateDecode(const int16_t result) {
return "RADIOLIB_ERR_COMMAND_QUEUE_ITEM_NOT_FOUND";
case RADIOLIB_ERR_JOIN_NONCE_INVALID:
return "RADIOLIB_ERR_JOIN_NONCE_INVALID";
case RADIOLIB_ERR_N_FCNT_DOWN_INVALID:
return "RADIOLIB_ERR_N_FCNT_DOWN_INVALID";
case RADIOLIB_ERR_A_FCNT_DOWN_INVALID:
return "RADIOLIB_ERR_A_FCNT_DOWN_INVALID";
case RADIOLIB_ERR_DWELL_TIME_EXCEEDED:
return "RADIOLIB_ERR_DWELL_TIME_EXCEEDED";
case RADIOLIB_ERR_CHECKSUM_MISMATCH:

Wyświetl plik

@ -67,8 +67,8 @@ String stateDecode(const int16_t result) {
return "ERR_PACKET_TOO_LONG";
case RADIOLIB_ERR_RX_TIMEOUT:
return "ERR_RX_TIMEOUT";
case RADIOLIB_ERR_CRC_MISMATCH:
return "ERR_CRC_MISMATCH";
case RADIOLIB_ERR_MIC_MISMATCH:
return "ERR_MIC_MISMATCH";
case RADIOLIB_ERR_INVALID_BANDWIDTH:
return "ERR_INVALID_BANDWIDTH";
case RADIOLIB_ERR_INVALID_SPREADING_FACTOR:
@ -99,10 +99,6 @@ String stateDecode(const int16_t result) {
return "RADIOLIB_ERR_COMMAND_QUEUE_ITEM_NOT_FOUND";
case RADIOLIB_ERR_JOIN_NONCE_INVALID:
return "RADIOLIB_ERR_JOIN_NONCE_INVALID";
case RADIOLIB_ERR_N_FCNT_DOWN_INVALID:
return "RADIOLIB_ERR_N_FCNT_DOWN_INVALID";
case RADIOLIB_ERR_A_FCNT_DOWN_INVALID:
return "RADIOLIB_ERR_A_FCNT_DOWN_INVALID";
case RADIOLIB_ERR_DWELL_TIME_EXCEEDED:
return "RADIOLIB_ERR_DWELL_TIME_EXCEEDED";
case RADIOLIB_ERR_CHECKSUM_MISMATCH:

Wyświetl plik

@ -67,8 +67,8 @@ String stateDecode(const int16_t result) {
return "ERR_PACKET_TOO_LONG";
case RADIOLIB_ERR_RX_TIMEOUT:
return "ERR_RX_TIMEOUT";
case RADIOLIB_ERR_CRC_MISMATCH:
return "ERR_CRC_MISMATCH";
case RADIOLIB_ERR_MIC_MISMATCH:
return "ERR_MIC_MISMATCH";
case RADIOLIB_ERR_INVALID_BANDWIDTH:
return "ERR_INVALID_BANDWIDTH";
case RADIOLIB_ERR_INVALID_SPREADING_FACTOR:
@ -99,10 +99,6 @@ String stateDecode(const int16_t result) {
return "RADIOLIB_ERR_COMMAND_QUEUE_ITEM_NOT_FOUND";
case RADIOLIB_ERR_JOIN_NONCE_INVALID:
return "RADIOLIB_ERR_JOIN_NONCE_INVALID";
case RADIOLIB_ERR_N_FCNT_DOWN_INVALID:
return "RADIOLIB_ERR_N_FCNT_DOWN_INVALID";
case RADIOLIB_ERR_A_FCNT_DOWN_INVALID:
return "RADIOLIB_ERR_A_FCNT_DOWN_INVALID";
case RADIOLIB_ERR_DWELL_TIME_EXCEEDED:
return "RADIOLIB_ERR_DWELL_TIME_EXCEEDED";
case RADIOLIB_ERR_CHECKSUM_MISMATCH:

Wyświetl plik

@ -357,6 +357,9 @@ beginABP KEYWORD2
activateOTAA KEYWORD2
activateABP KEYWORD2
isActivated KEYWORD2
setClass KEYWORD2
startMulticastSession KEYWORD2
stopMulticastSession KEYWORD2
sendReceive KEYWORD2
sendMacCommandReq KEYWORD2
getMacLinkCheckAns KEYWORD2
@ -489,8 +492,8 @@ RADIOLIB_ERR_UPLINK_UNAVAILABLE LITERAL1
RADIOLIB_ERR_COMMAND_QUEUE_FULL LITERAL1
RADIOLIB_ERR_COMMAND_QUEUE_ITEM_NOT_FOUND LITERAL1
RADIOLIB_ERR_JOIN_NONCE_INVALID LITERAL1
RADIOLIB_ERR_N_FCNT_DOWN_INVALID LITERAL1
RADIOLIB_ERR_A_FCNT_DOWN_INVALID LITERAL1
RADIOLIB_ERR_MIC_MISMATCH LITERAL1
RADIOLIB_ERR_MULTICAST_FCNT_INVALID LITERAL1
RADIOLIB_ERR_DWELL_TIME_EXCEEDED LITERAL1
RADIOLIB_ERR_CHECKSUM_MISMATCH LITERAL1
RADIOLIB_ERR_NO_JOIN_ACCEPT LITERAL1

Wyświetl plik

@ -549,14 +549,14 @@
#define RADIOLIB_ERR_JOIN_NONCE_INVALID (-1111)
/*!
\brief Received downlink Network frame counter is invalid (lower than last heard value).
\brief The downlink MIC could not be verified (incorrect key or invalid FCnt)
*/
#define RADIOLIB_ERR_N_FCNT_DOWN_INVALID (-1112)
#define RADIOLIB_ERR_MIC_MISMATCH (-1112)
/*!
\brief Received downlink Application frame counter is invalid (lower than last heard value).
\brief Multicast frame counter is invalid (outside bounds).
*/
#define RADIOLIB_ERR_A_FCNT_DOWN_INVALID (-1113)
#define RADIOLIB_ERR_MULTICAST_FCNT_INVALID (-1113)
/*!
\brief Uplink payload length at this datarate exceeds the active dwell time limitations.

Wyświetl plik

@ -1977,6 +1977,11 @@ int16_t LR11x0::stageMode(RadioModeType_t mode, RadioModeConfig_t* cfg) {
state = setPacketParamsLoRa(this->preambleLengthLoRa, this->headerType, this->implicitLen, this->crcTypeLoRa, this->invertIQEnabled);
RADIOLIB_ASSERT(state);
}
// if max(uint32_t) is used, revert to RxContinuous
if(cfg->receive.timeout == 0xFFFFFFFF) {
cfg->receive.timeout = 0xFFFFFF;
}
this->rxTimeout = cfg->receive.timeout;
} break;

Wyświetl plik

@ -1554,6 +1554,10 @@ int16_t SX126x::stageMode(RadioModeType_t mode, RadioModeConfig_t* cfg) {
state = startReceiveCommon(cfg->receive.timeout, cfg->receive.irqFlags, cfg->receive.irqMask);
RADIOLIB_ASSERT(state);
// if max(uint32_t) is used, revert to RxContinuous
if(cfg->receive.timeout == 0xFFFFFFFF) {
cfg->receive.timeout = 0xFFFFFF;
}
this->rxTimeout = cfg->receive.timeout;
} break;

Wyświetl plik

@ -1671,6 +1671,10 @@ int16_t SX127x::stageMode(RadioModeType_t mode, RadioModeConfig_t* cfg) {
int16_t modem = getActiveModem();
if(modem == RADIOLIB_SX127X_LORA) {
// if max(uint32_t) is used, revert to RxContinuous
if(cfg->receive.timeout == 0xFFFFFFFF) {
cfg->receive.timeout = 0;
}
if(cfg->receive.timeout != 0) {
// for non-zero timeout value, change mode to Rx single and set the timeout
this->rxMode = RADIOLIB_SX127X_RXSINGLE;

Wyświetl plik

@ -1426,6 +1426,10 @@ int16_t SX128x::stageMode(RadioModeType_t mode, RadioModeConfig_t* cfg) {
state = setPacketParamsLoRa(this->preambleLengthLoRa, this->headerType, this->payloadLen, this->crcLoRa, this->invertIQEnabled);
RADIOLIB_ASSERT(state);
}
// if max(uint32_t) is used, revert to RxContinuous
if(cfg->receive.timeout == 0xFFFFFFFF) {
cfg->receive.timeout = 0xFFFF;
}
this->rxTimeout = cfg->receive.timeout;
} break;

Wyświetl plik

@ -11,9 +11,9 @@
#define RADIOLIB_LORAWAN_MODE_NONE (0x0000)
// operation mode
#define RADIOLIB_LORAWAN_CLASS_A (0x0A)
#define RADIOLIB_LORAWAN_CLASS_B (0x0B)
#define RADIOLIB_LORAWAN_CLASS_C (0x0C)
#define RADIOLIB_LORAWAN_CLASS_A (0x00)
#define RADIOLIB_LORAWAN_CLASS_B (0x01)
#define RADIOLIB_LORAWAN_CLASS_C (0x02)
// preamble format
#define RADIOLIB_LORAWAN_LORA_SYNC_WORD (0x34)
@ -73,9 +73,11 @@
#define RADIOLIB_LORAWAN_DATA_RATE_UNUSED (0xFF << 0) // 7 0 unused data rate
// channels and channel plans
#define RADIOLIB_LORAWAN_UPLINK (0x00 << 0)
#define RADIOLIB_LORAWAN_DOWNLINK (0x01 << 0)
#define RADIOLIB_LORAWAN_DIR_RX2 (0x02 << 0)
#define RADIOLIB_LORAWAN_UPLINK (0x00 << 0)
#define RADIOLIB_LORAWAN_DOWNLINK (0x01 << 0)
#define RADIOLIB_LORAWAN_RX1 (0x01 << 0)
#define RADIOLIB_LORAWAN_RX2 (0x02 << 0)
#define RADIOLIB_LORAWAN_RX_BC (0x03 << 0)
#define RADIOLIB_LORAWAN_BAND_DYNAMIC (0)
#define RADIOLIB_LORAWAN_BAND_FIXED (1)
#define RADIOLIB_LORAWAN_CHANNEL_NUM_DATARATES (15)
@ -177,7 +179,7 @@
#define RADIOLIB_LORAWAN_MAX_CHANGES_DEFAULT (4)
// MAC commands
#define RADIOLIB_LORAWAN_NUM_MAC_COMMANDS (23)
#define RADIOLIB_LORAWAN_NUM_MAC_COMMANDS (24)
#define RADIOLIB_LORAWAN_MAC_RESET (0x01)
#define RADIOLIB_LORAWAN_MAC_LINK_CHECK (0x02)
@ -194,6 +196,7 @@
#define RADIOLIB_LORAWAN_MAC_DEVICE_TIME (0x0D)
#define RADIOLIB_LORAWAN_MAC_FORCE_REJOIN (0x0E)
#define RADIOLIB_LORAWAN_MAC_REJOIN_PARAM_SETUP (0x0F)
#define RADIOLIB_LORAWAN_MAC_DEVICE_MODE (0x20)
#define RADIOLIB_LORAWAN_MAC_PROPRIETARY (0x80)
// the length of internal MAC command queue - hopefully this is enough for most use cases
@ -251,6 +254,7 @@ constexpr LoRaWANMacCommand_t MacTable[RADIOLIB_LORAWAN_NUM_MAC_COMMANDS] = {
{ RADIOLIB_LORAWAN_MAC_DEVICE_TIME, 5, 0, false, true },
{ RADIOLIB_LORAWAN_MAC_FORCE_REJOIN, 2, 0, false, false },
{ RADIOLIB_LORAWAN_MAC_REJOIN_PARAM_SETUP, 1, 1, false, false },
{ RADIOLIB_LORAWAN_MAC_DEVICE_MODE, 1, 1, true, false },
{ RADIOLIB_LORAWAN_MAC_PROPRIETARY, 5, 0, false, true },
};
@ -260,8 +264,7 @@ enum LoRaWANSchemeBase_t {
RADIOLIB_LORAWAN_NONCES_START = 0x00,
RADIOLIB_LORAWAN_NONCES_VERSION = RADIOLIB_LORAWAN_NONCES_START, // 2 bytes
RADIOLIB_LORAWAN_NONCES_MODE = RADIOLIB_LORAWAN_NONCES_VERSION + sizeof(uint16_t), // 2 bytes
RADIOLIB_LORAWAN_NONCES_CLASS = RADIOLIB_LORAWAN_NONCES_MODE + sizeof(uint16_t), // 1 byte
RADIOLIB_LORAWAN_NONCES_PLAN = RADIOLIB_LORAWAN_NONCES_CLASS + sizeof(uint8_t), // 1 byte
RADIOLIB_LORAWAN_NONCES_PLAN = RADIOLIB_LORAWAN_NONCES_MODE + sizeof(uint16_t), // 1 byte
RADIOLIB_LORAWAN_NONCES_CHECKSUM = RADIOLIB_LORAWAN_NONCES_PLAN + sizeof(uint8_t), // 2 bytes
RADIOLIB_LORAWAN_NONCES_DEV_NONCE = RADIOLIB_LORAWAN_NONCES_CHECKSUM + sizeof(uint16_t), // 2 bytes
RADIOLIB_LORAWAN_NONCES_JOIN_NONCE = RADIOLIB_LORAWAN_NONCES_DEV_NONCE + sizeof(uint16_t), // 3 bytes
@ -288,7 +291,8 @@ enum LoRaWANSchemeSession_t {
RADIOLIB_LORAWAN_SESSION_RJ_COUNT1 = RADIOLIB_LORAWAN_SESSION_RJ_COUNT0 + sizeof(uint16_t), // 2 bytes
RADIOLIB_LORAWAN_SESSION_HOMENET_ID = RADIOLIB_LORAWAN_SESSION_RJ_COUNT1 + sizeof(uint16_t), // 4 bytes
RADIOLIB_LORAWAN_SESSION_VERSION = RADIOLIB_LORAWAN_SESSION_HOMENET_ID + sizeof(uint32_t), // 1 byte
RADIOLIB_LORAWAN_SESSION_LINK_ADR = RADIOLIB_LORAWAN_SESSION_VERSION + sizeof(uint8_t), // 14 bytes
RADIOLIB_LORAWAN_SESSION_CLASS = RADIOLIB_LORAWAN_SESSION_VERSION + 1, // 1 byte
RADIOLIB_LORAWAN_SESSION_LINK_ADR = RADIOLIB_LORAWAN_SESSION_CLASS + sizeof(uint8_t), // 14 bytes
RADIOLIB_LORAWAN_SESSION_DUTY_CYCLE = RADIOLIB_LORAWAN_SESSION_LINK_ADR + 14, // 1 byte
RADIOLIB_LORAWAN_SESSION_RX_PARAM_SETUP = RADIOLIB_LORAWAN_SESSION_DUTY_CYCLE + 1, // 4 bytes
RADIOLIB_LORAWAN_SESSION_RX_TIMING_SETUP = RADIOLIB_LORAWAN_SESSION_RX_PARAM_SETUP + 4, // 1 byte
@ -517,6 +521,9 @@ struct LoRaWANEvent_t {
/*! \brief Number of times this uplink was transmitted (ADR)*/
uint8_t nbTrans;
/*! \brief Multicast or unicast */
bool multicast;
};
/*!
@ -606,6 +613,26 @@ class LoRaWANNode {
/*! \brief Whether there is an ongoing session active */
bool isActivated();
/*! \brief Configure class (RADIOLIB_LORAWAN_CLASS_A or RADIOLIB_LORAWAN_CLASS_C) */
int16_t setClass(uint8_t cls);
/*!
\brief Start a Multicast session.
\param cls The LoRaWAN Class used for this session (only C is supported).
\param mcAddr The Multicast address.
\param mcAppSKey The Multicast payload encryption key.
\param mcNwkSKey The Multicast payload integrity key.
\param mcFCntMin The minimum expected Multicast frame counter.
\param mcFCntMin The maximum allowed Multicast frame counter.
\param mcFreq The frequency used for the Multicast downlinks (in Hz). Default = 0 uses Rx2 frequency.
\param mcDr The datarate used for the Multicast downlinks. Default = 0 uses Rx2 datarate.
\returns \ref status_codes
*/
int16_t startMulticastSession(uint8_t cls, uint32_t mcAddr, const uint8_t* mcAppSKey, const uint8_t* mcNwkSKey, uint32_t mcFCntMin = 0, uint32_t mcFCntMax = 0xFFFFFFFF, uint32_t mcFreq = 0, uint8_t mcDr = RADIOLIB_LORAWAN_DATA_RATE_UNUSED);
/*! \brief Stop an ongoing multicast session */
void stopMulticastSession();
#if defined(RADIOLIB_BUILD_ARDUINO)
/*!
\brief Send a message to the server and wait for a downlink during Rx1 and/or Rx2 window.
@ -680,6 +707,16 @@ class LoRaWANNode {
*/
virtual int16_t sendReceive(const uint8_t* dataUp, size_t lenUp, uint8_t fPort, uint8_t* dataDown, size_t* lenDown, bool isConfirmed = false, LoRaWANEvent_t* eventUp = NULL, LoRaWANEvent_t* eventDown = NULL);
/*!
\brief Check if there is an RxC downlink and parse it if available.
\param dataDown Buffer to save received data into.
\param lenDown Pointer to variable that will be used to save the number of received bytes.
\param eventDown Pointer to a structure to store extra information about the downlink event
(fPort, frame counter, etc.). If set to NULL, no extra information will be passed to the user.
\returns Window number > 0 if downlink was received, 0 is no downlink was received, otherwise \ref status_codes
*/
int16_t getDownlinkClassC(uint8_t* dataDown, size_t* lenDown, LoRaWANEvent_t* eventDown = NULL);
/*!
\brief Add a MAC command to the uplink queue.
Only LinkCheck and DeviceTime are available to the user.
@ -935,6 +972,14 @@ class LoRaWANNode {
RadioLibTime_t tUplink = 0; // scheduled uplink transmission time (internal clock)
RadioLibTime_t tDownlink = 0; // time at end of downlink reception
// multicast parameters
uint8_t multicast = false;
uint32_t mcAddr = 0;
uint8_t mcAppSKey[RADIOLIB_AES128_KEY_SIZE] = { 0 };
uint8_t mcNwkSKey[RADIOLIB_AES128_KEY_SIZE] = { 0 };
uint32_t mcAFCnt = 0;
uint32_t mcAFCntMax = 0;
// enable/disable CSMA for LoRaWAN
bool csmaEnabled = false;
@ -946,21 +991,21 @@ class LoRaWANNode {
// process at the same time.
uint8_t backoffMax = RADIOLIB_LORAWAN_BACKOFF_MAX_DEFAULT;
// number of CADs to estimate a clear CH
// number of CADs to estimate a clear channel
uint8_t difsSlots = RADIOLIB_LORAWAN_DIFS_DEFAULT;
// available channel frequencies from list passed during OTA activation
LoRaWANChannel_t channelPlan[2][RADIOLIB_LORAWAN_NUM_AVAILABLE_CHANNELS];
// currently configured channels for TX, RX1, RX2
LoRaWANChannel_t channels[3] = { RADIOLIB_LORAWAN_CHANNEL_NONE, RADIOLIB_LORAWAN_CHANNEL_NONE,
RADIOLIB_LORAWAN_CHANNEL_NONE };
// currently configured channels for Tx, Rx1, Rx2, RxBC
LoRaWANChannel_t channels[4] = { RADIOLIB_LORAWAN_CHANNEL_NONE, RADIOLIB_LORAWAN_CHANNEL_NONE,
RADIOLIB_LORAWAN_CHANNEL_NONE, RADIOLIB_LORAWAN_CHANNEL_NONE };
// delays between the uplink and RX1/2 windows
// delays between the uplink and Rx1/2 windows
// the first field is meaningless, but is used for offsetting for Rx windows 1 and 2
RadioLibTime_t rxDelays[3] = { 0, RADIOLIB_LORAWAN_RECEIVE_DELAY_1_MS, RADIOLIB_LORAWAN_RECEIVE_DELAY_2_MS };
// offset between TX and RX1 (such that RX1 has equal or lower DR)
// offset between Tx and Rx1 (such that Rx1 has equal or lower DR)
uint8_t rx1DrOffset = 0;
// LoRaWAN revision (1.0 vs 1.1)
@ -969,12 +1014,9 @@ class LoRaWANNode {
// Time on Air of last uplink
RadioLibTime_t lastToA = 0;
// timestamp to measure the RX1/2 delay (from uplink end)
// timestamp to measure the Rx1/2 delay (from uplink end)
RadioLibTime_t rxDelayStart = 0;
// timestamp when the Rx1/2 windows were closed (timeout or uplink received)
RadioLibTime_t rxDelayEnd = 0;
// duration of SPI transaction for phyLayer->launchMode()
RadioLibTime_t launchDuration = 0;
@ -1020,13 +1062,19 @@ class LoRaWANNode {
void micUplink(uint8_t* inOut, uint8_t lenInOut);
// transmit uplink buffer on a specified channel
int16_t transmitUplink(const LoRaWANChannel_t* chnl, uint8_t* in, uint8_t len, bool retrans);
int16_t transmitUplink(const LoRaWANChannel_t* chnl, uint8_t* in, uint8_t len, bool retrans = false);
// wait for, open and listen during receive windows; only performs listening
int16_t receiveCommon(uint8_t dir, const LoRaWANChannel_t* dlChannels, const RadioLibTime_t* dlDelays, uint8_t numWindows, RadioLibTime_t tReference);
// handle one of the Class A receive windows with a given channel and certain timestamps
int16_t receiveClassA(uint8_t dir, const LoRaWANChannel_t* dlChannel, uint8_t window, const RadioLibTime_t dlDelay, RadioLibTime_t tReference);
// handle a Class C receive window with timeout (between Class A windows) or without (between uplinks)
int16_t receiveClassC(RadioLibTime_t timeout = 0);
// open a series of Class A (and C) downlinks
int16_t receiveDownlink();
// extract downlink payload and process MAC commands
int16_t parseDownlink(uint8_t* data, size_t* len, LoRaWANEvent_t* event = NULL);
int16_t parseDownlink(uint8_t* data, size_t* len, uint8_t window, LoRaWANEvent_t* event = NULL);
// execute mac command, return the number of processed bytes for sequential processing
bool execMacCommand(uint8_t cid, uint8_t* optIn, uint8_t lenIn);
@ -1119,7 +1167,7 @@ class LoRaWANNode {
int16_t findDataRate(uint8_t dr, DataRate_t* dataRate);
// function to encrypt and decrypt payloads (regular uplink/downlink)
void processAES(const uint8_t* in, size_t len, uint8_t* key, uint8_t* out, uint32_t fCnt, uint8_t dir, uint8_t ctrId, bool counter);
void processAES(const uint8_t* in, size_t len, uint8_t* key, uint8_t* out, uint32_t addr, uint32_t fCnt, uint8_t dir, uint8_t ctrId, bool counter);
// function that allows sleeping via user-provided callback
void sleepDelay(RadioLibTime_t ms);