diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index a28335c19..0d973e2e2 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -63,6 +63,8 @@ const RegionProfile PROFILE_HAM_20KHZ = {PRESETS_TINY, 0, 0.0022f, false, true, // Ham '100kHz' profile. 62.5kHz bandwidth coerced to 100kHz via padding. const RegionProfile PROFILE_HAM_100KHZ = {PRESETS_NARROW, 0, 0.01875f, false, true, 0, 1, 1}; +Observable RadioInterface::loraRxPacketObservable; + #define RDEF(name, freq_start, freq_end, duty_cycle, power_limit, frequency_switching, wide_lora, profile_ptr, default_preset, \ override_slot) \ { \ @@ -1365,4 +1367,4 @@ size_t RadioInterface::beginSending(meshtastic_MeshPacket *p) sendingPacket = p; return p->encrypted.size + sizeof(PacketHeader); -} \ No newline at end of file +} diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index c199572c4..f34a99b3a 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -128,6 +128,9 @@ class RadioInterface virtual ~RadioInterface() {} + /// Fires once per valid received LoRa packet (arg = sender NodeNum). Used e.g. to flash LED_LORA. + static Observable loraRxPacketObservable; + /** * Coerce LoRa config fields (bandwidth/spread_factor) derived from presets. * This is used during early bootstrapping so UIs that display these fields directly remain consistent. diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index 2e671bf25..daf98696e 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -614,6 +614,10 @@ void RadioLibInterface::handleReceiveInterrupt() printPacket("Lora RX", mp); +#ifdef LED_LORA + loraRxPacketObservable.notifyObservers(mp->from); +#endif + airTime->logAirtime(RX_LOG, rxMsec); deliverToReceiver(mp); diff --git a/src/modules/StatusLEDModule.cpp b/src/modules/StatusLEDModule.cpp index f3a0e7a03..72aed21ff 100644 --- a/src/modules/StatusLEDModule.cpp +++ b/src/modules/StatusLEDModule.cpp @@ -1,6 +1,7 @@ #include "StatusLEDModule.h" #include "MeshService.h" #include "configuration.h" +#include "mesh/RadioInterface.h" #include /* @@ -17,6 +18,9 @@ StatusLEDModule::StatusLEDModule() : concurrency::OSThread("StatusLEDModule") if (inputBroker) inputObserver.observe(inputBroker); #endif +#ifdef LED_LORA + loraRxObserver.observe(&RadioInterface::loraRxPacketObservable); +#endif #ifdef NEOPIXEL_STATUS_POWER_PIN powerPixel.begin(); powerPixel.clear(); @@ -90,6 +94,18 @@ int StatusLEDModule::handleInputEvent(const InputEvent *event) return 0; } #endif +#ifdef LED_LORA +int StatusLEDModule::handleLoRaRx(uint32_t) +{ + // Briefly flash LED_LORA on each received packet. Turn it on now (we share the main thread with + // the radio's receive handler, so this is safe) and wake runOnce() at flash end to turn it off. + digitalWrite(LED_LORA, LED_STATE_ON); + LORA_LED_state = LED_STATE_ON; + LORA_LED_starttime = millis(); + setIntervalFromNow(LORA_RX_LED_FLASH_MS); + return 0; +} +#endif int32_t StatusLEDModule::runOnce() { @@ -227,6 +243,20 @@ int32_t StatusLEDModule::runOnce() digitalWrite(Battery_LED_4, chargeIndicatorLED4); #endif +#ifdef LED_LORA + // End the LoRa-RX flash once its duration has elapsed; otherwise make sure we come back + // exactly at flash end (only ever clamp my_interval down, so other LED timing is preserved). + if (LORA_LED_state == LED_STATE_ON) { + uint32_t elapsed = millis() - LORA_LED_starttime; + if (elapsed >= LORA_RX_LED_FLASH_MS) { + digitalWrite(LED_LORA, LED_STATE_OFF); + LORA_LED_state = LED_STATE_OFF; + } else if ((uint32_t)my_interval > LORA_RX_LED_FLASH_MS - elapsed) { + my_interval = LORA_RX_LED_FLASH_MS - elapsed; + } + } +#endif + return (my_interval); } diff --git a/src/modules/StatusLEDModule.h b/src/modules/StatusLEDModule.h index f20198e39..793877400 100644 --- a/src/modules/StatusLEDModule.h +++ b/src/modules/StatusLEDModule.h @@ -43,6 +43,9 @@ class StatusLEDModule : private concurrency::OSThread #if !MESHTASTIC_EXCLUDE_INPUTBROKER int handleInputEvent(const InputEvent *arg); #endif +#ifdef LED_LORA + int handleLoRaRx(uint32_t sender); +#endif void setPowerLED(bool); @@ -65,6 +68,10 @@ class StatusLEDModule : private concurrency::OSThread CallbackObserver inputObserver = CallbackObserver(this, &StatusLEDModule::handleInputEvent); #endif +#ifdef LED_LORA + CallbackObserver loraRxObserver = + CallbackObserver(this, &StatusLEDModule::handleLoRaRx); +#endif private: bool CHARGE_LED_state = LED_STATE_OFF; @@ -77,6 +84,11 @@ class StatusLEDModule : private concurrency::OSThread uint32_t lastUserbuttonTime = 0; uint32_t POWER_LED_starttime = 0; bool doing_fast_blink = false; +#ifdef LED_LORA + static constexpr uint32_t LORA_RX_LED_FLASH_MS = 100; + bool LORA_LED_state = LED_STATE_OFF; + uint32_t LORA_LED_starttime = 0; +#endif enum PowerState { discharging, charging, charged, critical }; diff --git a/variants/esp32s3/ELECROW-ThinkNode-M7/variant.cpp b/variants/esp32s3/ELECROW-ThinkNode-M7/variant.cpp index 17f767c0b..fbb9d37c5 100644 --- a/variants/esp32s3/ELECROW-ThinkNode-M7/variant.cpp +++ b/variants/esp32s3/ELECROW-ThinkNode-M7/variant.cpp @@ -5,4 +5,6 @@ void initVariant() { pinMode(LED_PAIRING, OUTPUT); digitalWrite(LED_PAIRING, !LED_STATE_ON); // Turn off the LED to start + pinMode(LED_LORA, OUTPUT); + digitalWrite(LED_LORA, !LED_STATE_ON); // Turn off the LED to start }