diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index d257964f..8573e50d 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -15,6 +15,16 @@ static bool g_sim_ready = false; #endif +#ifdef DISPLAY_HAS_BUSY_PUMP +// Run from the e-ink driver's BUSY-pin wait (DisplayDriver::setBusyPumpFn(), +// wired in setup() below) so packets landing during a refresh are pulled off +// the radio as they arrive, and a TX that finishes mid-refresh goes straight +// back to listening -- see RadioLibWrapper::pumpRecvDuringBlockingWait(). +static void pumpRadioDuringDisplayBusyWait(void*) { + radio_driver.pumpRecvDuringBlockingWait(); +} +#endif + // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { uint32_t n = 0; @@ -293,6 +303,9 @@ void setup() { if (disp && the_mesh.getNodePrefs()) disp->setBrightness(the_mesh.getNodePrefs()->display_brightness); ui_task.begin(disp, &sensors, the_mesh.getNodePrefs()); // still want to pass this in as dependency, as prefs might be moved +#ifdef DISPLAY_HAS_BUSY_PUMP + if (disp) disp->setBusyPumpFn(pumpRadioDuringDisplayBusyWait, nullptr); +#endif #endif #ifdef NRF52_PLATFORM diff --git a/src/helpers/radiolib/CustomLLCC68Wrapper.h b/src/helpers/radiolib/CustomLLCC68Wrapper.h index ae0fe0a2..8180b088 100644 --- a/src/helpers/radiolib/CustomLLCC68Wrapper.h +++ b/src/helpers/radiolib/CustomLLCC68Wrapper.h @@ -26,8 +26,8 @@ public: float getCurrentRSSI() override { return ((CustomLLCC68 *)_radio)->getRSSI(false); } - float getLastRSSI() const override { return ((CustomLLCC68 *)_radio)->getRSSI(); } - float getLastSNR() const override { return ((CustomLLCC68 *)_radio)->getSNR(); } + float readLiveRSSI() const override { return ((CustomLLCC68 *)_radio)->getRSSI(); } + float readLiveSNR() const override { return ((CustomLLCC68 *)_radio)->getSNR(); } float packetScore(float snr, int packet_len) override { int sf = ((CustomLLCC68 *)_radio)->spreadingFactor; diff --git a/src/helpers/radiolib/CustomLR1110Wrapper.h b/src/helpers/radiolib/CustomLR1110Wrapper.h index e7aaeb93..fdcaad7e 100644 --- a/src/helpers/radiolib/CustomLR1110Wrapper.h +++ b/src/helpers/radiolib/CustomLR1110Wrapper.h @@ -38,8 +38,8 @@ public: _radio->setPreambleLength(preambleLengthForSF(getSpreadingFactor())); // overcomes weird issues with small and big pkts } - float getLastRSSI() const override { return ((CustomLR1110 *)_radio)->getRSSI(); } - float getLastSNR() const override { return ((CustomLR1110 *)_radio)->getSNR(); } + float readLiveRSSI() const override { return ((CustomLR1110 *)_radio)->getRSSI(); } + float readLiveSNR() const override { return ((CustomLR1110 *)_radio)->getSNR(); } uint8_t getSpreadingFactor() const override { return ((CustomLR1110 *)_radio)->getSpreadingFactor(); } diff --git a/src/helpers/radiolib/CustomLR2021Wrapper.h b/src/helpers/radiolib/CustomLR2021Wrapper.h index 8299e2bf..6b3b5f46 100644 --- a/src/helpers/radiolib/CustomLR2021Wrapper.h +++ b/src/helpers/radiolib/CustomLR2021Wrapper.h @@ -88,8 +88,8 @@ public: _radio->setPreambleLength(preambleLengthForSF(getSpreadingFactor())); // overcomes weird issues with small and big pkts } - float getLastRSSI() const override { return ((CustomLR2021 *)_radio)->getRSSI(); } - float getLastSNR() const override { return ((CustomLR2021 *)_radio)->getSNR(); } + float readLiveRSSI() const override { return ((CustomLR2021 *)_radio)->getRSSI(); } + float readLiveSNR() const override { return ((CustomLR2021 *)_radio)->getSNR(); } uint8_t getSpreadingFactor() const override { return ((CustomLR2021 *)_radio)->getSpreadingFactor(); } diff --git a/src/helpers/radiolib/CustomSTM32WLxWrapper.h b/src/helpers/radiolib/CustomSTM32WLxWrapper.h index a4ba4971..4b537814 100644 --- a/src/helpers/radiolib/CustomSTM32WLxWrapper.h +++ b/src/helpers/radiolib/CustomSTM32WLxWrapper.h @@ -26,8 +26,8 @@ public: float getCurrentRSSI() override { return ((CustomSTM32WLx *)_radio)->getRSSI(false); } - float getLastRSSI() const override { return ((CustomSTM32WLx *)_radio)->getRSSI(); } - float getLastSNR() const override { return ((CustomSTM32WLx *)_radio)->getSNR(); } + float readLiveRSSI() const override { return ((CustomSTM32WLx *)_radio)->getRSSI(); } + float readLiveSNR() const override { return ((CustomSTM32WLx *)_radio)->getSNR(); } float packetScore(float snr, int packet_len) override { int sf = ((CustomSTM32WLx *)_radio)->spreadingFactor; diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index 13edda03..839eaef2 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -42,8 +42,8 @@ public: float getCurrentRSSI() override { return ((CustomSX1262 *)_radio)->getRSSI(false); } - float getLastRSSI() const override { return ((CustomSX1262 *)_radio)->getRSSI(); } - float getLastSNR() const override { return ((CustomSX1262 *)_radio)->getSNR(); } + float readLiveRSSI() const override { return ((CustomSX1262 *)_radio)->getRSSI(); } + float readLiveSNR() const override { return ((CustomSX1262 *)_radio)->getSNR(); } float packetScore(float snr, int packet_len) override { int sf = ((CustomSX1262 *)_radio)->spreadingFactor; diff --git a/src/helpers/radiolib/CustomSX1268Wrapper.h b/src/helpers/radiolib/CustomSX1268Wrapper.h index 70f5dabd..be1eec40 100644 --- a/src/helpers/radiolib/CustomSX1268Wrapper.h +++ b/src/helpers/radiolib/CustomSX1268Wrapper.h @@ -29,8 +29,8 @@ public: float getCurrentRSSI() override { return ((CustomSX1268 *)_radio)->getRSSI(false); } - float getLastRSSI() const override { return ((CustomSX1268 *)_radio)->getRSSI(); } - float getLastSNR() const override { return ((CustomSX1268 *)_radio)->getSNR(); } + float readLiveRSSI() const override { return ((CustomSX1268 *)_radio)->getRSSI(); } + float readLiveSNR() const override { return ((CustomSX1268 *)_radio)->getSNR(); } float packetScore(float snr, int packet_len) override { int sf = ((CustomSX1268 *)_radio)->spreadingFactor; diff --git a/src/helpers/radiolib/CustomSX1276Wrapper.h b/src/helpers/radiolib/CustomSX1276Wrapper.h index 9d75ce12..525e2ad8 100644 --- a/src/helpers/radiolib/CustomSX1276Wrapper.h +++ b/src/helpers/radiolib/CustomSX1276Wrapper.h @@ -25,8 +25,8 @@ public: float getCurrentRSSI() override { return ((CustomSX1276 *)_radio)->getRSSI(false); } - float getLastRSSI() const override { return ((CustomSX1276 *)_radio)->getRSSI(); } - float getLastSNR() const override { return ((CustomSX1276 *)_radio)->getSNR(); } + float readLiveRSSI() const override { return ((CustomSX1276 *)_radio)->getRSSI(); } + float readLiveSNR() const override { return ((CustomSX1276 *)_radio)->getSNR(); } float packetScore(float snr, int packet_len) override { int sf = ((CustomSX1276 *)_radio)->spreadingFactor; diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index d17d5777..33847a18 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -253,6 +253,23 @@ bool RadioLibWrapper::isInRecvMode() const { } int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) { + if (_pump_count > 0) { + // pumpRecvDuringBlockingWait() already pulled this off the chip while + // the main loop was stuck (e-ink refresh) -- hand over the staged copy. + // Any packet that has landed since stays flagged and is read live once + // the queue is empty. + uint8_t slot = _pump_head; + int len = _pump_len[slot]; + if (len > sz) len = sz; + memcpy(bytes, _pump_data[slot], len); + _pumped_pop_snr = _pump_snr[slot]; + _pumped_pop_rssi = _pump_rssi[slot]; + _last_recv_was_pumped = true; + _pump_head = (_pump_head + 1) % PUMP_QUEUE_SIZE; + _pump_count--; + return len; + } + int len = 0; if (state & STATE_INT_READY) { len = _radio->getPacketLength(); @@ -268,6 +285,7 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) { n_recv++; } } + _last_recv_was_pumped = false; #if defined(USE_LR2021) state = STATE_RX; // LR2021 stays in Rx after readData, calling startReceive while still in Rx throws -706 errors #else @@ -281,6 +299,49 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) { return len; } +// See the class comment in RadioLibWrappers.h. STATE_INT_READY is shared by +// the TX-done and RX-done interrupts, so the base state disambiguates them; +// anything else (mid-TX, idle) is left alone. +void RadioLibWrapper::pumpRecvDuringBlockingWait() { + if (state == (uint8_t)(STATE_TX_WAIT | STATE_INT_READY)) { + // After TX_DONE the chip falls back to standby: deaf until the main loop + // reaches isSendComplete()/onSendFinished(). Do the radio side of + // onSendFinished() now and go straight back to RX; those two then only + // report it (see _tx_done_early). + _radio->finishTransmit(); + _board->onAfterTransmit(); + n_sent++; + _tx_done_early = true; + state = STATE_IDLE; + armRecv(); + return; + } + if (state != (uint8_t)(STATE_RX | STATE_INT_READY)) return; + if (_pump_count >= PUMP_QUEUE_SIZE) return; // staging full -- let the main loop drain it via recvRaw() first + + uint8_t slot = (_pump_head + _pump_count) % PUMP_QUEUE_SIZE; + int len = _radio->getPacketLength(); + if (len > 0) { + if (len > MAX_TRANS_UNIT) len = MAX_TRANS_UNIT; + int err = _radio->readData(_pump_data[slot], len); + if (err == RADIOLIB_ERR_NONE) { + _pump_len[slot] = (uint8_t)len; + _pump_snr[slot] = readLiveSNR(); + _pump_rssi[slot] = readLiveRSSI(); + _pump_count++; + n_recv++; + } else { + n_recv_errors++; + } + } + #if defined(USE_LR2021) + state = STATE_RX; + #else + state = STATE_IDLE; + #endif + armRecv(); +} + uint32_t RadioLibWrapper::getEstAirtimeFor(int len_bytes) { return _radio->getTimeOnAir(len_bytes) / 1000; } @@ -299,6 +360,9 @@ bool RadioLibWrapper::startSendRaw(const uint8_t* bytes, int len) { } bool RadioLibWrapper::isSendComplete() { + // Already finished by pumpRecvDuringBlockingWait(): state now belongs to + // the re-armed RX (and may flag a newly received packet), so don't touch it. + if (_tx_done_early) return true; if (state & STATE_INT_READY) { state = STATE_IDLE; n_sent++; @@ -308,6 +372,10 @@ bool RadioLibWrapper::isSendComplete() { } void RadioLibWrapper::onSendFinished() { + if (_tx_done_early) { + _tx_done_early = false; + return; + } _radio->finishTransmit(); _board->onAfterTransmit(); state = STATE_IDLE; @@ -336,10 +404,13 @@ bool RadioLibWrapper::isChannelActive() { } float RadioLibWrapper::getLastRSSI() const { - return _radio->getRSSI(); + // A packet handed back via the pump-queue path (recvRaw()) needs the + // reading captured when it was pulled off the chip, not whatever's newest + // in the registers now -- see the field comments in RadioLibWrappers.h. + return _last_recv_was_pumped ? _pumped_pop_rssi : readLiveRSSI(); } float RadioLibWrapper::getLastSNR() const { - return _radio->getSNR(); + return _last_recv_was_pumped ? _pumped_pop_snr : readLiveSNR(); } float RadioLibWrapper::packetScoreInt(float snr, int sf, int packet_len) { diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 2302bec2..33a0dc1b 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -89,6 +89,47 @@ protected: // firmware defaults (frequency/bandwidth/SF/CR/TX power/preamble/gain). virtual bool radioHardReset() { return false; } + // --- Busy-wait packet pump (see pumpRecvDuringBlockingWait() below) --- + // A display driver's e-ink busy-wait can block the main loop for over a + // second per refresh (GxEPD2 setBusyCallback()). The radio stays in + // continuous RX meanwhile, but only the latest packet is readable: the + // chip reports a single length/offset, so if a second packet completes + // before the loop gets back to recvRaw(), the first is lost -- and IRQ + // flags are sticky, so a later CRC-failed packet makes readData() reject + // a good one still waiting. pumpRecvDuringBlockingWait() pulls each packet + // into this small staging queue as it lands, deliberately WITHOUT touching + // packet parsing/dispatch (Dispatcher::checkRecv() does that later, back + // on the main loop) -- reaching into routing/UI from here would reenter + // the very display code that's blocked calling us. + // A TX finishing mid-refresh is worse: after TX_DONE the chip falls back to + // standby and hears nothing at all until re-armed -- so the pump also + // finishes the TX and resumes RX right away. + static const uint8_t PUMP_QUEUE_SIZE = 2; + uint8_t _pump_data[PUMP_QUEUE_SIZE][MAX_TRANS_UNIT]; + uint8_t _pump_len[PUMP_QUEUE_SIZE]; + float _pump_snr[PUMP_QUEUE_SIZE]; + float _pump_rssi[PUMP_QUEUE_SIZE]; + uint8_t _pump_count = 0, _pump_head = 0; + // Set whenever recvRaw() hands back a packet that pumpRecvDuringBlockingWait() + // already pulled off the chip, so getLastRSSI()/getLastSNR() (below) report + // the reading captured at THAT read -- not whatever's newest in the chip's + // registers if another packet has landed (live or pumped) since. + bool _last_recv_was_pumped = false; + float _pumped_pop_snr = 0, _pumped_pop_rssi = 0; + // The pump already did onSendFinished()'s radio work and re-armed RX, so + // isSendComplete() reports done and onSendFinished() skips the radio part + // (it would drop the chip back to standby and clobber a pending RX flag). + // A subclass onSendFinished() extension then runs with RX already armed. + bool _tx_done_early = false; + + // Chip-specific SNR/RSSI register reads. Implemented per radio type + // (renamed from what used to be each subclass's getLastRSSI()/getLastSNR() + // override) so the base class can route getLastRSSI()/getLastSNR() through + // the pumped-packet cache above instead of every subclass needing to know + // about it. + virtual float readLiveRSSI() const = 0; + virtual float readLiveSNR() const = 0; + public: RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board), _preamble_sf(0) { n_recv = n_sent = 0; } @@ -144,6 +185,12 @@ public: void loop() override; + // Called only from a display driver's busy-wait callback (see + // DisplayDriver::setBusyPumpFn()) while the main loop is blocked -- see + // the class comment above. Never call this from anywhere that could end + // up back inside display/UI code. + void pumpRecvDuringBlockingWait(); + uint32_t getPacketsRecv() const { return n_recv; } uint32_t getPacketsRecvErrors() const { return n_recv_errors; } uint32_t getPacketsSent() const { return n_sent; } diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index dc055841..75cd19c3 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -30,6 +30,14 @@ protected: unsigned long _marquee_next_at = 0; DisplayDriver(int w, int h) { _w = w; _h = h; } void setDimensions(int w, int h) { _w = w; _h = h; } + + // Registered via setBusyPumpFn() below; drivers whose underlying vendor + // library exposes a hook for its blocking hardware wait (e.g. GxEPD2's + // setBusyCallback()) call this from it. Default no-op, so any driver/board + // that never wires one up behaves exactly as before. + void (*_busy_pump_fn)(void*) = nullptr; + void* _busy_pump_ctx = nullptr; + void callBusyPump() { if (_busy_pump_fn) _busy_pump_fn(_busy_pump_ctx); } public: enum Color { DARK=0, LIGHT, RED, GREEN, BLUE, YELLOW, ORANGE }; // on b/w screen, colors will be !=0 synonym of light @@ -499,6 +507,14 @@ public: virtual void setFullRefreshInterval(uint8_t n) { } // e-ink: do full refresh every n partial refreshes (0=never) virtual void endFrame() = 0; + // Called from board setup to run safe background work during a blocking + // hardware wait (currently: an e-ink panel's BUSY-pin poll, which can run + // for over a second on a full refresh and would otherwise stall the whole + // main loop -- see RadioLibWrapper::pumpRecvDuringBlockingWait(), the + // motivating use). fn must not touch this display or any UI state: it can + // run reentrantly, nested inside the very call it's servicing. + void setBusyPumpFn(void (*fn)(void*), void* ctx) { _busy_pump_fn = fn; _busy_pump_ctx = ctx; } + #ifdef ENABLE_SCREENSHOT // Screenshot support — return raw framebuffer and its size in bytes. // 0=OLED (page-based, column-major), 1=e-ink (row-major, MSB-first, 1=white/0=black). diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 099668d6..3a0a86d4 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -84,6 +84,10 @@ bool GxEPDDisplay::begin() { SPI1.begin(); #endif display.init(115200, true, 2, false); + // Runs ~every 1ms while _waitWhileBusy() polls the panel's BUSY pin, i.e. + // for as long as a refresh blocks the main loop. See busyCallbackTrampoline + // and callBusyPump()/setBusyPumpFn() (DisplayDriver.h). + display.epd2.setBusyCallback(busyCallbackTrampoline, this); display.setRotation(DISPLAY_ROTATION); setTextSize(1); display.setPartialWindow(0, 0, display.width(), display.height()); diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index 3d92f4eb..f99bd612 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -24,6 +24,11 @@ #include "DisplayDriver.h" #include "MiscFixedFont.h" +// This driver calls callBusyPump() during its BUSY-pin waits; app code keys +// its setBusyPumpFn() wiring off this so it isn't compiled for displays (or +// the sim, whose radio isn't a RadioLibWrapper) that never would. +#define DISPLAY_HAS_BUSY_PUMP 1 + #ifndef DISPLAY_ROTATION #define DISPLAY_ROTATION 0 #endif @@ -68,6 +73,18 @@ class GxEPDDisplay : public DisplayDriver { uint8_t glyphXAdvance(uint32_t cp, int sc); int scale() const { return (width() >= height()) ? 2 : 1; } + // GxEPD2_EPD::setBusyCallback() wants a plain function pointer with a + // void* param, not a member function -- this trampolines back into the + // instance so callBusyPump() (DisplayDriver.h) can reach whatever board + // setup registered via setBusyPumpFn(). _waitWhileBusy() calls the callback + // *instead of* its own delay(1), so keep that delay here: without it the + // wait becomes a hard spin that starves the RTOS idle task (no CPU sleep) + // and equal-priority tasks for the whole refresh. + static void busyCallbackTrampoline(const void* param) { + ((GxEPDDisplay*)param)->callBusyPump(); + delay(1); + } + public: #if defined(EINK_DISPLAY_MODEL) GxEPDDisplay() : DisplayDriver(EINK_DISP_W, EINK_DISP_H),