From 8c2c7b6cb5984c29dada8c4aca6ed14d90c07f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Grome=C5=A1?= Date: Thu, 6 Feb 2025 07:00:03 +0100 Subject: [PATCH] [LoRaWAN] Add methods to allow user-provided sleep function (#1410) * [LoRaWAN] Add methods to allow user-provided sleep function * Add example sleep function * [LoRaWAN] Switch all delay calls to sleepDelay * [LoRaWAN] Remove unused variable --------- Co-authored-by: StevenCellist --- .../LoRaWAN_Reference/LoRaWAN_Reference.ino | 3 ++ examples/LoRaWAN/LoRaWAN_Reference/config.h | 15 +++++++++ keywords.txt | 1 + src/protocols/LoRaWAN/LoRaWAN.cpp | 33 ++++++++++++++----- src/protocols/LoRaWAN/LoRaWAN.h | 20 +++++++++++ 5 files changed, 63 insertions(+), 9 deletions(-) diff --git a/examples/LoRaWAN/LoRaWAN_Reference/LoRaWAN_Reference.ino b/examples/LoRaWAN/LoRaWAN_Reference/LoRaWAN_Reference.ino index 87d7f1eb..884df675 100644 --- a/examples/LoRaWAN/LoRaWAN_Reference/LoRaWAN_Reference.ino +++ b/examples/LoRaWAN/LoRaWAN_Reference/LoRaWAN_Reference.ino @@ -48,6 +48,9 @@ void setup() { // Override the default join rate uint8_t joinDR = 4; + // Optionally provide a custom sleep function - see config.h + //node.setSleepFunction(customDelay); + // Setup the OTAA session information node.beginOTAA(joinEUI, devEUI, nwkKey, appKey); diff --git a/examples/LoRaWAN/LoRaWAN_Reference/config.h b/examples/LoRaWAN/LoRaWAN_Reference/config.h index 21c79f8e..565e32d1 100644 --- a/examples/LoRaWAN/LoRaWAN_Reference/config.h +++ b/examples/LoRaWAN/LoRaWAN_Reference/config.h @@ -142,4 +142,19 @@ void arrayDump(uint8_t *buffer, uint16_t len) { Serial.println(); } +// Custom delay function: +// Communication over LoRaWAN includes a lot of delays. +// By default, RadioLib will use the Arduino delay() function, +// which will waste a lot of power. However, you can put your +// microcontroller to sleep instead by customizing the function below, +// and providing it to RadioLib via "node.setSleepFunction". +// NOTE: You ahve to ensure that this function is timed precisely, and +// does actually wait for the amount of time specified! +// Failure to do so will result in missed downlinks or failed join! +void customDelay(RadioLibTime_t ms) { + // this is just an example, so we use the Arduino delay() function, + // but you can put your microcontroller to sleep here + ::delay(ms); +} + #endif diff --git a/keywords.txt b/keywords.txt index 85ed0ed9..64415e2f 100644 --- a/keywords.txt +++ b/keywords.txt @@ -379,6 +379,7 @@ getLastToA KEYWORD2 dutyCycleInterval KEYWORD2 timeUntilUplink KEYWORD2 getMaxPayloadLen KEYWORD2 +setSleepFunction KEYWORD2 ####################################### # Constants (LITERAL1) diff --git a/src/protocols/LoRaWAN/LoRaWAN.cpp b/src/protocols/LoRaWAN/LoRaWAN.cpp index 333623a1..6c7f5a31 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.cpp +++ b/src/protocols/LoRaWAN/LoRaWAN.cpp @@ -67,7 +67,6 @@ int16_t LoRaWANNode::sendReceive(const uint8_t* dataUp, size_t lenUp, uint8_t fP return(RADIOLIB_ERR_NULL_POINTER); } int16_t state = RADIOLIB_ERR_UNKNOWN; - Module* mod = this->phyLayer->getMod(); // if after (at) ADR_ACK_LIMIT frames no RekeyConf was received, revert to Join state if(this->fCntUp == (1UL << this->adrLimitExp)) { @@ -147,7 +146,7 @@ int16_t LoRaWANNode::sendReceive(const uint8_t* dataUp, size_t lenUp, uint8_t fP // RETRANSMIT_TIMEOUT is 2s +/- 1s (RP v1.0.4) // must be present after any confirmed frame, so we force this here if(isConfirmed) { - mod->hal->delay(this->phyLayer->random(1000, 3000)); + this->sleepDelay(this->phyLayer->random(1000, 3000)); } // if an error occured or a downlink was received, stop retransmission @@ -927,7 +926,7 @@ int16_t LoRaWANNode::activateOTAA(uint8_t joinDr, LoRaWANJoinEvent_t *joinEvent) if(this->tUplink > tNow) { RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Delaying transmission by %lu ms", (unsigned long)(this->tUplink - tNow)); if(this->tUplink > mod->hal->millis()) { - mod->hal->delay(this->tUplink - mod->hal->millis()); + this->sleepDelay(this->tUplink - mod->hal->millis()); } } @@ -936,7 +935,7 @@ int16_t LoRaWANNode::activateOTAA(uint8_t joinDr, LoRaWANJoinEvent_t *joinEvent) RADIOLIB_ASSERT(state); // sleep for the duration of the transmission - mod->hal->delay(toa); + this->sleepDelay(toa); RadioLibTime_t txEnd = mod->hal->millis(); // wait for an additional transmission duration as Tx timeout period @@ -1342,7 +1341,7 @@ int16_t LoRaWANNode::transmitUplink(const LoRaWANChannel_t* chnl, uint8_t* in, u if(this->tUplink > tNow) { RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Delaying transmission by %lu ms", (unsigned long)(this->tUplink - tNow)); if(this->tUplink > mod->hal->millis()) { - mod->hal->delay(this->tUplink - mod->hal->millis()); + this->sleepDelay(this->tUplink - mod->hal->millis()); } } @@ -1351,7 +1350,7 @@ int16_t LoRaWANNode::transmitUplink(const LoRaWANChannel_t* chnl, uint8_t* in, u RADIOLIB_ASSERT(state); // sleep for the duration of the transmission - mod->hal->delay(toa); + this->sleepDelay(toa); RadioLibTime_t txEnd = mod->hal->millis(); // wait for an additional transmission duration as Tx timeout period @@ -1398,7 +1397,7 @@ int16_t LoRaWANNode::receiveCommon(uint8_t dir, const LoRaWANChannel_t* dlChanne // if function was called while Rx windows are in progress, // wait until last window closes to prevent very bad stuff if(now < tReference + dlDelays[numWindows]) { - mod->hal->delay(dlDelays[numWindows] + tReference - now); + this->sleepDelay(dlDelays[numWindows] + tReference - now); } // update the end timestamp in case user got stuck between uplink and downlink this->rxDelayEnd = mod->hal->millis(); @@ -1444,7 +1443,7 @@ int16_t LoRaWANNode::receiveCommon(uint8_t dir, const LoRaWANChannel_t* dlChanne if(waitLen > this->scanGuard) { waitLen -= this->scanGuard; } - mod->hal->delay(waitLen); + this->sleepDelay(waitLen); // open Rx window by starting receive with specified timeout state = this->phyLayer->launchMode(); @@ -1453,7 +1452,7 @@ int16_t LoRaWANNode::receiveCommon(uint8_t dir, const LoRaWANChannel_t* dlChanne RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Opening Rx%d window (%d ms timeout)... <-- Rx Delay end ", window, (int)(timeoutHost / 1000 + 2)); // wait for the timeout to complete (and a small additional delay) - mod->hal->delay(timeoutHost / 1000 + 2); + this->sleepDelay(timeoutHost / 1000 + this->scanGuard / 2); RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Closing Rx%d window", window); // if the IRQ bit for Rx Timeout is not set, something is received, so stop the windows @@ -3376,6 +3375,10 @@ uint8_t LoRaWANNode::getMaxPayloadLen() { return(curLen - 13 - this->fOptsUpLen); } +void LoRaWANNode::setSleepFunction(SleepCb_t cb) { + this->sleepCb = cb; +} + int16_t LoRaWANNode::findDataRate(uint8_t dr, DataRate_t* dataRate) { int16_t state = this->phyLayer->standby(); if(state != RADIOLIB_ERR_NONE) { @@ -3500,6 +3503,18 @@ void LoRaWANNode::processAES(const uint8_t* in, size_t len, uint8_t* key, uint8_ } } +void LoRaWANNode::sleepDelay(RadioLibTime_t ms) { + // if the user did not provide sleep callback, or the duration is short, just call delay + if((this->sleepCb == nullptr) || (ms <= RADIOLIB_LORAWAN_DELAY_SLEEP_THRESHOLD)) { + Module* mod = this->phyLayer->getMod(); + mod->hal->delay(ms); + return; + } + + // otherwise, call the user-provided callback + this->sleepCb(ms); +} + int16_t LoRaWANNode::checkBufferCommon(const uint8_t *buffer, uint16_t size) { // check if there are actually values in the buffer size_t i = 0; diff --git a/src/protocols/LoRaWAN/LoRaWAN.h b/src/protocols/LoRaWAN/LoRaWAN.h index 48272336..fd86f992 100644 --- a/src/protocols/LoRaWAN/LoRaWAN.h +++ b/src/protocols/LoRaWAN/LoRaWAN.h @@ -210,6 +210,9 @@ #define RADIOLIB_LORAWAN_MAX_DOWNLINK_SIZE (250) +// threshold at which sleeping via user callback enabled, in ms +#define RADIOLIB_LORAWAN_DELAY_SLEEP_THRESHOLD (50) + /*! \struct LoRaWANMacCommand_t \brief MAC command specification structure. @@ -832,6 +835,18 @@ class LoRaWANNode { */ uint8_t getMaxPayloadLen(); + /*! \brief Callback to a user-provided sleep function. */ + typedef void (*SleepCb_t)(RadioLibTime_t ms); + + /*! + \brief Set custom delay/sleep function callback. If set, LoRaWAN node will call + this function to wait for periods of time longer than RADIOLIB_LORAWAN_DELAY_SLEEP_THRESHOLD. + This can be used to lower the power consumption by putting the host microcontroller to sleep. + NOTE: Since this method will call a user-provided function, it is up to the user to ensure + that the time duration spent in that sleep function is accurate to at least 1 ms! + */ + 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). @@ -973,6 +988,8 @@ class LoRaWANNode { // allow port 226 for devices implementing TS011 bool TS011 = false; + SleepCb_t sleepCb = nullptr; + // this will reset the device credentials, so the device starts completely new void clearNonces(); @@ -1102,6 +1119,9 @@ class LoRaWANNode { // 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); + // function that allows sleeping via user-provided callback + void sleepDelay(RadioLibTime_t ms); + // 16-bit checksum method that takes a uint8_t array of even length and calculates the checksum static uint16_t checkSum16(const uint8_t *key, uint16_t keyLen);