diff --git a/.gitignore b/.gitignore index d6d97c6c4..43cee78db 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ __pycache__ *~ venv/ +.venv/ release/ .vscode/extensions.json /compile_commands.json diff --git a/src/concurrency/NotifiedWorkerThread.cpp b/src/concurrency/NotifiedWorkerThread.cpp index 0e4e31d9b..29aff32a5 100644 --- a/src/concurrency/NotifiedWorkerThread.cpp +++ b/src/concurrency/NotifiedWorkerThread.cpp @@ -76,8 +76,10 @@ bool NotifiedWorkerThread::notifyLater(uint32_t delay, uint32_t v, bool overwrit void NotifiedWorkerThread::checkNotification() { - auto n = notification; - notification = 0; // clear notification + // Atomically read and clear. (This avoids a potential race condition where an interrupt handler could set a new notification + // after checkNotification reads but before it clears, which would cause us to miss that notification until the next one comes + // in.) + auto n = notification.exchange(0); // read+clear atomically: like `n = notification; notification = 0;` but interrupt-safe if (n) { onNotify(n); } diff --git a/src/concurrency/NotifiedWorkerThread.h b/src/concurrency/NotifiedWorkerThread.h index 7a150b0b0..166b9ea65 100644 --- a/src/concurrency/NotifiedWorkerThread.h +++ b/src/concurrency/NotifiedWorkerThread.h @@ -1,6 +1,7 @@ #pragma once #include "OSThread.h" +#include namespace concurrency { @@ -13,7 +14,7 @@ class NotifiedWorkerThread : public OSThread /** * The notification that was most recently used to wake the thread. Read from runOnce() */ - uint32_t notification = 0; + std::atomic notification{0}; public: NotifiedWorkerThread(const char *name) : OSThread(name) {} diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp index 26d6f03d3..520a6de97 100644 --- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp +++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp @@ -1524,7 +1524,15 @@ void InkHUD::MenuApplet::onButtonShortPress() if (!settings->joystick.enabled) { if (!cursorShown) { cursorShown = true; + // Select the first item that isn't a header cursor = 0; + while (cursor < items.size() && items.at(cursor).isHeader) { + cursor++; + } + if (cursor >= items.size()) { + cursorShown = false; + cursor = 0; + } } else { do { cursor = (cursor + 1) % items.size(); @@ -1576,7 +1584,15 @@ void InkHUD::MenuApplet::onNavUp() if (!cursorShown) { cursorShown = true; - cursor = 0; + // Select the last item that isn't a header + cursor = items.size() - 1; + while (items.at(cursor).isHeader) { + if (cursor == 0) { + cursorShown = false; + break; + } + cursor--; + } } else { do { if (cursor == 0) @@ -1597,7 +1613,15 @@ void InkHUD::MenuApplet::onNavDown() if (!cursorShown) { cursorShown = true; + // Select the first item that isn't a header cursor = 0; + while (cursor < items.size() && items.at(cursor).isHeader) { + cursor++; + } + if (cursor >= items.size()) { + cursorShown = false; + cursor = 0; + } } else { do { cursor = (cursor + 1) % items.size(); diff --git a/src/main.cpp b/src/main.cpp index f70b3975e..b4c1f6519 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -192,8 +192,6 @@ bool kb_found = false; // global bool to record that on-screen keyboard (OSK) is present bool osk_found = false; -unsigned long last_listen = 0; - // The I2C address of the RTC Module (if found) ScanI2C::DeviceAddress rtc_found = ScanI2C::ADDRESS_NONE; // The I2C address of the Accelerometer (if found) @@ -1121,10 +1119,12 @@ void loop() #endif power->powerCommandsCheck(); - if (RadioLibInterface::instance != nullptr && !Throttle::isWithinTimespanMs(last_listen, 1000 * 60) && - !(RadioLibInterface::instance->isSending() || RadioLibInterface::instance->isActivelyReceiving())) { - RadioLibInterface::instance->startReceive(); - LOG_DEBUG("attempting AGC reset"); + if (RadioLibInterface::instance != nullptr) { + static uint32_t lastRadioMissedIrqPoll; + if (!Throttle::isWithinTimespanMs(lastRadioMissedIrqPoll, 1000)) { + lastRadioMissedIrqPoll = millis(); + RadioLibInterface::instance->pollMissedIrqs(); + } } #ifdef DEBUG_STACK diff --git a/src/main.h b/src/main.h index 619eb184e..91e27951f 100644 --- a/src/main.h +++ b/src/main.h @@ -33,7 +33,6 @@ extern ScanI2C::DeviceAddress cardkb_found; extern uint8_t kb_model; extern bool kb_found; extern bool osk_found; -extern unsigned long last_listen; extern ScanI2C::DeviceAddress rtc_found; extern ScanI2C::DeviceAddress accelerometer_found; extern ScanI2C::FoundDevice rgb_found; diff --git a/src/mesh/LR11x0Interface.cpp b/src/mesh/LR11x0Interface.cpp index 7c73b56cd..a6ac4f418 100644 --- a/src/mesh/LR11x0Interface.cpp +++ b/src/mesh/LR11x0Interface.cpp @@ -263,6 +263,7 @@ template void LR11x0Interface::startReceive() // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits enableInterrupt(isrRxLevel0); + checkRxDoneIrqFlag(); #endif } diff --git a/src/mesh/RF95Interface.cpp b/src/mesh/RF95Interface.cpp index 0c12401ca..b3aa72f7a 100644 --- a/src/mesh/RF95Interface.cpp +++ b/src/mesh/RF95Interface.cpp @@ -301,6 +301,7 @@ void RF95Interface::startReceive() // Must be done AFTER, starting receive, because startReceive clears (possibly stale) interrupt pending register bits enableInterrupt(isrRxLevel0); + checkRxDoneIrqFlag(); } bool RF95Interface::isChannelActive() diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index af6ab30c1..78e0fc5b4 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -517,12 +517,26 @@ void RadioLibInterface::handleReceiveInterrupt() void RadioLibInterface::startReceive() { - // Note the updated timestamp, to avoid unneeded AGC resets - last_listen = millis(); isReceiving = true; powerMon->setState(meshtastic_PowerMon_State_Lora_RXOn); } +void RadioLibInterface::pollMissedIrqs() +{ + // RadioLibInterface::enableInterrupt uses EDGE-TRIGGERED interrupts. Poll as a backup to catch missed edges. + if (isReceiving) { + checkRxDoneIrqFlag(); + } +} + +void RadioLibInterface::checkRxDoneIrqFlag() +{ + if (iface->checkIrq(RADIOLIB_IRQ_RX_DONE)) { + LOG_WARN("caught missed RX_DONE"); + notify(ISR_RX, true); + } +} + void RadioLibInterface::configHardwareForSend() { powerMon->setState(meshtastic_PowerMon_State_Lora_TXOn); diff --git a/src/mesh/RadioLibInterface.h b/src/mesh/RadioLibInterface.h index 833c88710..4bca99b5f 100644 --- a/src/mesh/RadioLibInterface.h +++ b/src/mesh/RadioLibInterface.h @@ -112,6 +112,11 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified */ virtual void enableInterrupt(void (*)()) = 0; + /** + * Poll as a backup to catch missed edge-triggered interrupts. + */ + void pollMissedIrqs(); + /** * Debugging counts */ @@ -264,4 +269,6 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified */ bool removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) override; + + void checkRxDoneIrqFlag(); }; diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index 9dfc46bee..5b2fb3ac3 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -6,6 +6,10 @@ #ifdef ARCH_PORTDUINO #include "PortduinoGlue.h" #endif +#if defined(USE_GC1109_PA) && defined(ARCH_ESP32) +#include +#include +#endif #include "Throttle.h" @@ -55,14 +59,33 @@ template bool SX126xInterface::init() #if defined(USE_GC1109_PA) // GC1109 FEM chip initialization // See variant.h for full pin mapping and control logic documentation + // + // On deep sleep wake, PA_POWER and PA_EN are held HIGH by RTC latch (set in + // enableLoraInterrupt). We configure GPIO registers before releasing the hold + // so the pad transitions atomically from held-HIGH to register-HIGH with no + // power glitch. On cold boot the hold_dis is a harmless no-op. // VFEM_Ctrl (LORA_PA_POWER): Power enable for GC1109 LDO (always on) pinMode(LORA_PA_POWER, OUTPUT); digitalWrite(LORA_PA_POWER, HIGH); + rtc_gpio_hold_dis((gpio_num_t)LORA_PA_POWER); + + // TLV75733P LDO has ~550us startup time (datasheet tSTR). On cold boot, wait + // for VBAT to stabilise before driving CSD/CPS, per GC1109 requirement: + // "VBAT must be prior to CSD/CPS/CTX for the power on sequence" + // On deep sleep wake the LDO was held on via RTC latch, so no delay needed. +#if defined(ARCH_ESP32) + if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_UNDEFINED) { + delayMicroseconds(1000); + } +#else + delayMicroseconds(1000); +#endif // CSD (LORA_PA_EN): Chip enable - must be HIGH to enable GC1109 for both RX and TX pinMode(LORA_PA_EN, OUTPUT); digitalWrite(LORA_PA_EN, HIGH); + rtc_gpio_hold_dis((gpio_num_t)LORA_PA_EN); // CPS (LORA_PA_TX_EN): PA mode select - HIGH enables full PA during TX, LOW for RX (don't care) // Note: TX/RX path switching (CTX) is handled by DIO2 via SX126X_DIO2_AS_RF_SWITCH @@ -171,6 +194,17 @@ template bool SX126xInterface::init() LOG_INFO("Set RX gain to power saving mode (boosted mode off); result: %d", result); } +#ifdef USE_GC1109_PA + // Undocumented SX1262 register patch recommended by Heltec/Semtech for improved RX sensitivity + // on boards with the GC1109 FEM. Sets bit 0 of register 0x8B5. + // Reference: https://github.com/meshcore-dev/MeshCore/pull/1398 + if (module.SPIsetRegValue(0x8B5, 0x01, 0, 0) == RADIOLIB_ERR_NONE) { + LOG_INFO("Applied SX1262 register 0x8B5 patch for GC1109 RX improvement"); + } else { + LOG_WARN("Failed to apply SX1262 register 0x8B5 patch for GC1109"); + } +#endif + #if 0 // Read/write a register we are not using (only used for FSK mode) to test SPI comms uint8_t crcLSB = 0; @@ -328,6 +362,7 @@ template void SX126xInterface::startReceive() // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits enableInterrupt(isrRxLevel0); + checkRxDoneIrqFlag(); #endif } diff --git a/src/mesh/SX128xInterface.cpp b/src/mesh/SX128xInterface.cpp index 9fcedfe49..0e882ef05 100644 --- a/src/mesh/SX128xInterface.cpp +++ b/src/mesh/SX128xInterface.cpp @@ -271,6 +271,7 @@ template void SX128xInterface::startReceive() // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits enableInterrupt(isrRxLevel0); + checkRxDoneIrqFlag(); #endif } diff --git a/src/sleep.cpp b/src/sleep.cpp index 7c768d573..d42b9841a 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -162,6 +162,13 @@ void initDeepSleep() if (wakeCause != ESP_SLEEP_WAKEUP_UNDEFINED) { LOG_DEBUG("Disable any holds on RTC IO pads"); for (uint8_t i = 0; i <= GPIO_NUM_MAX; i++) { +#if defined(USE_GC1109_PA) + // Skip GC1109 FEM power pins - they are held HIGH during deep sleep to keep + // the LNA active for RX wake. Released later in SX126xInterface::init() after + // GPIO registers are set HIGH first, avoiding a power glitch. + if (i == LORA_PA_POWER || i == LORA_PA_EN) + continue; +#endif if (rtc_gpio_is_valid_gpio((gpio_num_t)i)) rtc_gpio_hold_dis((gpio_num_t)i); @@ -556,8 +563,13 @@ void enableLoraInterrupt() #endif #if defined(USE_GC1109_PA) - gpio_pullup_en((gpio_num_t)LORA_PA_POWER); - gpio_pullup_en((gpio_num_t)LORA_PA_EN); + // Keep GC1109 FEM powered during deep sleep so LNA remains active for RX wake. + // Set PA_POWER and PA_EN HIGH (overrides SX126xInterface::sleep() shutdown), + // then latch with RTC hold so the state survives deep sleep. + digitalWrite(LORA_PA_POWER, HIGH); + rtc_gpio_hold_en((gpio_num_t)LORA_PA_POWER); + digitalWrite(LORA_PA_EN, HIGH); + rtc_gpio_hold_en((gpio_num_t)LORA_PA_EN); gpio_pulldown_en((gpio_num_t)LORA_PA_TX_EN); #endif diff --git a/variants/nrf52840/rak3401_1watt/variant.h b/variants/nrf52840/rak3401_1watt/variant.h index 80b09cf69..9f81a15ba 100644 --- a/variants/nrf52840/rak3401_1watt/variant.h +++ b/variants/nrf52840/rak3401_1watt/variant.h @@ -52,6 +52,12 @@ extern "C" { #define LED_STATE_ON 1 // State when LED is litted +/* + * Buttons + */ +#define PIN_BUTTON1 (9) +#define BUTTON_NEED_PULLUP + /* * Analog pins */