From c8bc1570905b382502d36ee77a22d0febca47930 Mon Sep 17 00:00:00 2001 From: StevenCellist Date: Mon, 23 Oct 2023 23:14:17 +0200 Subject: [PATCH] [LoRaWAN] Change Rx windows from CAD to RxSingle --- src/modules/SX126x/SX126x.cpp | 30 ++++ src/modules/SX126x/SX126x.h | 14 +- src/protocols/LoRaWAN/LoRaWAN.cpp | 170 +++++++----------- src/protocols/PhysicalLayer/PhysicalLayer.cpp | 8 + src/protocols/PhysicalLayer/PhysicalLayer.h | 10 ++ 5 files changed, 120 insertions(+), 112 deletions(-) diff --git a/src/modules/SX126x/SX126x.cpp b/src/modules/SX126x/SX126x.cpp index 7f739cf8..77412c5c 100644 --- a/src/modules/SX126x/SX126x.cpp +++ b/src/modules/SX126x/SX126x.cpp @@ -1436,6 +1436,36 @@ uint32_t SX126x::getTimeOnAir(size_t len) { } } +// Table below defines the size of one symbol as +// symtime = 256us * 2^T(sf,bw) +// 256us is called one symunit. +// SF: +// BW: |__7___8___9__10__11__12 +// 125kHz | 2 3 4 5 6 7 +// 250kHz | 1 2 3 4 5 6 +// 500kHz | 0 1 2 3 4 5 +// +uint32_t SX126x::calculateRxTimeout(uint8_t numSymbols, DataRate_t *datarate, uint32_t offsetUs, uint32_t& timeoutUs) { + uint8_t exponent = 0; + exponent += datarate->lora.spreadingFactor - 7; + + switch((int)datarate->lora.bandwidth) { + case 125: + exponent += 2; + break; + case 250: + exponent += 1; + break; + default: // includes 500 kHz + exponent += 0; + break; + } + + timeoutUs = (uint32_t)numSymbols * 256 * (0x01 << exponent) + offsetUs; + uint32_t timeout = timeoutUs / 15.625; + return(timeout); +} + int16_t SX126x::implicitHeader(size_t len) { return(setHeaderType(RADIOLIB_SX126X_LORA_HEADER_IMPLICIT, len)); } diff --git a/src/modules/SX126x/SX126x.h b/src/modules/SX126x/SX126x.h index 5b18b969..1dcf04c3 100644 --- a/src/modules/SX126x/SX126x.h +++ b/src/modules/SX126x/SX126x.h @@ -56,7 +56,7 @@ #define RADIOLIB_SX126X_CMD_SET_PACKET_PARAMS 0x8C #define RADIOLIB_SX126X_CMD_SET_CAD_PARAMS 0x88 #define RADIOLIB_SX126X_CMD_SET_BUFFER_BASE_ADDRESS 0x8F -#define RADIOLIB_SX126X_CMD_SET_LORA_SYMB_NUM_TIMEOUT 0x0A +#define RADIOLIB_SX126X_CMD_SET_LORA_SYMB_NUM_TIMEOUT 0xA0 // status commands #define RADIOLIB_SX126X_CMD_GET_STATUS 0xC0 @@ -219,7 +219,7 @@ #define RADIOLIB_SX126X_IRQ_HEADER_ERR 0b0000000000100000 // 5 5 LoRa header CRC error #define RADIOLIB_SX126X_IRQ_HEADER_VALID 0b0000000000010000 // 4 4 valid LoRa header received #define RADIOLIB_SX126X_IRQ_SYNC_WORD_VALID 0b0000000000001000 // 3 3 valid sync word detected -#define RADIOLIB_SX126X_IRQ_RADIOLIB_PREAMBLE_DETECTED 0b0000000000000100 // 2 2 preamble detected +#define RADIOLIB_SX126X_IRQ_PREAMBLE_DETECTED 0b0000000000000100 // 2 2 preamble detected #define RADIOLIB_SX126X_IRQ_RX_DONE 0b0000000000000010 // 1 1 packet received #define RADIOLIB_SX126X_IRQ_TX_DONE 0b0000000000000001 // 0 0 packet transmission completed #define RADIOLIB_SX126X_IRQ_RX_DEFAULT 0b0000001001100010 // 14 0 default for Rx (RX_DONE, TIMEOUT, CRC_ERR and HEADER_ERR) @@ -949,6 +949,16 @@ class SX126x: public PhysicalLayer { */ uint32_t getTimeOnAir(size_t len) override; + /*! + \brief Get the Rx timeout required to listen to a preamble of a certain number of symbols + \param numSymbols Number of symbols to listen for + \param datarate The datarate for which to calculate the timeout + \param offsetUs Additional offset in microseconds to allow increasing the timeout + \param timeoutUs Returns the timeout in microseconds for the host to sleep + \returns Timeout value in a unit that is specific for the used module + */ + uint32_t calculateRxTimeout(uint8_t numSymbols, DataRate_t *datarate, uint32_t offsetUs, uint32_t& timeoutUss); + /*! \brief Set implicit header mode for future reception/transmission. \param len Payload length in bytes. diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index db3b543e..19aaf898 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -20,14 +20,14 @@ static void LoRaWANNodeOnDownlink(void) { } // flag to indicate whether channel scan operation is complete -static volatile bool scanFlag = false; +static volatile bool downlinkTimeout = false; // interrupt service routine to handle downlinks automatically #if defined(ESP8266) || defined(ESP32) IRAM_ATTR #endif static void LoRaWANNodeOnChannelScan(void) { - scanFlag = true; + downlinkTimeout = true; } LoRaWANNode::LoRaWANNode(PhysicalLayer* phy, const LoRaWANBand_t* band) { @@ -629,7 +629,7 @@ int16_t LoRaWANNode::downlink(String& str) { int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { // check if there are any upcoming Rx windows Module* mod = this->phyLayer->getMod(); - const uint32_t scanGuard = 10; + const uint32_t scanGuard = 50; if(mod->hal->millis() - this->rxDelayStart > (this->rxDelays[1] + scanGuard)) { // time since last Tx is greater than RX2 delay + some guard period // we have nothing to downlink @@ -646,18 +646,22 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { RADIOLIB_ASSERT(state); } - // calculate the channel scanning timeout - // according to the spec, this must be at least enough time to effectively detect a preamble - uint32_t scanTimeout = this->phyLayer->getTimeOnAir(0)/1000; - - // set up everything for channel scan + downlinkTimeout = false; downlinkReceived = false; - scanFlag = false; - bool packetDetected = false; this->phyLayer->setChannelScanAction(LoRaWANNodeOnChannelScan); + // calculate the Rx timeout + // according to the spec, this must be at least enough time to effectively detect a preamble + // but pad it a bit on both sides (start and end) to make sure it is wide enough + DataRate_t dataRate; + findDataRate(this->dataRates[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK], &dataRate); + uint32_t timeoutHost = 0; + uint32_t timeoutMod = this->phyLayer->calculateRxTimeout(8, &dataRate, 2*scanGuard*1000, timeoutHost); + RADIOLIB_DEBUG_PRINTLN("RxTimeout host: %d, module: %06x", timeoutHost, timeoutMod); + // perform listening in the two Rx windows for(uint8_t i = 0; i < 2; i++) { + // wait for the start of the Rx window // the waiting duration is shortened a bit to cover any possible timing errors uint32_t waitLen = this->rxDelays[i] - (mod->hal->millis() - this->rxDelayStart); @@ -668,49 +672,27 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { mod->hal->delay(waitLen); RADIOLIB_DEBUG_PRINTLN("Opening Rx%d window...", (i+1)); - // wait until we get a preamble - uint32_t scanStart = mod->hal->millis(); - while((mod->hal->millis() - scanStart) < (scanTimeout + scanGuard)) { - // check channel detection timeout - state = this->phyLayer->startChannelScan(); - RADIOLIB_ASSERT(state); + // TODO somehow make this module-independent, currently configured for SX126x + uint16_t irqFlags = 0b0000000000000010; // RxDone + uint16_t irqMask = 0b0000000000000010; + irqFlags |= 0b0000001000000000; // RxTimeout + irqMask |= 0b0000001000000000; - // wait with some timeout, though it should not be hit - uint32_t cadStart = mod->hal->millis(); - while(!scanFlag) { - mod->hal->yield(); - if(mod->hal->millis() - cadStart >= 3000) { - // timed out, stop waiting - break; - } - } - - // check the scan result - scanFlag = false; - state = this->phyLayer->getChannelScanResult(); - if((state == RADIOLIB_PREAMBLE_DETECTED) || (state == RADIOLIB_LORA_DETECTED)) { - packetDetected = true; - - // channel scan is finished, swap the actions - this->phyLayer->clearChannelScanAction(); - this->phyLayer->setPacketReceivedAction(LoRaWANNodeOnDownlink); - - // start receiving - state = this->phyLayer->startReceive(0x00, 0b0000001001100010, 0b0000000000000010, 0); // RxSingle - RADIOLIB_ASSERT(state); - - break; - } - + uint32_t rxStart = mod->hal->millis(); + this->phyLayer->startReceive(timeoutMod, irqFlags, irqMask, 0); + RADIOLIB_DEBUG_PRINT("Opened Rx%d window (%d us timeout)... ", i+1, timeoutHost); + while(!downlinkTimeout && ((mod->hal->millis() - rxStart) < (timeoutHost / 1000))) { + mod->hal->yield(); } + RADIOLIB_DEBUG_PRINTLN("closing"); - // check if we have a packet - if(packetDetected) { - RADIOLIB_DEBUG_PRINTLN("Detected a packet..."); + // check if we detected something of a packet + if(!downlinkTimeout) { break; } else if(i == 0) { // nothing in the first window, configure for the second + this->phyLayer->standby(); state = this->phyLayer->setFrequency(this->rx2.freq); RADIOLIB_ASSERT(state); @@ -718,12 +700,15 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { findDataRate(this->rx2.drMax, &dataRate); state = this->phyLayer->setDataRate(dataRate); RADIOLIB_ASSERT(state); + timeoutMod = this->phyLayer->calculateRxTimeout(8, &dataRate, 2*scanGuard*1000, timeoutHost); + downlinkTimeout = false; } } + this->phyLayer->clearChannelScanAction(); // check if we received a packet at all - if(!packetDetected) { + if(downlinkTimeout) { this->phyLayer->standby(); if(!this->FSK) { this->phyLayer->invertIQ(false); @@ -731,27 +716,14 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { return(RADIOLIB_ERR_RX_TIMEOUT); } + this->phyLayer->setPacketReceivedAction(LoRaWANNodeOnDownlink); - // wait for reception with some timeout - uint32_t rxStart = mod->hal->millis(); while(!downlinkReceived) { mod->hal->yield(); - // 3 seconds is maximum airtime - if(mod->hal->millis() - rxStart >= 3000) { - // timed out - RADIOLIB_DEBUG_PRINTLN("Packet length: %d", this->phyLayer->getPacketLength()); - this->phyLayer->standby(); - if(!this->FSK) { - this->phyLayer->invertIQ(false); - } - // return(RADIOLIB_ERR_RX_TIMEOUT); - break; - } } // we have a message, clear actions, go to standby and reset the IQ inversion - downlinkReceived = false; - this->phyLayer->standby(); + this->phyLayer->standby(); // TODO check: this should be done automagically due to RxSingle? this->phyLayer->clearPacketReceivedAction(); if(!this->FSK) { state = this->phyLayer->invertIQ(false); @@ -760,6 +732,7 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { // get the packet length size_t downlinkMsgLen = this->phyLayer->getPacketLength(); + RADIOLIB_DEBUG_PRINTLN("Downlink message length: %d", downlinkMsgLen); // check the minimum required frame length // an extra byte is subtracted because downlink frames may not have a port @@ -803,7 +776,6 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { // TODO cache the ADR bit? uint16_t fcnt16 = LoRaWANNode::ntoh(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FCNT_POS]); LoRaWANNode::hton(&downlinkMsg[RADIOLIB_LORAWAN_BLOCK_FCNT_POS], fcnt16); - uint32_t fcnt32 = fcnt16; // calculate possible rollover once decided if this is network downlink or application downlink RADIOLIB_DEBUG_PRINTLN("downlinkMsg:"); RADIOLIB_DEBUG_HEXDUMP(downlinkMsg, RADIOLIB_AES128_BLOCK_SIZE + downlinkMsgLen); @@ -825,9 +797,12 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { fcntDownPrev = mod->hal->getPersistentParameter(RADIOLIB_PERSISTENT_PARAM_LORAWAN_N_FCNT_DOWN_ID); } + RADIOLIB_DEBUG_PRINTLN("fcnt: %d, fcntPrev: %d, isAppDownlink: %d", fcnt16, fcntDownPrev, (int)isAppDownlink); + // if this is not the first downlink... // assume a 16-bit to 32-bit rollover if difference between counters in LSB is smaller than MAX_FCNT_GAP // if that isn't the case and the received fcnt is smaller or equal to the last heard fcnt, then error + uint32_t fcnt32 = fcnt16; if(fcntDownPrev > 0) { if((fcnt16 <= fcntDownPrev) && ((0xFFFF - (uint16_t)fcntDownPrev + fcnt16) > RADIOLIB_LORAWAN_MAX_FCNT_GAP)) { #if !defined(RADIOLIB_STATIC_ONLY) @@ -839,7 +814,7 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { return(RADIOLIB_ERR_N_FCNT_DOWN_INVALID); } } else if (fcnt16 <= fcntDownPrev) { - uint16_t msb = (fcntDownPrev >> 16) + 1; // assume a rollover + uint16_t msb = (fcntDownPrev >> 16) + 1; // assume a rollover fcnt32 |= (msb << 16); // add back the MSB part } } @@ -923,7 +898,7 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { RADIOLIB_DEBUG_PRINTLN("Uplink MAC payload (%d commands):", foptsNum); RADIOLIB_DEBUG_HEXDUMP(foptsBuff, foptsBufSize); - isMACPayload = true; + this->isMACPayload = true; this->uplink(foptsBuff, foptsBufSize, RADIOLIB_LORAWAN_FPORT_MAC_COMMAND); } @@ -951,9 +926,9 @@ int16_t LoRaWANNode::downlink(uint8_t* data, size_t* len) { // there is payload, and so there should be a port too // TODO pass the port? *len = payLen - 1; + // TODO it COULD be the case that the assumed rollover is incorrect, then figure out a way to catch this and retry with just fcnt16 - // TODO does the erratum hold here as well? - processAES(&downlinkMsg[RADIOLIB_LORAWAN_FHDR_FOPTS_POS], downlinkMsgLen, this->appSKey, data, fcnt32, RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, 0x00, true); + processAES(&downlinkMsg[RADIOLIB_LORAWAN_FRAME_PAYLOAD_POS(foptsLen)], payLen - 1, this->appSKey, data, fcnt32, RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK, 0x00, true); #if !defined(RADIOLIB_STATIC_ONLY) delete[] downlinkMsg; @@ -1061,20 +1036,20 @@ int16_t LoRaWANNode::setupChannels(uint8_t* cfList) { num++; } } - // if(cfList != nullptr) { - // for(uint8_t i = 0; i < 5; i++) { - // chnl.enabled = true; - // chnl.idx = num; - // uint32_t freq = LoRaWANNode::ntoh(&cfList[3*i], 3); - // chnl.freq = (float)freq/10000.0; - // chnl.drMin = this->band->txFreqs[0].drMin; // drMin is equal for all channels - // chnl.drMax = this->band->txFreqs[0].drMax; // drMax is equal for all channels - // availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num] = chnl; - // availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][num] = chnl; - // RADIOLIB_DEBUG_PRINTLN("Channel UL/DL %d frequency = %f MHz", num, availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].freq); - // num++; - // } - // } + if(cfList != nullptr) { + for(uint8_t i = 0; i < 5; i++) { + chnl.enabled = true; + chnl.idx = num; + uint32_t freq = LoRaWANNode::ntoh(&cfList[3*i], 3); + chnl.freq = (float)freq/10000.0; + chnl.drMin = this->band->txFreqs[0].drMin; // drMin is equal for all channels + chnl.drMax = this->band->txFreqs[0].drMax; // drMax is equal for all channels + availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num] = chnl; + availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_DOWNLINK][num] = chnl; + RADIOLIB_DEBUG_PRINTLN("Channel UL/DL %d frequency = %f MHz", num, availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][num].freq); + num++; + } + } } else { // RADIOLIB_LORAWAN_CFLIST_TYPE_MASK uint8_t chSpan = 0; uint8_t chNum = 0; @@ -1301,15 +1276,6 @@ int16_t LoRaWANNode::pushMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQ } memcpy(&queue->commands[queue->numCommands], cmd, sizeof(LoRaWANMacCommand_t)); - /*RADIOLIB_DEBUG_PRINTLN("push MAC CID = %02x, len = %d, payload = %02x %02x %02x %02x %02x, repeat = %d ", - queue->commands[queue->numCommands - 1].cid, - queue->commands[queue->numCommands - 1].len, - queue->commands[queue->numCommands - 1].payload[0], - queue->commands[queue->numCommands - 1].payload[1], - queue->commands[queue->numCommands - 1].payload[2], - queue->commands[queue->numCommands - 1].payload[3], - queue->commands[queue->numCommands - 1].payload[4], - queue->commands[queue->numCommands - 1].repeat);*/ queue->numCommands++; queue->len += 1 + cmd->len; // 1 byte for command ID, len bytes for payload @@ -1322,15 +1288,6 @@ int16_t LoRaWANNode::popMacCommand(LoRaWANMacCommand_t* cmd, LoRaWANMacCommandQu } if(cmd) { - // RADIOLIB_DEBUG_PRINTLN("pop MAC CID = %02x, len = %d, payload = %02x %02x %02x %02x %02x, repeat = %d ", - // queue->commands[index].cid, - // queue->commands[index].len, - // queue->commands[index].payload[0], - // queue->commands[index].payload[1], - // queue->commands[index].payload[2], - // queue->commands[index].payload[3], - // queue->commands[index].payload[4], - // queue->commands[index].repeat); memcpy(cmd, &queue->commands[index], sizeof(LoRaWANMacCommand_t)); } @@ -1350,15 +1307,6 @@ int16_t LoRaWANNode::deleteMacCommand(uint8_t cid, LoRaWANMacCommandQueue_t* que for(size_t index = 0; index < queue->numCommands; index++) { if(queue->commands[index].cid == cid) { - // RADIOLIB_DEBUG_PRINTLN("delete MAC CID = %02x, len = %d, payload = %02x %02x %02x %02x %02x, repeat = %d ", - // queue->commands[index].cid, - // queue->commands[index].len, - // queue->commands[index].payload[0], - // queue->commands[index].payload[1], - // queue->commands[index].payload[2], - // queue->commands[index].payload[3], - // queue->commands[index].payload[4], - // queue->commands[index].repeat); queue->len -= (1 + queue->commands[index].len); // 1 byte for command ID, len for payload // move all subsequent commands one forward in the queue if(index < RADIOLIB_LORAWAN_MAC_COMMAND_QUEUE_SIZE - 1) { @@ -1412,7 +1360,7 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { uint16_t chMask = LoRaWANNode::ntoh(&cmd->payload[1]); uint8_t chMaskCntl = (cmd->payload[3] & 0x70) >> 4; uint8_t nbTrans = cmd->payload[3] & 0x0F; - RADIOLIB_DEBUG_PRINTLN("ADR REQ: dataRate = %d, txPower = %d, chMask = 0x%04x, chMaskCntl = %02x, nbTrans = %d", dr, txPower, chMask, chMaskCntl, nbTrans); + RADIOLIB_DEBUG_PRINTLN("ADR REQ: dataRate = %d, txPower = %d, chMask = 0x%02x, chMaskCntl = %02x, nbTrans = %d", dr, txPower, chMask, chMaskCntl, nbTrans); // apply the configuration uint8_t drAck = 0; @@ -1447,17 +1395,18 @@ size_t LoRaWANNode::execMacCommand(LoRaWANMacCommand_t* cmd) { this->nbTrans = nbTrans; // TODO implement channel mask uint8_t chMaskAck = 1; - (void)chMask; (void)chMaskCntl; if(this->band->cfListType == RADIOLIB_LORAWAN_CFLIST_TYPE_FREQUENCIES) { for(uint8_t i = 0; i < 16; i++) { // check if this channel ID should be enabled + RADIOLIB_DEBUG_PRINTLN("ADR channel %d: %d --> %d", i, this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled, (chMask >> i) & 0x01); if(chMask & (1UL << i)) { // if it should be enabled but is not currently defined, stop immediately if(this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled == false) { chMaskAck = 0; break; } + this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = true; } else { this->availableChannels[RADIOLIB_LORAWAN_CHANNEL_DIR_UPLINK][i].enabled = false; } @@ -1732,6 +1681,7 @@ void LoRaWANNode::processAES(uint8_t* in, size_t len, uint8_t* key, uint8_t* out // on downlink frames, this has a decryption effect because server actually "decrypts" the plaintext size_t remLen = len; for(size_t i = 0; i < numBlocks; i++) { + if(counter) { encBlock[RADIOLIB_LORAWAN_ENC_BLOCK_COUNTER_POS] = i + 1; } diff --git a/src/protocols/PhysicalLayer/PhysicalLayer.cpp b/src/protocols/PhysicalLayer/PhysicalLayer.cpp index b244b856..de31e571 100644 --- a/src/protocols/PhysicalLayer/PhysicalLayer.cpp +++ b/src/protocols/PhysicalLayer/PhysicalLayer.cpp @@ -293,6 +293,14 @@ uint32_t PhysicalLayer::getTimeOnAir(size_t len) { (void)len; return(0); } + +uint32_t PhysicalLayer::calculateRxTimeout(uint8_t numSymbols, DataRate_t *datarate, uint32_t offsetUs, uint32_t& timeoutUs) { + (void)numSymbols; + (void)datarate; + (void)offsetUs; + (void)timeoutUs; + return(RADIOLIB_ERR_UNSUPPORTED); +} int16_t PhysicalLayer::startChannelScan() { return(RADIOLIB_ERR_UNSUPPORTED); diff --git a/src/protocols/PhysicalLayer/PhysicalLayer.h b/src/protocols/PhysicalLayer/PhysicalLayer.h index c083e476..12ce0a7e 100644 --- a/src/protocols/PhysicalLayer/PhysicalLayer.h +++ b/src/protocols/PhysicalLayer/PhysicalLayer.h @@ -309,6 +309,16 @@ class PhysicalLayer { \returns Expected time-on-air in microseconds. */ virtual uint32_t getTimeOnAir(size_t len); + + /*! + \brief Get the Rx timeout required to listen to a preamble of a certain number of symbols + \param numSymbols Number of symbols to listen for + \param datarate The datarate for which to calculate the timeout + \param offsetUs Additional offset in microseconds to allow increasing the timeout + \param timeoutUs Returns the timeout in microseconds for the host to sleep + \returns Timeout value in a unit that is specific for the used module + */ + virtual uint32_t calculateRxTimeout(uint8_t numSymbols, DataRate_t* datarate, uint32_t offsetUs, uint32_t& timeoutUs); /*! \brief Interrupt-driven channel activity detection method. interrupt will be activated