#define RADIOLIB_STATIC_ONLY 1 #include "RadioLibWrappers.h" #define STATE_IDLE 0 #define STATE_RX 1 #define STATE_TX_WAIT 3 #define STATE_TX_DONE 4 #define STATE_INT_READY 16 #define NUM_NOISE_FLOOR_SAMPLES 64 #define SAMPLING_THRESHOLD 14 // periodic noise-floor calibration windows (RX duty-cycle powersaving only) #define NF_CALIB_INTERVAL_MS 60000UL // at least once a minute #define NF_CALIB_TIMEOUT_MS 5000UL // give up on the batch (busy channel) #define NF_CALIB_SETTLE_MS 20UL // frontend/AGC settle after RX entry static volatile uint8_t state = STATE_IDLE; // this function is called when a complete packet // is transmitted by the module static #if defined(ESP8266) || defined(ESP32) ICACHE_RAM_ATTR #endif void setFlag(void) { // we sent a packet, set the flag state |= STATE_INT_READY; } void RadioLibWrapper::begin() { _radio->setPacketReceivedAction(setFlag); // this is also SentComplete interrupt _preamble_sf = getSpreadingFactor(); _radio->setPreambleLength(preambleLengthForSF(_preamble_sf)); // longer preamble for lower SF improves reliability state = STATE_IDLE; if (_board->getStartupReason() == BD_STARTUP_RX_PACKET) { // received a LoRa packet (while in deep sleep) setFlag(); // LoRa packet is already received } _noise_floor = 0; _threshold = 0; _cad_enabled = false; // start average out some samples _num_floor_samples = 0; _floor_sample_sum = 0; } uint32_t RadioLibWrapper::getRngSeed() { return _radio->random(0x7FFFFFFF); } void RadioLibWrapper::setTxPower(int8_t dbm) { _cur_dbm = dbm; _dbm_valid = true; _radio->setOutputPower(dbm); } void RadioLibWrapper::idle() { _radio->standby(); state = STATE_IDLE; // need another startReceive() } void RadioLibWrapper::triggerNoiseFloorCalibrate(int threshold) { _threshold = threshold; if (_num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES) { // ignore trigger if currently sampling _num_floor_samples = 0; _floor_sample_sum = 0; } } void RadioLibWrapper::doResetAGC() { _radio->sleep(); // warm sleep to reset analog frontend } void RadioLibWrapper::resetAGC() { // make sure we're not mid-receiving and mid-sending of packet! if ((state & STATE_INT_READY) != 0 || isReceivingPacket() || (state == STATE_TX_WAIT)) return; doResetAGC(); state = STATE_IDLE; // trigger a startReceive() // Reset noise floor sampling so it reconverges from scratch. // Without this, a stuck _noise_floor of -120 makes the sampling threshold // too low (-106) to accept normal samples (~-105), self-reinforcing the // stuck value even after the receiver has recovered. _noise_floor = 0; _num_floor_samples = 0; _floor_sample_sum = 0; } void RadioLibWrapper::rxPsWatchdogCheck() { // don't interfere mid-transmit or with a completed-but-unread packet // (a pending DIO1 event is itself proof the radio is alive; recvRaw() will // re-arm and re-base the watchdog) if ((state & STATE_INT_READY) != 0 || (state & ~STATE_INT_READY) == STATE_TX_WAIT) { _wd_observe_until = 0; return; } unsigned long now = millis(); bool tripped = false; if (_rx_ps_armed && state == STATE_RX && _wd_stuck_thresh > 0) { bool busy = isChipBusy(); if (busy != _wd_last_busy) { // the sleep/listen wave is present -> radio healthy _wd_last_busy = busy; _wd_last_transition = now; _wd_stage = 0; _wd_strikes = 0; _wd_observe_until = 0; } else if (_wd_observe_until != 0) { // active observation window in progress (MCU kept awake via // isWatchdogObserving()); a healthy chip must toggle BUSY within it if ((long)(now - _wd_observe_until) >= 0) { _wd_observe_until = 0; if (!busy && isReceivingPacket()) { // BUSY held low by an ongoing reception (extended RX) - alive _wd_last_transition = now; _wd_strikes = 0; } else if (++_wd_strikes >= 2) { _wd_strikes = 0; tripped = true; } else { _wd_last_transition = now; // full threshold before the next window } } } else if (now - _wd_last_transition > _wd_stuck_thresh) { // no proof of life for too long: actively watch one full cycle _wd_observe_until = now + _wd_observe_ms; if (_wd_observe_until == 0) _wd_observe_until = 1; // 0 means "off" } } else { _wd_observe_until = 0; } if (_startrx_fails >= 3) tripped = true; // can't even re-arm receive mode if (!tripped) return; _wd_last_transition = now; // grace period before the next escalation _startrx_fails = 0; _wd_observe_until = 0; if (_wd_stage == 0) { _wd_stage = 1; n_wd_soft++; MESH_DEBUG_PRINTLN("RadioLibWrapper: watchdog: RX duty-cycle stuck, soft re-arm"); state = STATE_IDLE; // next recvRaw() re-arms receive mode } else { _wd_stage = 2; n_wd_hard++; MESH_DEBUG_PRINTLN("RadioLibWrapper: watchdog: still stuck, hard radio reset"); if (radioDeepInit()) { _rx_ps_armed = false; // chip is factory-fresh after NRST _radio->setPacketReceivedAction(setFlag); if (_params_valid) setParams(_cur_freq, _cur_bw, _cur_sf, _cur_cr); if (_dbm_valid) _radio->setOutputPower(_cur_dbm); } state = STATE_IDLE; // re-arm (rx powersaving settings are kept in members) } } // Initial and periodic noise-floor calibration, active only with RX duty-cycle powersaving: // a duty-cycled receiver can't be sampled reliably (the frontend is off in the // sleep windows and settling right after each wake), so at least once a minute // the receive mode is dropped to plain continuous RX, a fresh sample batch is // collected exactly like the non-powersaving path does, and the duty cycle is // re-armed. The published average stays in _noise_floor as usual. void RadioLibWrapper::noiseFloorCalibCheck() { unsigned long now = millis(); if (_nf_calib_active) { if (!_rx_ps_enabled || (long)(now - _nf_calib_deadline) >= 0) { // powersaving turned off mid-window, or the batch couldn't complete // (busy channel / stuck filter) - keep the previous floor endNoiseFloorCalib(now); } } else if (_rx_ps_enabled && _rx_ps_armed && state == STATE_RX && (_nf_last_calib == 0 || now - _nf_last_calib >= NF_CALIB_INTERVAL_MS) && !isReceivingPacket()) { // never interrupt an ongoing reception to calibrate (a TX in flight is // already excluded by state == STATE_RX); retries next loop iteration _nf_calib_active = true; _nf_calib_deadline = now + NF_CALIB_TIMEOUT_MS; _nf_sample_from = now + NF_CALIB_SETTLE_MS; _num_floor_samples = 0; // start a fresh batch for this window _floor_sample_sum = 0; state = STATE_IDLE; // recvRaw() re-arms; startReceiveMode() sees the // active flag and starts continuous RX, not duty-cycle } } void RadioLibWrapper::endNoiseFloorCalib(unsigned long now) { _nf_calib_active = false; _nf_last_calib = now; // force a receive re-arm back into duty-cycle mode, but don't clobber a // completed-but-unread packet or an in-flight TX (recvRaw()/onSendFinished() // will re-arm right after those anyway; same guard style as setRxPowerSaving) if ((state & STATE_INT_READY) == 0 && (state & ~STATE_INT_READY) != STATE_TX_WAIT) { state = STATE_IDLE; } } void RadioLibWrapper::loop() { if (_rx_ps_enabled) { rxPsWatchdogCheck(); } noiseFloorCalibCheck(); if (state == STATE_RX && _num_floor_samples < NUM_NOISE_FLOOR_SAMPLES) { // Noise floor is only sampled outside RX duty-cycle mode: continuously in // plain RX (powersaving off), or inside the periodic calibration window // (powersaving on), skipping the first moments after RX entry there while // the frontend/AGC settles (unsettled GetRssiInst reads ~-127 dBm garbage). if (!_rx_ps_armed && !(_nf_calib_active && (long)(millis() - _nf_sample_from) < 0) && !isReceivingPacket()) { int rssi = getCurrentRSSI(); if (rssi < _noise_floor + SAMPLING_THRESHOLD) { // only consider samples below current floor + sampling THRESHOLD _num_floor_samples++; _floor_sample_sum += rssi; } } } else if (_num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES && _floor_sample_sum != 0) { _noise_floor = _floor_sample_sum / NUM_NOISE_FLOOR_SAMPLES; if (_noise_floor < -120) { _noise_floor = -120; // clamp to lower bound of -120dBi } _floor_sample_sum = 0; MESH_DEBUG_PRINTLN("RadioLibWrapper: noise_floor = %d", (int)_noise_floor); if (_nf_calib_active) { endNoiseFloorCalib(millis()); // fresh floor published - back to duty cycle } } } void RadioLibWrapper::startRecv() { int err = startReceiveMode(); if (err == RADIOLIB_ERR_NONE) { state = STATE_RX; _startrx_fails = 0; if (_rx_ps_armed) { // (re)base the duty-cycle watchdog on the freshly armed cycle _wd_last_busy = isChipBusy(); _wd_last_transition = millis(); // Longest legitimate silence on the BUSY pin: one full cycle, plus the // extended RX after a (possibly false) preamble detect (2*rx + sleep), // plus a worst-case packet airtime, plus margin for TCXO/transitions. // Floored at 60s so a light-sleeping MCU (ESP32 wakes every ~30s) opens // an observation window every couple of wakeups instead of on each one. uint32_t rx_ms = _rx_ps_rx_us / 1000, sleep_ms = _rx_ps_sleep_us / 1000; _wd_stuck_thresh = (rx_ms + sleep_ms) + 2 * (2 * rx_ms + sleep_ms) + getEstAirtimeFor(MAX_TRANS_UNIT) + 1000; if (_wd_stuck_thresh < 60000) _wd_stuck_thresh = 60000; // active observation window must cover one full duty cycle _wd_observe_ms = rx_ms + sleep_ms + 50; if (_wd_observe_ms > 1500) _wd_observe_ms = 1500; } } else { if (_startrx_fails < 255) _startrx_fails++; MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startReceiveMode(%d)", err); } } int RadioLibWrapper::startReceiveMode() { return _radio->startReceive(); } void RadioLibWrapper::stopReceiveDutyCycle() { // The duty-cycle sequencer only stops on RxDone or an explicit standby; // issuing other mode commands while it runs leads to undefined behaviour. _radio->standby(); _rx_ps_armed = false; } bool RadioLibWrapper::isPacketReady() { if (!_rx_ps_armed) return true; // continuous RX: DIO1 only fires for RxDone/TxDone here // In duty-cycle RX the DIO1 interrupt also fires for RX timeout (false // preamble detect) and header errors. GetRxBufferStatus still reports the // *previous* packet's length then, so reading the buffer would re-deliver // stale bytes as a ghost packet. Only read when the radio reports RxDone. // (checkIrq errors are treated as ready, falling back to old behaviour.) return _radio->checkIrq(RADIOLIB_IRQ_RX_DONE) != 0; } bool RadioLibWrapper::isInRecvMode() const { return (state & ~STATE_INT_READY) == STATE_RX; } bool RadioLibWrapper::setRxPowerSaving(bool enabled, uint32_t rx_us, uint32_t sleep_us) { if (enabled && !supportsRxPowerSaving()) { return false; } _rx_ps_enabled = enabled; _rx_ps_rx_us = rx_us; _rx_ps_sleep_us = sleep_us; // Force the next recvRaw() to arm the requested RX mode, but don't clobber a // completed-but-unread packet (STATE_INT_READY): recvRaw() will consume it and // then re-arm with the new mode. Also leave an in-flight TX alone. (Same // non-atomic guard style as resetAGC().) if ((state & STATE_INT_READY) == 0 && (state & ~STATE_INT_READY) != STATE_TX_WAIT) { state = STATE_IDLE; } return true; } int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) { int len = 0; if (state & STATE_INT_READY) { if (isPacketReady()) { len = _radio->getPacketLength(); if (len > 0) { if (len > sz) { len = sz; } int err = _radio->readData(bytes, len); if (err != RADIOLIB_ERR_NONE) { MESH_DEBUG_PRINTLN("RadioLibWrapper: error: readData(%d)", err); len = 0; n_recv_errors++; } else { // Serial.print(" readData() -> "); Serial.println(len); n_recv++; } } } state = STATE_IDLE; // need another startReceive() } if (state != STATE_RX) { startRecv(); } return len; } uint32_t RadioLibWrapper::getEstAirtimeFor(int len_bytes) { return _radio->getTimeOnAir(len_bytes) / 1000; } bool RadioLibWrapper::startSendRaw(const uint8_t* bytes, int len) { if (_rx_ps_armed) { // stop the duty-cycle sequencer before SetTx, otherwise its next RTC // event can fire mid-transmission and abort the TX stopReceiveDutyCycle(); } _board->onBeforeTransmit(); int err = _radio->startTransmit((uint8_t *) bytes, len); if (err == RADIOLIB_ERR_NONE) { state = STATE_TX_WAIT; return true; } MESH_DEBUG_PRINTLN("RadioLibWrapper: error: startTransmit(%d)", err); idle(); // trigger another startRecv() _board->onAfterTransmit(); return false; } bool RadioLibWrapper::isSendComplete() { if (state & STATE_INT_READY) { state = STATE_IDLE; n_sent++; return true; } return false; } void RadioLibWrapper::onSendFinished() { _radio->finishTransmit(); _board->onAfterTransmit(); state = STATE_IDLE; } int16_t RadioLibWrapper::performChannelScan() { return _radio->scanChannel(); } bool RadioLibWrapper::isChannelActive() { // int.thresh: RSSI-based interference detection (relative to noise floor). // In RX duty-cycle mode only checked while the chip is in a listen window // (during the sleep window the frontend is off and the read would stall). if (_threshold != 0 && !(_rx_ps_armed && isChipBusy()) && getCurrentRSSI() > _noise_floor + _threshold) return true; // cad: hardware channel activity detection if (_cad_enabled) { int16_t result = performChannelScan(); // scanChannel() triggers DIO interrupt (CAD done) which sets STATE_INT_READY // via setFlag() ISR. Clear it before restarting RX so recvRaw() doesn't // try to read a non-existent packet and count a spurious recv error. state = STATE_IDLE; startRecv(); if (result != RADIOLIB_CHANNEL_FREE) { _board->n_cad_busy++; return true; } } return false; } float RadioLibWrapper::getLastRSSI() const { return _radio->getRSSI(); } float RadioLibWrapper::getLastSNR() const { return _radio->getSNR(); } // Approximate SNR threshold per SF for successful reception (based on Semtech datasheets) static float snr_threshold[] = { -7.5, // SF7 needs at least -7.5 dB SNR -10, // SF8 needs at least -10 dB SNR -12.5, // SF9 needs at least -12.5 dB SNR -15, // SF10 needs at least -15 dB SNR -17.5,// SF11 needs at least -17.5 dB SNR -20 // SF12 needs at least -20 dB SNR }; float RadioLibWrapper::packetScoreInt(float snr, int sf, int packet_len) { if (sf < 7) return 0.0f; if (snr < snr_threshold[sf - 7]) return 0.0f; // Below threshold, no chance of success auto success_rate_based_on_snr = (snr - snr_threshold[sf - 7]) / 10.0; auto collision_penalty = 1 - (packet_len / 256.0); // Assuming max packet of 256 bytes return max(0.0, min(1.0, success_rate_based_on_snr * collision_penalty)); }