mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 19:26:40 +00:00
fix(radio): keep the radio serviced while an e-ink refresh blocks the loop
GxEPD2 polls the panel's BUSY pin for the whole refresh (up to ~1-2s), stalling the main loop. A TX finishing in that window left the SX126x in standby -- deaf -- until the refresh ended; packets received meanwhile could be lost (only the latest is readable, and a later CRC failure rejects a good one still pending). Hook GxEPD2's busy callback to finish TX / drain RX into a small staging queue and re-arm right away, without touching dispatch/UI. Wired only where the display driver supports it (GxEPD2: the two e-ink Solo boards); OLED and sim builds are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user