From 6e9b148e81db0ec58759cee49ba09db051669455 Mon Sep 17 00:00:00 2001 From: Christos Themelis Date: Sun, 3 May 2026 19:31:27 +0300 Subject: [PATCH] public chat receive fix --- src/helpers/esp32/SenseCapHAL.h | 55 ++++++++++++++++--- src/helpers/esp32/SenseCapSX1262Wrapper.h | 27 +++++++++ src/helpers/radiolib/RadioLibWrappers.cpp | 19 ++++++- src/helpers/radiolib/RadioLibWrappers.h | 8 +++ .../sensecap_indicator-espnow/platformio.ini | 2 +- variants/sensecap_indicator-espnow/target.h | 5 +- 6 files changed, 104 insertions(+), 12 deletions(-) create mode 100644 src/helpers/esp32/SenseCapSX1262Wrapper.h diff --git a/src/helpers/esp32/SenseCapHAL.h b/src/helpers/esp32/SenseCapHAL.h index a063462a7..9a6f2f822 100644 --- a/src/helpers/esp32/SenseCapHAL.h +++ b/src/helpers/esp32/SenseCapHAL.h @@ -42,6 +42,22 @@ class SenseCapHAL : public ArduinoHal { int _sclk, _miso, _mosi; // SPI data pins SemaphoreHandle_t _mutex; // shared Wire mutex (set via setMutex() after creation) + // ── Deferred IRQ dispatch ────────────────────────────────────────────── + // The TCA9535 /INT fires for ANY input change on Port 0, not just DIO1. + // BUSY transitions, touch-INT (pin 6), and other inputs all trigger GPIO 42. + // We cannot do I2C inside an ISR, so the raw ISR just sets a flag. + // dispatchPendingIrq() (called from the main loop via recvRaw) reads Port 0 + // to verify DIO1 is actually HIGH before forwarding the event to RadioLib. + inline static volatile bool _s_pending = false; + inline static void (*_s_cb)(void) = nullptr; + uint8_t _irqBit = 0; // Port 0 bit-mask for DIO1, set in attachInterrupt() + + // NOTE: no IRAM_ATTR — class-static IRAM functions cause Xtensa literal-pool + // linker errors when defined inline in a header. Running from flash is fine + // here: the SenseCAP uses ESP32-S3 with no mid-operation flash cache disables, + // and LoRa packet timescales (tens of ms) dwarf any flash-cache miss latency. + static void _rawIsr() { _s_pending = true; } + // A pin is on the expander when its upper nibble equals IO_EXPANDER static bool isExp(uint32_t pin) { return (pin & ~0x0Fu) == (uint32_t)IO_EXPANDER; @@ -229,24 +245,49 @@ public: // Called with either: // a) raw expander pin (e.g. 0x43) — direct call path // b) IO_EXPANDER_IRQ (42) — via pinToInterrupt() path (RadioLib 7.x) - // In both cases redirect to GPIO 42 FALLING (TCA9535 /INT goes LOW when DIO1 rises). + // + // DEFERRED DISPATCH: instead of calling `cb` (= RadioLib's setFlag) directly + // from the ISR, we attach a lightweight raw ISR that only sets _s_pending. + // The real dispatch (reading Port 0 to verify DIO1 is HIGH) happens later + // in dispatchPendingIrq(), called from the main loop via recvRaw(). + // This prevents touch-INT (pin 6), BUSY, and other Port 0 input changes + // from triggering false radio-interrupt events. void attachInterrupt(uint32_t pin, void (*cb)(void), uint32_t mode) override { if (isExp(pin) || pin == (uint32_t)IO_EXPANDER_IRQ) { + _s_cb = cb; // save RadioLib's setFlag callback + _irqBit = isExp(pin) ? expBit(pin) // direct expander pin → derive bit + : (uint8_t)(1u << 3u); // pin=42 path → DIO1 is bit 3 // Clear any pending TCA9535 /INT by reading BOTH port registers. - // /INT stays LOW until the port that triggered it is read; floating - // Port 1 inputs keep /INT stuck even after reading Port 0 alone. - readReg(0x00); // Input Port 0 (radio pins) - readReg(0x01); // Input Port 1 (prevent spurious /INT from port 1) + readReg(0x00); + readReg(0x01); ::pinMode(IO_EXPANDER_IRQ, INPUT_PULLUP); int gpio42 = ::digitalRead(IO_EXPANDER_IRQ); - ::attachInterrupt(digitalPinToInterrupt(IO_EXPANDER_IRQ), cb, FALLING); - Serial.printf("[SenseCapHAL] DIO1 interrupt → GPIO %d state=%d (FALLING)\n", + ::attachInterrupt(digitalPinToInterrupt(IO_EXPANDER_IRQ), _rawIsr, FALLING); + Serial.printf("[SenseCapHAL] DIO1 interrupt → GPIO %d state=%d (FALLING, deferred)\n", IO_EXPANDER_IRQ, gpio42); } else { ArduinoHal::attachInterrupt(pin, cb, mode); } } + // ── dispatchPendingIrq ──────────────────────────────────────────────── + // Call from the main loop (NOT from ISR). Reads Port 0 to check whether + // DIO1 is actually HIGH. If so, forwards the event to RadioLib (setFlag). + // Ignores spurious triggers from BUSY, touch-INT, or any other Port 0 input. + void dispatchPendingIrq() { + if (!_s_pending) return; + _s_pending = false; + + // Reading Port 0 also acknowledges the TCA9535 /INT for ALL inputs, + // including touch INT and BUSY — preventing /INT from staying stuck LOW. + uint8_t port0 = readReg(0x00); + if (_irqBit && (port0 & _irqBit)) { + // DIO1 is HIGH → this is a real radio interrupt (RX_DONE or TX_DONE) + if (_s_cb) _s_cb(); + } + // else: spurious — BUSY transition, touch INT, etc. Do nothing. + } + void detachInterrupt(uint32_t pin) override { if (isExp(pin) || pin == (uint32_t)IO_EXPANDER_IRQ) { ::detachInterrupt(digitalPinToInterrupt(IO_EXPANDER_IRQ)); diff --git a/src/helpers/esp32/SenseCapSX1262Wrapper.h b/src/helpers/esp32/SenseCapSX1262Wrapper.h new file mode 100644 index 000000000..3e19e5cbb --- /dev/null +++ b/src/helpers/esp32/SenseCapSX1262Wrapper.h @@ -0,0 +1,27 @@ +#pragma once + +// SenseCapSX1262Wrapper — extends CustomSX1262Wrapper with a DIO1-verified +// dispatchPendingIrq() override. +// +// On SenseCAP Indicator the SX1262 DIO1, BUSY, and touch-INT all share the +// same TCA9535 /INT line (GPIO 42). The raw ISR in SenseCapHAL only sets a +// pending flag; this override reads Port 0 via I2C and calls setFlag() only +// when DIO1 is actually HIGH, silencing spurious triggers from BUSY +// transitions, touch events, or any other Port 0 input change. + +#include +#include "SenseCapHAL.h" + +class SenseCapSX1262Wrapper : public CustomSX1262Wrapper { +public: + SenseCapSX1262Wrapper(CustomSX1262& radio, mesh::MainBoard& board) + : CustomSX1262Wrapper(radio, board) {} + +protected: + void dispatchPendingIrq() override { + // Access HAL via Module (Module::hal is public in RadioLib 7.x) + auto* hal = static_cast( + static_cast(_radio)->getMod()->hal); + hal->dispatchPendingIrq(); + } +}; diff --git a/src/helpers/radiolib/RadioLibWrappers.cpp b/src/helpers/radiolib/RadioLibWrappers.cpp index 029080af1..d0a4b5f80 100644 --- a/src/helpers/radiolib/RadioLibWrappers.cpp +++ b/src/helpers/radiolib/RadioLibWrappers.cpp @@ -47,7 +47,11 @@ void RadioLibWrapper::idle() { void RadioLibWrapper::triggerNoiseFloorCalibrate(int threshold) { _threshold = threshold; - if (_num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES) { // ignore trigger if currently sampling + // Only restart sampling when threshold > 0 (interference detection enabled). + // With threshold=0 the noise floor is never used, so periodic SPI re-sampling + // is pointless — and on boards with a shared-bus IRQ (e.g. TCA9535) the BUSY + // transitions generated by those SPI calls fire spurious interrupts that break RX. + if (threshold > 0 && _num_floor_samples >= NUM_NOISE_FLOOR_SAMPLES) { _num_floor_samples = 0; _floor_sample_sum = 0; } @@ -96,6 +100,11 @@ bool RadioLibWrapper::isInRecvMode() const { } int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) { + // Allow subclasses to gate STATE_INT_READY on the physical IRQ pin. + // On boards with a shared-bus /INT (e.g. TCA9535), this reads DIO1 + // via I2C and only sets STATE_INT_READY when the radio actually asserted it. + dispatchPendingIrq(); + int len = 0; if (state & STATE_INT_READY) { len = _radio->getPacketLength(); @@ -115,7 +124,13 @@ int RadioLibWrapper::recvRaw(uint8_t* bytes, int sz) { state = STATE_IDLE; // need another startReceive() } - if (state != STATE_RX && !isReceivingPacket()) { + if (state != STATE_RX) { + // Note: we intentionally do NOT call isReceivingPacket() here. + // On boards where DIO1/BUSY share a TCA9535 /INT line, isReceivingPacket() + // does an SPI read which generates another BUSY pulse → another spurious + // interrupt → infinite pkt_len=0 loop. Calling startReceive() unconditionally + // is safe: if pkt_len was 0 the packet was not yet complete (or the interrupt + // was fully spurious), so restarting RX is correct. int err = _radio->startReceive(); if (err == RADIOLIB_ERR_NONE) { state = STATE_RX; diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 9ac1bbaeb..fd9df39da 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -17,6 +17,14 @@ protected: float packetScoreInt(float snr, int sf, int packet_len); virtual bool isReceivingPacket() =0; + // Called at the start of recvRaw() before checking STATE_INT_READY. + // Override on boards where the radio IRQ line is shared with other inputs + // (e.g. TCA9535 Port 0 also carries BUSY and touch INT). The override + // must read the physical IRQ pin and, ONLY if the radio has actually + // asserted it, call setFlag() to set STATE_INT_READY. Default: no-op + // (interrupt-driven boards set STATE_INT_READY directly in the ISR). + virtual void dispatchPendingIrq() {} + public: RadioLibWrapper(PhysicalLayer& radio, mesh::MainBoard& board) : _radio(&radio), _board(&board) { n_recv = n_sent = 0; } diff --git a/variants/sensecap_indicator-espnow/platformio.ini b/variants/sensecap_indicator-espnow/platformio.ini index 959dfc850..2190f6dbb 100644 --- a/variants/sensecap_indicator-espnow/platformio.ini +++ b/variants/sensecap_indicator-espnow/platformio.ini @@ -43,7 +43,7 @@ build_flags = -D LV_CONF_PATH=lv_conf.h -D SEEED_SENSECAP_INDICATOR -D RADIO_CLASS=CustomSX1262 - -D WRAPPER_CLASS=CustomSX1262Wrapper + -D WRAPPER_CLASS=SenseCapSX1262Wrapper -D PIN_USER_BTN=38 -D LANG_GR -D LORA_FREQ=869.525 diff --git a/variants/sensecap_indicator-espnow/target.h b/variants/sensecap_indicator-espnow/target.h index e18ca3fcb..ddf7d8288 100644 --- a/variants/sensecap_indicator-espnow/target.h +++ b/variants/sensecap_indicator-espnow/target.h @@ -10,7 +10,8 @@ #include #include #include -#include // TCA9535 IO expander HAL for RadioLib +#include // TCA9535 IO expander HAL for RadioLib +#include // DIO1-verified IRQ dispatch #include #include @@ -34,7 +35,7 @@ extern ESP32Board board; // ------------------------------------------------- // Radio (SX1262 - SenseCAP uses SX1262) // ------------------------------------------------- -extern CustomSX1262Wrapper radio_driver; +extern SenseCapSX1262Wrapper radio_driver; // -------------------------------------------------