From 9d52b1fd68d8cb8e1e72bfc253cd80d37d6a6066 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 20 Oct 2026 08:59:39 -0700 Subject: [PATCH] Add saved LED controls across supported boards Add get leds and set leds on|off for Companion, repeater, room server and sensor builds. Preserve the setting across reboots and cover GPIO, RGB, Bluetooth, notification and expander indicators without changing radio amplifier controls or screen backlights. RAK Companions blink while advertising and keep the Bluetooth LED off once connected, including late timer callbacks. Adapted from meshcore-dev/MeshCore#3605 and #2694. Validated with host regressions and firmware builds for SenseCAP Solar, RAK4631, T096, Heltec V4, T-Beam, Xiao S3, Pico W and Wio E5 Mini. Co-authored-by: R0ck Co-authored-by: Nick Dunklee --- docs/cli_commands.md | 27 ++ examples/companion_radio/MyMesh.cpp | 2 + examples/companion_radio/Notifications.cpp | 1 + examples/companion_radio/main.cpp | 2 + examples/companion_radio/ui-new/UITask.cpp | 1 + examples/companion_radio/ui-orig/UITask.cpp | 2 + examples/companion_radio/ui-tiny/UITask.cpp | 1 + examples/simple_repeater/UITask.cpp | 2 +- examples/simple_repeater/main.cpp | 2 + examples/simple_room_server/main.cpp | 2 + examples/simple_sensor/main.cpp | 2 + scripts/nrf52_ble_cccd_fix.py | 45 +++ src/MeshCore.h | 3 + src/helpers/CommonCLI.cpp | 2 + src/helpers/ESP32Board.h | 15 +- src/helpers/LedPolicy.cpp | 24 ++ src/helpers/LedPolicy.h | 16 + src/helpers/LedSettings.cpp | 107 +++++++ src/helpers/LedSettings.h | 14 + src/helpers/NRF52Board.cpp | 7 + src/helpers/NRF52Board.h | 1 + src/helpers/esp32/TBeamBoard.h | 16 +- src/helpers/nrf52/BluetoothLed.h | 50 +++ src/helpers/nrf52/SerialBLEInterface.cpp | 8 +- src/helpers/stm32/STM32Board.h | 6 +- test/fixtures/notification_led_ownership.cpp | 19 ++ test/fixtures/nrf52_shutdown_leds.cpp | 1 + test/test_board_led_policy.py | 188 +++++++++++ test/test_companion_reader.py | 1 + test/test_heltec_r8_external_power.py | 2 + test/test_led_settings.py | 229 +++++++++++++ test/test_notification_led_ownership.py | 3 +- test/test_nrf52_ble_startup.py | 14 + test/test_nrf52_led_policy.py | 301 ++++++++++++++++++ test/test_status_led_policy.py | 134 ++++++++ test/test_tbeam_charge_target.py | 3 + test/test_wio_tracker_l2_gps_power.py | 2 + .../GAT56230SMeshKitBoard.h | 7 +- .../gat562_mesh_evb_pro/GAT562EVBProBoard.h | 7 +- .../GAT562MeshTrackerProBoard.h | 7 +- variants/heltec_rc32/HeltecRC32Board.cpp | 2 +- variants/heltec_t096/T096Board.cpp | 7 +- variants/heltec_t096/T096Board.h | 1 + variants/heltec_t1/T1Board.cpp | 7 +- variants/heltec_t1/T1Board.h | 1 + variants/heltec_t114/T114Board.h | 7 +- .../heltec_tower_v2/HeltecTowerV2Board.cpp | 7 +- variants/heltec_tower_v2/HeltecTowerV2Board.h | 1 + .../HeltecTrackerV2Board.cpp | 2 +- variants/heltec_v4/HeltecV4Board.cpp | 2 +- variants/heltec_v4_r8/HeltecV4R8Board.cpp | 2 +- variants/ikoka_handheld_nrf/IkokaNrf52Board.h | 7 +- variants/ikoka_nano_nrf/IkokaNanoNRFBoard.h | 7 +- variants/ikoka_stick_nrf/IkokaStickNRFBoard.h | 7 +- variants/keepteen_lt1/KeepteenLT1Board.h | 7 +- .../lilygo_t_impulse_plus/TImpulsePlusBoard.h | 7 +- variants/lilygo_tbeam_1w/TBeam1WBoard.cpp | 6 +- variants/lilygo_tbeam_1w/TBeam1WBoard.h | 1 + variants/lilygo_tdeck/TDeckBoard.h | 18 +- variants/lilygo_techo_card/TechoCardBoard.cpp | 9 + variants/lilygo_techo_card/TechoCardBoard.h | 1 + .../meshnology_w12/MeshnologyW12Board.cpp | 6 +- variants/meshnology_w12/MeshnologyW12Board.h | 1 + variants/meshtiny/MeshtinyBoard.h | 7 +- variants/meshtracker_x1/MeshTrackerX1Board.h | 2 +- .../MinewsemiME25LS01Board.h | 7 +- variants/muziworks_r1_neo/R1NeoBoard.h | 7 +- variants/rak11310/RAK11310Board.h | 6 +- variants/rak_wismesh_tag/RAKWismeshTagBoard.h | 9 +- variants/rpi_picow/PicoWBoard.h | 6 +- variants/sensecap_solar/SenseCapSolarBoard.h | 7 +- variants/t1000-e/T1000eBoard.h | 2 +- variants/thinknode_m1/ThinkNodeM1Board.h | 7 +- variants/thinknode_m3/ThinkNodeM3Board.h | 7 +- variants/thinknode_m4/ThinkNodeM4Board.h | 9 +- variants/thinknode_m5/ThinknodeM5Board.h | 6 +- variants/thinknode_m6/ThinkNodeM6Board.h | 7 +- variants/thinknode_m7/ThinkNodeM7Board.h | 6 +- variants/thinknode_m8/ThinkNodeM8Board.h | 7 +- .../waveshare_rp2040_lora/WaveshareBoard.h | 6 +- .../wio-tracker-l1-1w/WioTrackerL1Board.h | 7 +- variants/wio-tracker-l1/WioTrackerL1Board.h | 7 +- variants/wio-tracker-l2/WioTrackerL2Board.cpp | 2 +- variants/wio-tracker-l2/WioTrackerL2Board.h | 3 + variants/wio_wm1110/WioWM1110Board.cpp | 2 +- variants/wio_wm1110/WioWM1110Board.h | 8 +- variants/xiao_c3/XiaoC3Board.h | 2 +- variants/xiao_nrf52/XiaoNrf52Board.h | 9 +- variants/xiao_rp2040/XiaoRP2040Board.h | 6 +- variants/xiao_s3/platformio.ini | 1 + variants/xiao_s3/target.cpp | 3 +- 91 files changed, 1485 insertions(+), 65 deletions(-) create mode 100644 src/helpers/LedPolicy.cpp create mode 100644 src/helpers/LedPolicy.h create mode 100644 src/helpers/LedSettings.cpp create mode 100644 src/helpers/LedSettings.h create mode 100644 src/helpers/nrf52/BluetoothLed.h create mode 100644 test/test_board_led_policy.py create mode 100644 test/test_led_settings.py create mode 100644 test/test_nrf52_led_policy.py create mode 100644 test/test_status_led_policy.py diff --git a/docs/cli_commands.md b/docs/cli_commands.md index cbec4abc8..472f0d5b7 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1958,6 +1958,33 @@ the verified pin assignments, startup fixes, and saved amplifier settings. --- +#### Turn indicator LEDs on or off + +```text +get leds +set leds off +set leds on +``` + +Available in Companion, repeater, room-server, and sensor firmware. The setting +is saved across reboots and defaults to `on`. `off` suppresses software-controlled +transmit, Bluetooth, status, and notification LEDs. `on` restores their normal +behavior; it does not leave every LED lit. A build with `DISABLE_BLE_LED` keeps +the Bluetooth indicator disabled. + +Supported outputs include ordinary and inverted GPIO LEDs, addressable RGB LEDs, +and board-controlled expander LEDs on nRF52, ESP32, RP2040, and STM32 boards. +Screen backlights and manually controlled torches keep their own controls. +Power and charging LEDs wired directly to a charger cannot be switched off by +firmware. Bootloader and early startup indicators may still appear before the +saved setting is loaded. + +This extends the SenseCAP Solar LED switch from +[upstream PR #3605](https://github.com/meshcore-dev/MeshCore/pull/3605). +With LEDs enabled, RAK-family Companions blink their Bluetooth LED while +advertising and leave it off once connected, following +[upstream PR #2694](https://github.com/meshcore-dev/MeshCore/pull/2694). + #### Turn WiFi, ESP-NOW, or all 2.4 GHz services on or off ```text diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 4c8f4df2e..38f4f6695 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -10725,6 +10726,7 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, } if (mesh::power::handleRegulatorCommand(command, reply, reply_capacity)) return true; + if (mesh::leds::handleCommand(command, reply, reply_capacity)) return true; #if COMPANION_FEATURE_READER if (sender_timestamp == 0 && mesh::morseTimingActive(_radio) diff --git a/examples/companion_radio/Notifications.cpp b/examples/companion_radio/Notifications.cpp index 184221fce..cadd2353a 100644 --- a/examples/companion_radio/Notifications.cpp +++ b/examples/companion_radio/Notifications.cpp @@ -20,6 +20,7 @@ void MyMesh::pulse(mesh::notify::Output output,bool on,int8_t pin) { } else if(output==mesh::notify::Vibration) { if(_ui)_ui->notificationVibration(on); } else if(output==mesh::notify::Led) { + on = on && mesh::leds::enabled(); #if defined(PIN_STATUS_LED_R) && defined(PIN_STATUS_LED_G) && defined(PIN_STATUS_LED_B) analogWrite(PIN_STATUS_LED_R,on?255:0); analogWrite(PIN_STATUS_LED_G,on?90:0); diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index cd8a4650b..5372afbe5 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -1,4 +1,5 @@ #include +#include #include // needed for PlatformIO #include #include @@ -3634,6 +3635,7 @@ void setup() { if (!mesh::storage::internalPrimaryFilesystemReady(primary_fs_boot) || store.isVolatilePrimaryFS()) regulator_prefs_fs = nullptr; #endif + mesh::leds::beginSettings(board, regulator_prefs_fs); if (!mesh::power::beginRegulatorSettings(board, radio_driver, regulator_prefs_fs)) { MESH_DEBUG_PRINTLN("Regulator selection could not be applied"); } diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index ed1c71517..314739de8 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -2105,6 +2105,7 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, } void UITask::userLedHandler() { + if (!mesh::leds::enabled()) return; if (_notification_led_owned || !(_notification_outputs & mesh::notify::Led)) return; #ifdef PIN_STATUS_LED const uint32_t cur_time = millis(); diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index fd66182bc..75acf7f12 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -451,6 +451,7 @@ void UITask::renderCurrScreen() { #ifdef STATUS_LED_RGB static void statusLedWrite(uint8_t r, uint8_t g, uint8_t b) { + if (!mesh::leds::enabled()) r = g = b = 0; analogWrite(PIN_STATUS_LED_R, r); analogWrite(PIN_STATUS_LED_G, g); analogWrite(PIN_STATUS_LED_B, b); @@ -470,6 +471,7 @@ static void statusLedWheel(uint8_t pos) { // smooth hue sweep, 0..255 #endif void UITask::userLedHandler() { + if (!mesh::leds::enabled()) return; if (_notification_led_owned || !(_notification_outputs & mesh::notify::Led)) return; #ifdef STATUS_LED_RGB enum class FlourishState : uint8_t { Unseen, Boot, Message, Idle }; diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index 489c2652b..e7fa3012c 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -751,6 +751,7 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, } void UITask::userLedHandler() { + if (!mesh::leds::enabled()) return; if (_notification_led_owned || !(_notification_outputs & mesh::notify::Led)) return; #ifdef PIN_STATUS_LED const uint32_t cur_time = millis(); diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index bd92ee567..c1e7a0e6b 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -981,7 +981,7 @@ void UITask::loop() { if (_powering_off_at > 0) { // power off timer armed #if defined(LED_PIN) && defined(LED_STATE_ON) - digitalWrite(LED_PIN, LED_STATE_ON); // switch on the led until poweroff + if (mesh::leds::enabled()) digitalWrite(LED_PIN, LED_STATE_ON); delay(1000); digitalWrite(LED_PIN, !LED_STATE_ON); // off LED #endif diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 207a775a9..ab965be14 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -1,4 +1,5 @@ #include +#include #include // needed for PlatformIO #include #include @@ -208,6 +209,7 @@ void setup() { #if defined(NRF52_PLATFORM) if (volatile_primary_fs) regulator_prefs_fs = nullptr; #endif + mesh::leds::beginSettings(board, regulator_prefs_fs); if (!mesh::power::beginRegulatorSettings(board, radio_driver, regulator_prefs_fs)) { MESH_DEBUG_PRINTLN("Regulator selection could not be applied"); } diff --git a/examples/simple_room_server/main.cpp b/examples/simple_room_server/main.cpp index f837dca26..a60beda64 100644 --- a/examples/simple_room_server/main.cpp +++ b/examples/simple_room_server/main.cpp @@ -1,4 +1,5 @@ #include +#include #include // needed for PlatformIO #include #include @@ -142,6 +143,7 @@ void setup() { #if defined(NRF52_PLATFORM) if (volatile_primary_fs) regulator_prefs_fs = nullptr; #endif + mesh::leds::beginSettings(board, regulator_prefs_fs); if (!mesh::power::beginRegulatorSettings(board, radio_driver, regulator_prefs_fs)) { MESH_DEBUG_PRINTLN("Regulator selection could not be applied"); } diff --git a/examples/simple_sensor/main.cpp b/examples/simple_sensor/main.cpp index 56ea058b6..5d5e9c6f6 100644 --- a/examples/simple_sensor/main.cpp +++ b/examples/simple_sensor/main.cpp @@ -1,4 +1,5 @@ #include +#include #include "SensorMesh.h" #include #include @@ -167,6 +168,7 @@ void setup() { #if defined(NRF52_PLATFORM) if (volatile_primary_fs) regulator_prefs_fs = nullptr; #endif + mesh::leds::beginSettings(board, regulator_prefs_fs); if (!mesh::power::beginRegulatorSettings(board, radio_driver, regulator_prefs_fs)) { MESH_DEBUG_PRINTLN("Regulator selection could not be applied"); } diff --git a/scripts/nrf52_ble_cccd_fix.py b/scripts/nrf52_ble_cccd_fix.py index f3c219c12..2ba0ab140 100644 --- a/scripts/nrf52_ble_cccd_fix.py +++ b/scripts/nrf52_ble_cccd_fix.py @@ -155,6 +155,49 @@ def patched_trace_source(source): ''') +def patched_led_source(source): + """A queued advertising blink must not relight a disabled indicator.""" + source = source.replace("\r\n", "\n") + old = """ (void) xTimer; + digitalToggle(LED_BLUE); +""" + new = """ (void) xTimer; + // MeshCore saved LED policy also covers already queued timer callbacks. + if (LED_BLUE < 0 || (uint32_t)LED_BLUE >= PINS_COUNT) return; + if (digitalPinToPinName(LED_BLUE) == 0xff) return; +#ifdef DISABLE_BLE_LED + const bool enabled = false; +#elif defined(RAK_BOARD) && defined(BLE_PIN_CODE) + const bool enabled = mesh::leds::enabled() && !Bluefruit.connected(); +#else + const bool enabled = mesh::leds::enabled(); +#endif + if (enabled) digitalToggle(LED_BLUE); + else digitalWrite(LED_BLUE, !LED_STATE_ON); +""" + include = '#include "bluefruit.h"\n' + policy_include = '#include \n' + old_connected = " digitalWrite(LED_BLUE, on_off ? LED_STATE_ON : (1-LED_STATE_ON) );" + new_connected = """ // RAK Companions blink while advertising, then go dark when connected. +#if defined(DISABLE_BLE_LED) || (defined(RAK_BOARD) && defined(BLE_PIN_CODE)) + on_off = false; +#else + on_off = on_off && mesh::leds::enabled(); +#endif + if (LED_BLUE >= 0 && (uint32_t)LED_BLUE < PINS_COUNT + && digitalPinToPinName(LED_BLUE) != 0xff) + digitalWrite(LED_BLUE, on_off ? LED_STATE_ON : (1-LED_STATE_ON));""" + if (new in source and new_connected in source + and policy_include in source and old not in source + and old_connected not in source): + return source + if (source.count(old) != 1 or source.count(include) != 1 + or source.count(old_connected) != 1): + raise RuntimeError("nRF52 BLE LED policy: unrecognized timer callback") + return source.replace(old, new).replace(old_connected, new_connected).replace( + include, include + policy_include) + + def replace_framework_source(build_env, node): source = Path(node.srcnode().get_abspath()) if source.name not in ("BLEGatt.cpp", "BLEConnection.cpp", "bluefruit.cpp") or source.parent.name != "src": @@ -166,6 +209,8 @@ def replace_framework_source(build_env, node): patched = patched_connection_source(original) else: patched = patched_bluefruit_source(original) + patched = patched_led_source(patched) + build_env.AppendUnique(CPPPATH=[build_env.subst("$PROJECT_DIR/src")]) if "MESH_NRF52_BLE_TRACE" in str(build_env.get("CPPDEFINES", [])): patched = patched_trace_source(patched) build_env.AppendUnique(CPPPATH=[build_env.subst("$PROJECT_DIR/src")]) diff --git a/src/MeshCore.h b/src/MeshCore.h index 9b6daf6ff..ebccbed08 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -7,6 +7,7 @@ #include "helpers/BatteryChargeSupport.h" #include "helpers/RegulatorMode.h" #include "helpers/PowerSavingDiagnostics.h" +#include "helpers/LedPolicy.h" #if defined(ARDUINO) && (defined(RP2040_PLATFORM) || defined(STM32_PLATFORM)) #include #endif @@ -145,6 +146,8 @@ public: virtual const char* getManufacturerName() const = 0; virtual void onBeforeTransmit() { } virtual void onAfterTransmit() { } + // Update only indicator outputs; transmit hooks may also control RF hardware. + virtual void applyLedPolicy() { } virtual void reboot() = 0; MESH_REGULATOR_VIRTUAL uint8_t mcuRegulatorModes() const { return 0; } MESH_REGULATOR_VIRTUAL power::RegulatorMode defaultMcuRegulatorMode() const { diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 7abe0efa0..28b662f21 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -1,4 +1,5 @@ #include +#include #include #include "CarrierTools.h" #include "PacketFileLoggingConfig.h" @@ -2980,6 +2981,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re if (mesh::power::handleBatteryChargeCommand(*_board, command, reply, 160)) return; #endif if (mesh::power::handleRegulatorCommand(command, reply, 157)) return; + if (mesh::leds::handleCommand(command, reply, 157)) return; if (mesh::handleUsbLoggingWatchdogCommand(command, reply, 160)) return; if (mesh::powerdiag::handleCommand(command, reply, 157, millis())) return; if (handleManagementCommand(command, reply)) return; diff --git a/src/helpers/ESP32Board.h b/src/helpers/ESP32Board.h index 1a2e3690e..8d1bc90b6 100644 --- a/src/helpers/ESP32Board.h +++ b/src/helpers/ESP32Board.h @@ -271,9 +271,18 @@ public: uint8_t getStartupReason() const override { return startup_reason; } bool isUserGpioAvailable(uint8_t pin) const override; + void applyLedPolicy() override { + if (mesh::leds::enabled()) return; +#if defined(P_LORA_TX_LED) + digitalWrite(P_LORA_TX_LED, LOW); +#elif defined(P_LORA_TX_NEOPIXEL_LED) + neopixelWrite(P_LORA_TX_NEOPIXEL_LED, 0, 0, 0); +#endif + } + #if defined(P_LORA_TX_LED) void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off @@ -282,7 +291,9 @@ public: #define NEOPIXEL_BRIGHTNESS 64 // white brightness (max 255) void onBeforeTransmit() override { - neopixelWrite(P_LORA_TX_NEOPIXEL_LED, NEOPIXEL_BRIGHTNESS, NEOPIXEL_BRIGHTNESS, NEOPIXEL_BRIGHTNESS); // turn TX neopixel on (White) + if (mesh::leds::enabled()) { + neopixelWrite(P_LORA_TX_NEOPIXEL_LED, NEOPIXEL_BRIGHTNESS, NEOPIXEL_BRIGHTNESS, NEOPIXEL_BRIGHTNESS); // turn TX neopixel on (White) + } } void onAfterTransmit() override { neopixelWrite(P_LORA_TX_NEOPIXEL_LED, 0, 0, 0); // turn TX neopixel off diff --git a/src/helpers/LedPolicy.cpp b/src/helpers/LedPolicy.cpp new file mode 100644 index 000000000..c7156f243 --- /dev/null +++ b/src/helpers/LedPolicy.cpp @@ -0,0 +1,24 @@ +#include "LedPolicy.h" +#include +#include + +namespace mesh { namespace leds { + +void applyStatusPolicy() { + if (enabled()) return; +#if defined(PIN_STATUS_LED_R) && defined(PIN_STATUS_LED_G) && defined(PIN_STATUS_LED_B) + analogWrite(PIN_STATUS_LED_R, 0); + analogWrite(PIN_STATUS_LED_G, 0); + analogWrite(PIN_STATUS_LED_B, 0); +#elif defined(PIN_STATUS_LED) + if (PIN_STATUS_LED < 0) return; +#ifdef LED_STATE_ON + digitalWrite(PIN_STATUS_LED, !LED_STATE_ON); +#else + digitalWrite(PIN_STATUS_LED, LOW); +#endif + pinMode(PIN_STATUS_LED, OUTPUT); +#endif +} + +}} // namespace mesh::leds diff --git a/src/helpers/LedPolicy.h b/src/helpers/LedPolicy.h new file mode 100644 index 000000000..e5951e338 --- /dev/null +++ b/src/helpers/LedPolicy.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace mesh { namespace leds { + +inline std::atomic& policy() { + static std::atomic value{true}; + return value; +} + +inline bool enabled() { return policy().load(std::memory_order_relaxed); } +inline void setEnabled(bool value) { policy().store(value, std::memory_order_relaxed); } +void applyStatusPolicy(); + +}} // namespace mesh::leds diff --git a/src/helpers/LedSettings.cpp b/src/helpers/LedSettings.cpp new file mode 100644 index 000000000..f55b5919a --- /dev/null +++ b/src/helpers/LedSettings.cpp @@ -0,0 +1,107 @@ +#include "LedSettings.h" +#include +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) +#include +#else +#include +#endif +#include +#include + +namespace mesh { namespace leds { +namespace { +MainBoard* main_board = nullptr; +FILESYSTEM* settings_fs = nullptr; +const char* const path = "/leds_v1"; +const char* const backup = "/leds_v1.bak"; + +void encode(bool on, uint8_t (&data)[8]) { + const uint8_t value = on ? 1 : 0; + const uint8_t image[] = {'L', 'E', 'D', 1, value, + static_cast(value ^ 0xff), 0, 0}; + memcpy(data, image, sizeof(data)); +} + +bool readSetting(const char* name, bool& on) { + if (!settings_fs) return false; +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + File file(*settings_fs); + if (!file.open(name, FILE_O_READ)) return false; +#else + File file = settings_fs->open(name, "r"); + if (!file) return false; +#endif + uint8_t data[8], expected[8]; + const bool complete = file.size() == sizeof(data) + && file.read(data, sizeof(data)) == sizeof(data); + file.close(); + if (!complete || data[4] > 1) return false; + encode(data[4] != 0, expected); + if (memcmp(data, expected, sizeof(data)) != 0) return false; + on = data[4] != 0; + return true; +} + +bool saveSetting(bool on) { + if (!settings_fs) return false; + uint8_t data[8]; + encode(on, data); +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + AtomicFileWriter file(settings_fs, path); +#else + ContactFileTransaction file(settings_fs, path); +#endif + if (!file || file.write(data, sizeof(data)) != sizeof(data) || !file.commit()) + return false; + bool checked = !on; + return readSetting(path, checked) && checked == on; +} +} // namespace + +void beginSettings(MainBoard& board, FILESYSTEM* fs) { + main_board = &board; + settings_fs = fs; + bool on = true; + if (!readSetting(path, on) && readSetting(backup, on)) { + // Recover the rename gap without discarding the last complete preference. + if ((!fs->exists(path) || fs->remove(path)) && fs->rename(backup, path)) { + bool checked = !on; + if (!readSetting(path, checked) || checked != on) settings_fs = nullptr; + } else { + settings_fs = nullptr; + } + } + setEnabled(on); + board.applyLedPolicy(); + applyStatusPolicy(); +} + +bool handleCommand(const char* command, char* reply, size_t capacity) { + if (!command || !reply || capacity == 0) return false; + const bool get = strncmp(command, "get ", 4) == 0; + const bool set = strncmp(command, "set ", 4) == 0; + if ((!get && !set) || strncmp(command + 4, "leds", 4) != 0) return false; + const char* value = command + 8; + if (*value && *value != ' ' && *value != '\t') return false; + if (get) { + if (*value) snprintf(reply, capacity, "Error: usage get leds"); + else snprintf(reply, capacity, "> %s", enabled() ? "on" : "off"); + return true; + } + while (*value == ' ' || *value == '\t') ++value; + if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) { + snprintf(reply, capacity, "Error: usage set leds on|off"); + return true; + } + const bool on = strcmp(value, "on") == 0; + if (!main_board || !saveSetting(on)) { + snprintf(reply, capacity, "Error: LED setting save unconfirmed; reboot may differ"); + return true; + } + setEnabled(on); + main_board->applyLedPolicy(); + applyStatusPolicy(); + snprintf(reply, capacity, "OK"); + return true; +} +} } // namespace mesh::leds diff --git a/src/helpers/LedSettings.h b/src/helpers/LedSettings.h new file mode 100644 index 000000000..9665d638a --- /dev/null +++ b/src/helpers/LedSettings.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +namespace mesh { +class MainBoard; +namespace leds { + +void beginSettings(MainBoard& board, FILESYSTEM* fs); +bool handleCommand(const char* command, char* reply, size_t capacity); + +} // namespace leds +} // namespace mesh diff --git a/src/helpers/NRF52Board.cpp b/src/helpers/NRF52Board.cpp index 170c52447..aadf055c9 100644 --- a/src/helpers/NRF52Board.cpp +++ b/src/helpers/NRF52Board.cpp @@ -6,6 +6,7 @@ #include "nrf52/SoftDeviceState.h" #include "nrf52/SleepCycleState.h" #include "nrf52/ShutdownLeds.h" +#include "nrf52/BluetoothLed.h" #include #include "UserGpioPinPolicy.h" #include "PowerSavingDiagnostics.h" @@ -263,6 +264,10 @@ bool NRF52Board::applyMcuRegulator(mesh::power::RegulatorMode mode) { return true; } +void NRF52Board::applyLedPolicy() { + mesh_nrf52::applyBluetoothLedPolicy(); +} + void NRF52Board::loop() { #ifdef NRF52_POWER_MANAGEMENT if (!mesh::power::voltagePolicySupported()) return; @@ -959,10 +964,12 @@ bool NRF52Board::startOTAUpdate(const char *id, char reply[], bool force_ap) { // Even a failed begin can leave SoftDevice enabled partway through SDK // startup. Require a fresh authoritative query before the next sleep. mesh_nrf52::sleepCycleState().invalidate(); + mesh_nrf52::applyBluetoothLedPolicy(); if (!Bluefruit.begin(1, 0)) { return false; } ota_ble_started = true; + mesh_nrf52::bluetoothLedStarted(); // To be consistent OTA DFU should be added first if it exists bledfu.begin(); diff --git a/src/helpers/NRF52Board.h b/src/helpers/NRF52Board.h index 9972b2b99..41ae8562e 100644 --- a/src/helpers/NRF52Board.h +++ b/src/helpers/NRF52Board.h @@ -61,6 +61,7 @@ public: {} virtual void begin(); void loop() override; + void applyLedPolicy() override; // Feed the hardware main-loop watchdog. The first call starts it, so normal // setup (including filesystem mount and radio initialization) is not timed. // A loop blocked forever in SoftDevice-backed flash I/O will stop feeding it diff --git a/src/helpers/esp32/TBeamBoard.h b/src/helpers/esp32/TBeamBoard.h index 234be4657..b78a867b0 100644 --- a/src/helpers/esp32/TBeamBoard.h +++ b/src/helpers/esp32/TBeamBoard.h @@ -139,9 +139,23 @@ public: return charge_target_restore_failed; } + void applyLedPolicy() override { +#ifndef TBEAM_SUPREME_SX1262 + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); +#else + ESP32Board::applyLedPolicy(); +#endif +#if !defined(PORTABLE_MQTT_OBSERVER) || defined(TBEAM_SUPREME_SX1262) + if (charge_pmu_ready && PMU) { + PMU->setChargingLedMode(mesh::leds::enabled() + ? XPOWERS_CHG_LED_CTRL_CHG : XPOWERS_CHG_LED_OFF); + } +#endif + } + #ifndef TBEAM_SUPREME_SX1262 void onBeforeTransmit() override{ - digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on - invert pin for SX1276 + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on - invert pin for SX1276 } void onAfterTransmit() override{ digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED off - invert pin for SX1276 diff --git a/src/helpers/nrf52/BluetoothLed.h b/src/helpers/nrf52/BluetoothLed.h new file mode 100644 index 000000000..6d4e4b13c --- /dev/null +++ b/src/helpers/nrf52/BluetoothLed.h @@ -0,0 +1,50 @@ +#pragma once + +#include "../LedPolicy.h" +#include "ShutdownLeds.h" +#include + +namespace mesh_nrf52 { + +inline bool& bluetoothLedReady() { + static bool ready = false; + return ready; +} + +inline void applyBluetoothLedPolicy() { +#ifdef LED_BLUE + if (LED_BLUE < 0 || static_cast(LED_BLUE) >= PINS_COUNT + || digitalPinToPinName(LED_BLUE) == 0xff) { + Bluefruit.autoConnLed(false); + return; + } +#else + Bluefruit.autoConnLed(false); + return; +#endif +#ifdef DISABLE_BLE_LED + const bool enable = false; +#else + const bool enable = mesh::leds::enabled(); +#endif + Bluefruit.autoConnLed(enable); + if (!enable) { +#ifdef LED_BLUE + turnOffShutdownLed(LED_BLUE); +#endif + } else if (bluetoothLedReady()) { +#if defined(RAK_BOARD) && defined(BLE_PIN_CODE) + Bluefruit._setConnLed(false); +#else + Bluefruit._setConnLed(Bluefruit.connected() != 0); +#endif + if (Bluefruit.Advertising.isRunning()) Bluefruit._startConnLed(); + } +} + +inline void bluetoothLedStarted() { + bluetoothLedReady() = true; + applyBluetoothLedPolicy(); +} + +} // namespace mesh_nrf52 diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index b9d20c303..da6d95f0d 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -4,6 +4,7 @@ #include "SoftDeviceState.h" #include "SleepCycleState.h" #include "BleDebugTrace.h" +#include "BluetoothLed.h" #include #include #include "ble_gap.h" @@ -836,11 +837,7 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name, char charpin[20]; snprintf(charpin, sizeof(charpin), "%lu", (unsigned long)pin_code); -#ifdef DISABLE_BLE_LED - // Opt out of Bluefruit's automatic LED control without changing the normal - // board/library default or the application's own LED handling. - Bluefruit.autoConnLed(false); -#endif + mesh_nrf52::applyBluetoothLedPolicy(); // Configure individual connection values rather than the all-or-nothing // Bluefruit preset. Defaults are identical to BANDWIDTH_MAX; constrained // board profiles can lower event/buffer reservations without shrinking the @@ -875,6 +872,7 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name, return false; } + mesh_nrf52::bluetoothLedStarted(); if (clear_bonds) Bluefruit.Periph.clearBonds(); if (custom_address != nullptr) { diff --git a/src/helpers/stm32/STM32Board.h b/src/helpers/stm32/STM32Board.h index f6dfe2e80..01cad1d13 100644 --- a/src/helpers/stm32/STM32Board.h +++ b/src/helpers/stm32/STM32Board.h @@ -36,8 +36,12 @@ public: } #if defined(P_LORA_TX_LED) + void applyLedPolicy() override { + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); + } + void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED off diff --git a/test/fixtures/notification_led_ownership.cpp b/test/fixtures/notification_led_ownership.cpp index defa3fb76..2fba358f4 100644 --- a/test/fixtures/notification_led_ownership.cpp +++ b/test/fixtures/notification_led_ownership.cpp @@ -208,6 +208,25 @@ int main() { checkGuard(node, false); }); + run("board-led-policy-overrides-alerts-and-heartbeat", [] { + MyMesh node; + mesh::leds::setEnabled(false); + command(node, "set notify.led all 50,50"); + command(node, "notify.test all", "OK - alert test started"); + expectLed(false); + checkGuard(node, false); + seedUiRgb(); + expectLed(false); + node._notifications.loop(1050, false); + node._notifications.loop(1100, false); + mesh::leds::setEnabled(true); + node.syncNotifications(); + checkGuard(node, true); + node.pulse(mesh::notify::Led, true, -1); + expectLed(true); + node.pulse(mesh::notify::Led, false, -1); + }); + run("disabled-feature-restores-ui-policy", [] { MyMesh node; startAlert(node); diff --git a/test/fixtures/nrf52_shutdown_leds.cpp b/test/fixtures/nrf52_shutdown_leds.cpp index 80efe350d..de867cbdf 100644 --- a/test/fixtures/nrf52_shutdown_leds.cpp +++ b/test/fixtures/nrf52_shutdown_leds.cpp @@ -31,6 +31,7 @@ #define pdMS_TO_TICKS(ms) (ms) #include +#include std::vector events; int levels[PINS_COUNT]; diff --git a/test/test_board_led_policy.py b/test/test_board_led_policy.py new file mode 100644 index 000000000..3218418bd --- /dev/null +++ b/test/test_board_led_policy.py @@ -0,0 +1,188 @@ +"""Exercise production board LED callbacks without touching hardware.""" +from pathlib import Path +import os +import re +import shutil +import subprocess +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[1] + + +def body(path, name, index=0): + source = (ROOT / path).read_text() + matches = list(re.finditer( + rf"\bvoid\s+(?:\w+::)?{name}\([^)]*\)(?:\s+override)?\s*\{{", source)) + start = matches[index].end() + end, depth = start, 1 + while depth: + depth += (source[end] == "{") - (source[end] == "}") + end += 1 + return source[start:end - 1] + + +STUBS = r""" +#include +#include +#define HIGH 1 +#define LOW 0 +#define P_LORA_TX_LED 1 +#define LED_PIN 1 +#define LED_BUILTIN 1 +#define LED_RED 1 +#define EXP_PIN_LED 1 +#define EXP_PIN_USER_LED 1 +#define LED_STATE_ON LOW +int levels[8], tx, rx, fan_calls, fan_state; +void digitalWrite(int pin, int value) { levels[pin] = value; } +struct Expander { void digitalWrite(int p, int v) { ::digitalWrite(p, v); } } expander; +void expWritePin(int p, int v) { digitalWrite(p, v); } +struct NRF52Board { static void applyLedPolicy() {} }; +struct Fem { + void setTxModeEnable() { ++tx; } + void setRxModeEnable() { ++rx; } +} loRaFEMControl; +void reset() { + for (auto &v : levels) v = -1; + tx = rx = fan_calls = fan_state = 0; + mesh::leds::setEnabled(false); +} +""" + + +class BoardLedPolicyTests(unittest.TestCase): + def run_cpp(self, source): + compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++") + if not compiler: + self.skipTest("C++ compiler unavailable") + with tempfile.TemporaryDirectory() as directory: + directory = Path(directory) + cpp = directory / "led_policy.cpp" + exe = directory / ("led_policy.exe" if os.name == "nt" else "led_policy") + cpp.write_text(source) + result = subprocess.run( + [compiler, "-std=c++17", "-I", str(ROOT / "src"), str(cpp), "-o", str(exe)], + capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + result = subprocess.run([str(exe)], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_gpio_polarity_and_fem_callbacks(self): + # Exercise active-low, active-high, LED_STATE_ON and expander boards. + cases = [ + ("variants/lilygo_tdeck/TDeckBoard.h", 0, False, None), + ("variants/thinknode_m7/ThinkNodeM7Board.h", 0, False, None), + ("variants/thinknode_m5/ThinknodeM5Board.h", 1, False, None), + ("variants/xiao_nrf52/XiaoNrf52Board.h", 0, False, None), + ("variants/rak11310/RAK11310Board.h", 1, False, None), + ("variants/xiao_rp2040/XiaoRP2040Board.h", 1, False, None), + ("variants/rpi_picow/PicoWBoard.h", 1, False, None), + ("variants/sensecap_solar/SenseCapSolarBoard.h", 1, False, None), + ("variants/heltec_t096/T096Board.cpp", 1, True, None), + ("variants/heltec_tower_v2/HeltecTowerV2Board.cpp", 0, True, None), + ("variants/heltec_v4/HeltecV4Board.cpp", 1, True, "src/helpers/ESP32Board.h"), + ("variants/heltec_v4_r8/HeltecV4R8Board.cpp", 1, True, "src/helpers/ESP32Board.h"), + ("src/helpers/stm32/STM32Board.h", 0, False, None), + ] + source = STUBS + for index, (path, on, fem, policy_path) in enumerate(cases): + source += f""" +namespace test{index} {{ +void before() {{ {body(path, "onBeforeTransmit")} }} +void after() {{ {body(path, "onAfterTransmit")} }} +void apply() {{ {body(policy_path or path, "applyLedPolicy")} }} +void run() {{ + reset(); + levels[1] = {on}; + apply(); + assert(levels[1] == {1 - on}); + assert(tx == 0 && rx == 0); + before(); + assert(levels[1] == {1 - on}); + assert(tx == {int(fem)} && rx == 0); + after(); + assert(levels[1] == {1 - on}); + assert(rx == {int(fem)}); + mesh::leds::setEnabled(true); + before(); + assert(levels[1] == {on}); + after(); + assert(levels[1] == {1 - on}); +}} +}} +""" + source += "int main() {" + "".join(f"test{i}::run();" for i in range(len(cases))) + "}" + self.run_cpp(source) + + def test_wio_expander_preserves_gnss_wake_level(self): + path = "variants/wio-tracker-l2/WioTrackerL2Board.h" + self.run_cpp(STUBS + f""" +bool expander_ok = true; +void setLed(bool on) {{ {body("variants/wio-tracker-l2/WioTrackerL2Board.cpp", "setLed")} }} +void before() {{ {body(path, "onBeforeTransmit")} }} +void after() {{ {body(path, "onAfterTransmit")} }} +void apply() {{ {body(path, "applyLedPolicy")} }} +int main() {{ + reset(); + apply(); assert(levels[1] == HIGH); + before(); assert(levels[1] == HIGH); + after(); assert(levels[1] == HIGH); + mesh::leds::setEnabled(true); + before(); assert(levels[1] == LOW); + after(); assert(levels[1] == HIGH); + expander_ok = false; levels[1] = -1; + apply(); before(); after(); assert(levels[1] == -1); +}} +""") + + def test_tbeam_fan_runs_with_leds_disabled(self): + path = "variants/lilygo_tbeam_1w/TBeam1WBoard.cpp" + self.run_cpp(STUBS + r""" +constexpr int FAN_AUTO = 2, FAN_TX_COOLDOWN_MS = 1000; +int _fan_mux = 0, _mode = FAN_AUTO; +bool _tx_active = false, _stopped = false, _tx_cooldown_active = false; +unsigned _tx_until_ms = 0; +void portENTER_CRITICAL(int*) {} +void portEXIT_CRITICAL(int*) {} +unsigned millis() { return 23; } +void setFanOutputLocked(bool on) { ++fan_calls; fan_state = on; } +""" + f""" +void before() {{ {body(path, "onBeforeTransmit")} }} +void after() {{ {body(path, "onAfterTransmit")} }} +void apply() {{ {body(path, "applyLedPolicy")} }} +int main() {{ + reset(); apply(); + before(); + assert(levels[1] == LOW && _tx_active && fan_calls == 1 && fan_state); + apply(); assert(_tx_active && fan_calls == 1 && !_tx_cooldown_active); + after(); + assert(!_tx_active && _tx_cooldown_active && _tx_until_ms == 1023); +}} +""") + + def test_rgb_tx_does_not_change_torch(self): + path = "variants/lilygo_techo_card/TechoCardBoard.cpp" + self.run_cpp(STUBS + r""" +struct Pixel { + int value = 0, shows = 0; + void setPixelColor(int, int r, int g, int b) { value = r + g + b; } + void show() { ++shows; } +} Led_A, Led_C; +""" + f""" +void before() {{ {body(path, "onBeforeTransmit")} }} +void after() {{ {body(path, "onAfterTransmit")} }} +void apply() {{ {body(path, "applyLedPolicy")} }} +int main() {{ + reset(); Led_A.value = 60; Led_C.value = 765; + apply(); before(); + assert(Led_A.value == 0 && Led_C.value == 765 && Led_C.shows == 0); + mesh::leds::setEnabled(true); before(); assert(Led_A.value == 60); + after(); assert(Led_A.value == 0 && Led_C.value == 765); +}} +""") + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_companion_reader.py b/test/test_companion_reader.py index 7c8b076ac..458d48786 100644 --- a/test/test_companion_reader.py +++ b/test/test_companion_reader.py @@ -272,6 +272,7 @@ struct Radio { bool active = false; }; namespace mesh { bool morseTimingActive(const Radio* radio) { return radio && radio->active; } namespace power { bool handleRegulatorCommand(const char*,char*,size_t) { return false; } } +namespace leds { bool handleCommand(const char*,char*,size_t) { return false; } } } @PREDICATE@ struct MyMesh { diff --git a/test/test_heltec_r8_external_power.py b/test/test_heltec_r8_external_power.py index 640c965dd..c130083fc 100644 --- a/test/test_heltec_r8_external_power.py +++ b/test/test_heltec_r8_external_power.py @@ -30,12 +30,14 @@ inline uint32_t analogReadMilliVolts(int) { return 830; } BASE_BOARD = r''' #pragma once #include +#include class ESP32Board { protected: uint8_t startup_reason = 0; public: void begin() {} virtual ~ESP32Board() = default; + virtual void applyLedPolicy() {} virtual void onBeforeTransmit() {} virtual void onAfterTransmit() {} virtual void powerOff() {} diff --git a/test/test_led_settings.py b/test/test_led_settings.py new file mode 100644 index 000000000..5c754fedd --- /dev/null +++ b/test/test_led_settings.py @@ -0,0 +1,229 @@ +"""Exercise persisted LED commands on every filesystem backend.""" +from pathlib import Path +import os +import shutil +import subprocess +import tempfile +import unittest + +from test_regulator_settings import FILESYSTEM +from test_serial_wifi_sessions import SANITIZER_FLAGS + +ROOT = Path(__file__).resolve().parents[1] + +BOARD = r''' +#pragma once +#include +namespace mesh { +namespace leds { +inline bool& state() { static bool value = true; return value; } +inline bool enabled() { return state(); } +inline void setEnabled(bool on) { state() = on; } +inline void applyStatusPolicy() {} +} +struct MainBoard { + unsigned applies = 0; + bool last_applied = true; + void applyLedPolicy() { ++applies; last_applied = leds::enabled(); } +}; +} +''' + +HARNESS = r''' +#include +#include +#include +#include +#include +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) +#define ATOMIC_FILE_WRITER_IMPLEMENTATION +#include +#undef ATOMIC_FILE_WRITER_IMPLEMENTATION +#endif +#include + +using namespace mesh::leds; +static const char* const target = "/leds_v1"; +static const char* const rollback = "/leds_v1.bak"; + +std::string command(const char* text) { + char reply[160] = {}; + assert(handleCommand(text, reply, sizeof(reply))); + return reply; +} +void defaults() { + mesh::MainBoard board; + MockFS fs; + setEnabled(false); + beginSettings(board, &fs); + assert(enabled() && board.last_applied && board.applies == 1); + assert(command("get leds") == "> on"); + assert(fs.writes == 0); + assert(command("set leds off") == "OK"); + assert(!enabled() && !board.last_applied && board.applies == 2); + assert(command("get leds") == "> off"); + assert(fs.files[target].size() == 8); + assert(!fs.exists("/leds_v1.tmp") && !fs.exists(rollback)); + setEnabled(true); + beginSettings(board, &fs); + assert(!enabled() && !board.last_applied); + assert(command("set leds on") == "OK"); + beginSettings(board, &fs); + assert(enabled() && board.last_applied); + beginSettings(board, nullptr); + const unsigned applies = board.applies; + assert(command("set leds off").find("save unconfirmed") != std::string::npos); + assert(enabled() && board.applies == applies); +} +void parsing() { + mesh::MainBoard board; + MockFS fs; + beginSettings(board, &fs); + const char* unrelated[] = {"", "g", "get", "get ", "get led", "get ledsX", + "set ledsX off", "set led off", "get\tleds"}; + for (const char* text : unrelated) { + char reply[80] = "untouched"; + assert(!handleCommand(text, reply, sizeof(reply))); + assert(strcmp(reply, "untouched") == 0); + } + const char* invalid[] = {"set leds", "set leds ", "set leds 0", + "set leds off extra", "set leds on ", "set leds ON", "get leds off", "get leds "}; + for (const char* text : invalid) + assert(command(text).find("Error: usage") == 0); + assert(fs.writes == 0 && board.applies == 1); + assert(command("set leds\t off") == "OK" && !enabled()); + char bounded[3] = {'x', 'y', 'z'}; + assert(handleCommand("get leds", bounded, 1)); + assert(bounded[0] == 0 && bounded[1] == 'y' && bounded[2] == 'z'); + assert(!handleCommand(nullptr, bounded, sizeof(bounded))); + assert(!handleCommand("get leds", nullptr, sizeof(bounded))); + assert(!handleCommand("get leds", bounded, 0)); +} +void failures() { + for (unsigned failure = 0; failure < 5; ++failure) { + mesh::MainBoard board; + MockFS fs; + beginSettings(board, &fs); + assert(command("set leds on") == "OK"); + const auto original = fs.files[target]; + const unsigned applies = board.applies; + if (failure == 0) fs.fail_open = true; + if (failure == 1) fs.fail_write = true; + if (failure == 2) fs.corrupt_write = true; + if (failure == 3) fs.fail_rename_from = "/leds_v1.tmp"; + if (failure == 4) fs.fail_read_path = target; + assert(command("set leds off").find("save unconfirmed") != std::string::npos); + assert(enabled() && board.applies == applies); + if (failure != 4) assert(fs.files[target] == original); + fs.fail_open = fs.fail_write = fs.corrupt_write = false; + fs.fail_rename_from.clear(); + fs.fail_read_path.clear(); + beginSettings(board, &fs); + assert(enabled() == (failure != 4)); + } +} +void recovery() { + mesh::MainBoard board; + MockFS fs; + beginSettings(board, &fs); + assert(command("set leds off") == "OK"); + const auto saved = fs.files[target]; + fs.files[rollback] = saved; + fs.files.erase(target); + fs.files["/leds_v1.tmp"] = {'x'}; + beginSettings(board, &fs); + assert(!enabled() && fs.files[target] == saved && !fs.exists(rollback)); + assert(command("set leds on") == "OK"); + assert(!fs.exists("/leds_v1.tmp")); + fs.files[rollback] = saved; + fs.files[target] = {'x'}; + beginSettings(board, &fs); + assert(!enabled() && fs.files[target] == saved); + fs.files[rollback] = saved; + fs.files.erase(target); + fs.fail_rename_from = rollback; + beginSettings(board, &fs); + assert(!enabled() && fs.files[rollback] == saved); + const unsigned applies = board.applies; + assert(command("set leds on").find("save unconfirmed") != std::string::npos); + assert(!enabled() && board.applies == applies && fs.files[rollback] == saved); +} +void corrupt() { + mesh::MainBoard board; + MockFS fs; + beginSettings(board, &fs); + assert(command("set leds off") == "OK"); + const auto saved = fs.files[target]; + for (unsigned i = 0; i < saved.size(); ++i) { + fs.files[target] = saved; + fs.files[target][i] ^= 2; + beginSettings(board, &fs); + assert(enabled()); + } + for (unsigned length = 0; length < saved.size(); ++length) { + fs.files[target] = {saved.begin(), saved.begin() + length}; + beginSettings(board, &fs); + assert(enabled()); + } + fs.files[target] = saved; + fs.files[target].push_back(0); + beginSettings(board, &fs); + assert(enabled()); +} +int main() { + defaults(); + parsing(); + failures(); + recovery(); + corrupt(); +} +''' + + +class LedSettingsTests(unittest.TestCase): + def test_commands_persistence_failures_and_recovery_across_platforms(self): + compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++") + if not compiler: + self.skipTest("native C++ compiler unavailable") + with tempfile.TemporaryDirectory(prefix="meshcore-led-settings-") as directory: + folder = Path(directory) + helpers = folder / "helpers" + helpers.mkdir() + filesystem = FILESYSTEM.replace( + " void flush() {}", " void flush() {}\n bool setBufferSize(size_t) { return true; }" + ).replace( + "#if !defined(NRF52_PLATFORM)", + "#if !defined(NRF52_PLATFORM) && !defined(STM32_PLATFORM)", + ) + (helpers / "IdentityStore.h").write_text(filesystem) + (helpers / "ContactFileTransaction.h").write_text( + (ROOT / "src/helpers/ContactFileTransaction.h").read_text() + ) + (folder / "MeshCore.h").write_text(BOARD) + source = folder / "test.cpp" + source.write_text(HARNESS) + for platform, flags in { + "esp32": ["ESP32=1", "ESP32_PLATFORM=1"], + "nrf52": ["NRF52_PLATFORM=1"], + "stm32": ["STM32_PLATFORM=1"], + "rp2040": ["RP2040_PLATFORM=1"], + }.items(): + with self.subTest(platform=platform): + binary = folder / platform + built = subprocess.run( + [compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror", + *SANITIZER_FLAGS, *["-D" + flag for flag in flags], + "-I", str(folder), "-I", str(ROOT / "src"), + "-I", str(ROOT / "src/helpers"), + str(source), "-o", str(binary)], + capture_output=True, text=True, timeout=60, + ) + self.assertEqual(built.returncode, 0, built.stdout + built.stderr) + result = subprocess.run( + [str(binary)], capture_output=True, text=True, timeout=20 + ) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_notification_led_ownership.py b/test/test_notification_led_ownership.py index 9952c0484..f6595fcad 100644 --- a/test/test_notification_led_ownership.py +++ b/test/test_notification_led_ownership.py @@ -24,6 +24,7 @@ RGB = "defined(PIN_STATUS_LED_R) && defined(PIN_STATUS_LED_G) && defined(PIN_STA HARNESS = r''' #pragma once #include +#include #include #include inline unsigned levels[64] = {}, analog_writes[64] = {}, digital_writes[64] = {}, mode_writes[64] = {}; @@ -133,7 +134,7 @@ class NotificationLedOwnershipTest(unittest.TestCase): checked = subprocess.run([str(binary)], capture_output=True, text=True, timeout=20) print(f"\n[{name}]\n" + checked.stdout + checked.stderr, flush=True) self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr) - expected_scenarios = 10 if expected_rgb or expected_mono else 4 + expected_scenarios = 11 if expected_rgb or expected_mono else 4 self.assertEqual(checked.stdout.count("PASS:"), expected_scenarios, checked.stdout) self.assertRegex(checked.stdout, rf"{expected_scenarios} scenarios, [0-9]+ checks, 0 failures") diff --git a/test/test_nrf52_ble_startup.py b/test/test_nrf52_ble_startup.py index 5c5875f9d..51f17bc4e 100644 --- a/test/test_nrf52_ble_startup.py +++ b/test/test_nrf52_ble_startup.py @@ -13,6 +13,13 @@ ROOT = Path(__file__).resolve().parents[1] RTOS = r''' #pragma once #include +#define PINS_COUNT 64 +#define LED_STATE_ON 1 +#define LED_BLUE 12 +#define OUTPUT 1 +inline int digitalPinToPinName(int pin) { return pin; } +inline void digitalWrite(int, int) {} +inline void pinMode(int, int) {} using BaseType_t = int; using UBaseType_t = unsigned; using TaskHandle_t = void*; @@ -89,6 +96,7 @@ extern "C" BaseType_t __real_xTaskCreate(TaskFunction_t, const char* name, return pdPASS; } struct Settings { + bool isRunning() const { return false; } template void clearBonds(T...) {} template void setMITM(T...) {} template void setPIN(T...) {} @@ -132,8 +140,13 @@ struct BluefruitStub { } void setTxPower(int) {} void setName(const char*) {} + void autoConnLed(bool) {} + void _setConnLed(bool) {} + void _startConnLed() {} + unsigned connected() const { return 0; } template void setEventCallback(T) {} } Bluefruit; +#include struct Service { int id; int begin() { return fail_service == id ? 1 : ERROR_NONE; } @@ -251,6 +264,7 @@ class Nrf52BleStartupTest(unittest.TestCase): with tempfile.TemporaryDirectory(prefix="meshcore-ble-start-") as temp: temp = Path(temp) (temp / "Arduino.h").write_text(RTOS) + (temp / "bluefruit.h").write_text("#pragma once\n") for name in ("FreeRTOS.h", "task.h"): (temp / name).write_text('#include "Arduino.h"\n') cpp = temp / "startup.cpp" diff --git a/test/test_nrf52_led_policy.py b/test/test_nrf52_led_policy.py new file mode 100644 index 000000000..f9dcd360b --- /dev/null +++ b/test/test_nrf52_led_policy.py @@ -0,0 +1,301 @@ +#!/usr/bin/env python3 +"""Run the nRF52 LED policy and patched Bluefruit timer callback on the host.""" + +from importlib.util import module_from_spec, spec_from_file_location +from pathlib import Path +import os +import shutil +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +SPEC = spec_from_file_location("nrf52_led_patch", ROOT / "scripts/nrf52_ble_cccd_fix.py") +PATCH = module_from_spec(SPEC) +SPEC.loader.exec_module(PATCH) + +ARDUINO = r''' +#pragma once +#include +#include +#define PINS_COUNT 64 +#define OUTPUT 1 +extern int led_level, writes; +inline int digitalPinToPinName(int pin) { return pin == 62 ? 0xff : pin; } +inline bool validPin(int pin) { + return pin >= 0 && pin < PINS_COUNT && digitalPinToPinName(pin) != 0xff; +} +inline void digitalWrite(int pin, int value) { + assert(validPin(pin)); + led_level = value; + ++writes; +} +inline void digitalToggle(int pin) { digitalWrite(pin, !led_level); } +inline void pinMode(int pin, int) { assert(validPin(pin)); } +''' + +BLUEFRUIT = r''' +#pragma once +#include +using TimerHandle_t = void*; +struct Advertiser { + bool running = false; + bool isRunning() const { return running; } +}; +struct AdafruitBluefruit { + bool ready = false, automatic = true, linked = false; + bool _led_conn = true; + int timer_starts = 0, timer_stops = 0, connection_queries = 0; + Advertiser Advertising; + void autoConnLed(bool enabled) { + automatic = _led_conn = enabled; + if (!enabled && ready) ++timer_stops; + } + unsigned connected() { + assert(ready); + ++connection_queries; + return linked; + } + void _startConnLed() { + assert(ready); + if (automatic) ++timer_starts; + } +#ifdef EXTERNAL_LED_METHOD + void _setConnLed(bool on); +#else + void _setConnLed(bool on) { + assert(ready); +#ifdef LED_BLUE + if (automatic) digitalWrite(LED_BLUE, on ? LED_STATE_ON : !LED_STATE_ON); +#else + (void)on; +#endif + } +#endif +}; +using MockBluefruit = AdafruitBluefruit; +extern MockBluefruit Bluefruit; +''' + +# Exact callback from the pinned Adafruit nRF52 Bluefruit52Lib/src/bluefruit.cpp. +# The build middleware patches this body in its private framework copy. +CALLBACK = r'''#include "bluefruit.h" +static void bluefruit_blinky_cb( TimerHandle_t xTimer ) +{ + (void) xTimer; + digitalToggle(LED_BLUE); +} + +void AdafruitBluefruit::_setConnLed (bool on_off) +{ + if (_led_conn) + { + digitalWrite(LED_BLUE, on_off ? LED_STATE_ON : (1-LED_STATE_ON) ); + } +} +''' + +HARNESS = r''' +#include +MockBluefruit Bluefruit; +int led_level = LED_STATE_ON, writes = 0; +int main() { +#if defined(DISABLE_BLE_LED) || !defined(LED_BLUE) + const bool supported = false; +#else + const bool supported = validPin(LED_BLUE); +#endif + // With the default enabled preference, the helper may configure the owner + // before begin(), but must not query a connection or start a missing timer. + mesh_nrf52::applyBluetoothLedPolicy(); + assert(Bluefruit.automatic == supported); + assert(Bluefruit.timer_starts == 0 && Bluefruit.timer_stops == 0); + assert(Bluefruit.connection_queries == 0); + + // Saved preferences are applied before begin() has created an LED timer. + mesh::leds::setEnabled(false); + mesh_nrf52::applyBluetoothLedPolicy(); + assert(!Bluefruit.automatic); + assert(Bluefruit.timer_starts == 0 && Bluefruit.timer_stops == 0); + assert(Bluefruit.connection_queries == 0); +#if defined(LED_BLUE) + if (validPin(LED_BLUE)) assert(led_level == !LED_STATE_ON); +#endif + + // A later stack start must preserve the off preference. + Bluefruit.ready = true; + Bluefruit.Advertising.running = true; + mesh_nrf52::bluetoothLedStarted(); + assert(!Bluefruit.automatic && Bluefruit.timer_starts == 0); + + // Enabling again restores the current state without restarting Bluetooth. + mesh::leds::setEnabled(true); + mesh_nrf52::applyBluetoothLedPolicy(); + assert(Bluefruit.automatic == supported); + if (supported) { + assert(Bluefruit.timer_starts == 1); + assert(led_level == !LED_STATE_ON); + } else { + assert(Bluefruit.timer_starts == 0); + } + + Bluefruit.Advertising.running = false; + Bluefruit.linked = true; + mesh_nrf52::applyBluetoothLedPolicy(); +#if defined(RAK_BOARD) && defined(BLE_PIN_CODE) + if (supported) assert(led_level == !LED_STATE_ON); +#else + if (supported) assert(led_level == LED_STATE_ON); +#endif + mesh::leds::setEnabled(false); + mesh_nrf52::applyBluetoothLedPolicy(); + assert(!Bluefruit.automatic); +#ifdef LED_BLUE + if (validPin(LED_BLUE)) assert(led_level == !LED_STATE_ON); + else assert(writes == 0); +#else + assert(writes == 0); +#endif +} +''' + +TIMER_HARNESS = r''' +MockBluefruit Bluefruit; +int led_level = LED_STATE_ON, writes = 0; +int main() { + Bluefruit.ready = true; + // A previously queued callback can still arrive after autoConnLed(false). + mesh::leds::setEnabled(false); + for (int i = 0; i < 4; ++i) { + bluefruit_blinky_cb(nullptr); + if (validPin(LED_BLUE)) assert(led_level == !LED_STATE_ON); + } + Bluefruit._setConnLed(true); + if (validPin(LED_BLUE)) assert(led_level == !LED_STATE_ON); + mesh::leds::setEnabled(true); + bluefruit_blinky_cb(nullptr); +#ifdef DISABLE_BLE_LED + if (validPin(LED_BLUE)) assert(led_level == !LED_STATE_ON); +#else + if (validPin(LED_BLUE)) assert(led_level == LED_STATE_ON); +#endif + // PR #2694: advertising still blinks on a RAK Companion, but connecting + // leaves the LED dark. Other boards retain the steady connected indicator. + Bluefruit.linked = true; + Bluefruit._setConnLed(true); +#if defined(DISABLE_BLE_LED) || (defined(RAK_BOARD) && defined(BLE_PIN_CODE)) + if (validPin(LED_BLUE)) assert(led_level == !LED_STATE_ON); +#else + if (validPin(LED_BLUE)) assert(led_level == LED_STATE_ON); +#endif +#if defined(RAK_BOARD) && defined(BLE_PIN_CODE) + // Stopping the advertising timer is asynchronous. Any callback already + // queued when the connection completes must leave the RAK indicator dark. + for (int i = 0; i < 4; ++i) { + bluefruit_blinky_cb(nullptr); + if (validPin(LED_BLUE)) assert(led_level == !LED_STATE_ON); + } +#endif + Bluefruit.linked = false; + Bluefruit._setConnLed(false); + if (validPin(LED_BLUE)) assert(led_level == !LED_STATE_ON); +#if defined(RAK_BOARD) && defined(BLE_PIN_CODE) + // A later disconnection lets the existing advertising timer blink again. + bluefruit_blinky_cb(nullptr); +#ifdef DISABLE_BLE_LED + if (validPin(LED_BLUE)) assert(led_level == !LED_STATE_ON); +#else + if (validPin(LED_BLUE)) assert(led_level == LED_STATE_ON); +#endif +#endif + if (!validPin(LED_BLUE)) assert(writes == 0); +} +''' + + +def compile_run(case, source, defines, expect_success=True): + if "AdafruitBluefruit::_setConnLed" in source: + defines = [*defines, "EXTERNAL_LED_METHOD"] + compiler = shutil.which("c++") or shutil.which("g++") + case.assertIsNotNone(compiler, "run this host suite where c++ is available") + with tempfile.TemporaryDirectory(prefix="mesh-nrf52-led-policy-") as folder: + folder = Path(folder) + (folder / "Arduino.h").write_text(ARDUINO) + (folder / "bluefruit.h").write_text(BLUEFRUIT) + (folder / "case.cpp").write_text(source) + binary = folder / ("case.exe" if os.name == "nt" else "case") + built = subprocess.run([ + compiler, "-std=c++11", "-Wall", "-Wextra", "-Werror", + "-I", str(folder), "-I", str(ROOT / "src"), + *["-D" + value for value in defines], str(folder / "case.cpp"), + "-o", str(binary)], text=True, capture_output=True, timeout=30) + case.assertEqual(built.returncode, 0, built.stderr) + result = subprocess.run([str(binary)], text=True, capture_output=True, timeout=10) + if expect_success: + case.assertEqual(result.returncode, 0, result.stdout + result.stderr) + else: + case.assertNotEqual(result.returncode, 0, "negative control did not fail") + + +class Nrf52LedPolicyTests(unittest.TestCase): + def test_policy_before_start_and_during_advertising_and_connection(self): + for polarity in (0, 1): + for disabled in (False, True): + defines = ["LED_BLUE=12", f"LED_STATE_ON={polarity}"] + if disabled: + defines.append("DISABLE_BLE_LED") + with self.subTest(polarity=polarity, disabled=disabled): + compile_run(self, HARNESS, defines) + + def test_absent_and_unmapped_pins_never_enable_bluefruit_led(self): + for pin in (None, "-1", "255", "PINS_COUNT", "62"): + defines = ["LED_STATE_ON=0"] + ([] if pin is None else ["LED_BLUE=" + pin]) + with self.subTest(pin=pin): + compile_run(self, HARNESS, defines) + + def test_rak_companion_policy_leaves_connection_indicator_off(self): + # https://github.com/meshcore-dev/MeshCore/pull/2694 + for flags in (["RAK_BOARD"], ["BLE_PIN_CODE=123456"], + ["RAK_BOARD", "BLE_PIN_CODE=123456"]): + with self.subTest(flags=flags): + compile_run(self, HARNESS, ["LED_BLUE=12", "LED_STATE_ON=0", *flags]) + + def test_pending_callback_respects_saved_policy_and_compile_time_disable(self): + source = PATCH.patched_led_source(CALLBACK) + TIMER_HARNESS + for polarity in (0, 1): + for disabled in (False, True): + defines = ["LED_BLUE=12", f"LED_STATE_ON={polarity}"] + if disabled: + defines.append("DISABLE_BLE_LED") + with self.subTest(polarity=polarity, disabled=disabled): + compile_run(self, source, defines) + + def test_pending_callback_ignores_invalid_pins(self): + source = PATCH.patched_led_source(CALLBACK) + TIMER_HARNESS + for pin in ("-1", "255", "PINS_COUNT", "62"): + with self.subTest(pin=pin): + compile_run(self, source, ["LED_BLUE=" + pin, "LED_STATE_ON=0"]) + + def test_rak_companion_blinks_while_advertising_then_goes_dark(self): + source = PATCH.patched_led_source(CALLBACK) + TIMER_HARNESS + for flags in (["RAK_BOARD"], ["BLE_PIN_CODE=123456"], + ["RAK_BOARD", "BLE_PIN_CODE=123456"], + ["RAK_BOARD", "BLE_PIN_CODE=123456", "DISABLE_BLE_LED"]): + for polarity in (0, 1): + with self.subTest(flags=flags, polarity=polarity): + compile_run(self, source, + ["LED_BLUE=12", f"LED_STATE_ON={polarity}", *flags]) + + def test_unpatched_callback_negative_control(self): + compile_run(self, "#include \n" + CALLBACK + TIMER_HARNESS, + ["LED_BLUE=12", "LED_STATE_ON=0"], expect_success=False) + + def test_patch_is_idempotent_and_rejects_unrecognized_callback(self): + patched = PATCH.patched_led_source(CALLBACK) + self.assertEqual(PATCH.patched_led_source(patched), patched) + with self.assertRaises(RuntimeError): + PATCH.patched_led_source(CALLBACK.replace("digitalToggle(LED_BLUE);", "changed();")) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_status_led_policy.py b/test/test_status_led_policy.py new file mode 100644 index 000000000..0d939f3c6 --- /dev/null +++ b/test/test_status_led_policy.py @@ -0,0 +1,134 @@ +"""Run the common LED policy and Xiao S3 LED initialization with fake GPIO.""" + +from pathlib import Path +import re +import shutil +import subprocess +import tempfile +import unittest + +from test_replay_reset_integration import extract_braced + +ROOT = Path(__file__).resolve().parents[1] + +ARDUINO = r''' +#pragma once +#include +#include +#define LOW 0 +#define HIGH 1 +#define INPUT 0 +#define OUTPUT 1 +struct Event { int kind, pin, value; }; +extern std::vector events; +inline void digitalWrite(int pin, int value) { events.push_back({0, pin, value}); } +inline void pinMode(int pin, int mode) { events.push_back({1, pin, mode}); } +inline void analogWrite(int pin, int value) { events.push_back({2, pin, value}); } +''' + +HARNESS = r''' +#include +#include +#include +std::vector events; +int main() { + // Enabling permits the existing heartbeat owner to run; it does not light + // an idle LED or change its output mode as a side effect of the command. + assert(mesh::leds::enabled()); + mesh::leds::applyStatusPolicy(); + assert(events.empty()); + mesh::leds::setEnabled(false); + mesh::leds::applyStatusPolicy(); + assert(!mesh::leds::enabled()); + assert(events.size() == EXPECT_EVENTS); +#if EXPECT_EVENTS == 2 + assert(events[0].kind == 0 && events[0].pin == PIN_STATUS_LED); + assert(events[0].value == EXPECT_OFF); + assert(events[1].kind == 1 && events[1].pin == PIN_STATUS_LED); + assert(events[1].value == OUTPUT); // Set the off latch before enabling output. +#elif EXPECT_EVENTS == 3 + const int pins[] = {PIN_STATUS_LED_R, PIN_STATUS_LED_G, PIN_STATUS_LED_B}; + for (unsigned i = 0; i < 3; ++i) + assert(events[i].kind == 2 && events[i].pin == pins[i] && events[i].value == 0); +#endif + events.clear(); + mesh::leds::setEnabled(true); + mesh::leds::applyStatusPolicy(); + assert(mesh::leds::enabled() && events.empty()); +} +''' + + +def compile_run(case, source, defines): + compiler = shutil.which("c++") or shutil.which("g++") + case.assertIsNotNone(compiler, "run this host suite where c++ is available") + with tempfile.TemporaryDirectory(prefix="mesh-status-led-") as folder: + folder = Path(folder) + (folder / "Arduino.h").write_text(ARDUINO) + (folder / "target.h").write_text("#pragma once\n" + "\n".join( + "#define " + define.replace("=", " ", 1) for define in defines)) + (folder / "case.cpp").write_text(source) + binary = folder / "case" + built = subprocess.run([ + compiler, "-std=c++11", "-Wall", "-Wextra", "-Werror", + "-I", str(folder), "-I", str(ROOT / "src"), + str(ROOT / "src/helpers/LedPolicy.cpp"), str(folder / "case.cpp"), + "-o", str(binary)], text=True, capture_output=True, timeout=30) + case.assertEqual(built.returncode, 0, built.stderr) + ran = subprocess.run([str(binary)], text=True, capture_output=True, timeout=10) + case.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr) + + +class StatusLedPolicyTests(unittest.TestCase): + def test_off_is_immediate_and_on_leaves_heartbeat_in_control(self): + cases = ( + ("active-high", ["PIN_STATUS_LED=12", "LED_STATE_ON=HIGH", + "EXPECT_OFF=LOW", "EXPECT_EVENTS=2"]), + ("active-low", ["PIN_STATUS_LED=12", "LED_STATE_ON=LOW", + "EXPECT_OFF=HIGH", "EXPECT_EVENTS=2"]), + ("default-polarity", ["PIN_STATUS_LED=12", "EXPECT_OFF=LOW", + "EXPECT_EVENTS=2"]), + ("absent", ["EXPECT_EVENTS=0"]), + ("negative-pin", ["PIN_STATUS_LED=-1", "EXPECT_EVENTS=0"]), + ("rgb", ["PIN_STATUS_LED_R=3", "PIN_STATUS_LED_G=24", + "PIN_STATUS_LED_B=28", "PIN_STATUS_LED=24", "EXPECT_EVENTS=3"]), + ) + for name, defines in cases: + with self.subTest(board=name): + compile_run(self, HARNESS, defines) + + def test_xiao_s3_uses_active_low_output_during_radio_init(self): + ini = (ROOT / "variants/xiao_s3/platformio.ini").read_text() + names = ("PIN_STATUS_LED", "LED_STATE_ON", "P_LORA_SCLK", + "P_LORA_MISO", "P_LORA_MOSI") + definitions = {name: re.findall(r"-D\s+" + name + r"=([^\s;]+)", ini) + for name in names} + self.assertEqual(definitions["PIN_STATUS_LED"], ["21"]) + self.assertEqual(definitions["LED_STATE_ON"], ["LOW"]) + for name in names: + self.assertEqual(len(definitions[name]), 1, name) + method = extract_braced((ROOT / "variants/xiao_s3/target.cpp").read_text(), + "bool radio_init()") + source = r''' +#include +#include +std::vector events; +struct Clock { template void begin(T...) {} } fallback_clock, rtc_clock; +struct Spi { void begin(int, int, int) {} } spi; +struct Radio { bool std_init(Spi*) { return true; } } radio; +int Wire = 0; +@METHOD@ +int main() { + assert(radio_init()); + assert(events.size() == 3); + assert(events[0].kind == 0 && events[0].pin == 21 && events[0].value == HIGH); + assert(events[1].kind == 1 && events[1].pin == 21 && events[1].value == OUTPUT); + assert(events[2].kind == 1 && events[2].pin == 48 && events[2].value == OUTPUT); +} +''' + compile_run(self, source.replace("@METHOD@", method), + [name + "=" + definitions[name][0] for name in names]) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_tbeam_charge_target.py b/test/test_tbeam_charge_target.py index 73d5d8140..bc261efaa 100644 --- a/test/test_tbeam_charge_target.py +++ b/test/test_tbeam_charge_target.py @@ -231,6 +231,7 @@ MOCK_SDK = r''' #pragma once #include #include +#include #include #include #include @@ -291,6 +292,7 @@ public: int startup_reason = 0; virtual ~ESP32Board() {} void begin() {} + virtual void applyLedPolicy() {} virtual void onBeforeTransmit() {} virtual void onAfterTransmit() {} virtual bool getBatteryChargeTarget(uint16_t&) { return false; } @@ -305,6 +307,7 @@ constexpr int XPOWERS_DCDC2 = 0, XPOWERS_DCDC3 = 0, XPOWERS_DCDC4 = 0; constexpr int XPOWERS_DCDC5 = 0, XPOWERS_ALDO1 = 0, XPOWERS_ALDO2 = 0; constexpr int XPOWERS_ALDO3 = 0, XPOWERS_ALDO4 = 0, XPOWERS_BLDO1 = 0; constexpr int XPOWERS_BLDO2 = 0, XPOWERS_VBACKUP = 0, XPOWERS_CHG_LED_CTRL_CHG = 0; +constexpr int XPOWERS_CHG_LED_OFF = 1; constexpr int XPOWERS_DLDO1 = 0, XPOWERS_DLDO2 = 0; constexpr int XPOWERS_AXP2101_DCDC4_VOL2_MAX = 0, XPOWERS_AXP192_ALL_IRQ = 0; constexpr int XPOWERS_AXP2101_ALL_IRQ = 0, XPOWERS_AXP192_CHG_CUR_450MA = 0; diff --git a/test/test_wio_tracker_l2_gps_power.py b/test/test_wio_tracker_l2_gps_power.py index b9018c295..e47e943b8 100644 --- a/test/test_wio_tracker_l2_gps_power.py +++ b/test/test_wio_tracker_l2_gps_power.py @@ -114,9 +114,11 @@ inline TwoWire Wire; ESP_BOARD = r''' #pragma once #include +#include class ESP32Board { public: virtual void begin() {} + virtual void applyLedPolicy() {} virtual uint16_t getBattMilliVolts() { return 0; } virtual const char* getManufacturerName() const { return "host"; } virtual void onBeforeTransmit() {} diff --git a/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.h b/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.h index b18ea8afa..eba6a7176 100644 --- a/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.h +++ b/variants/gat562_30s_mesh_kit/GAT56230SMeshKitBoard.h @@ -34,8 +34,13 @@ public: } #if defined(P_LORA_TX_LED) + void applyLedPolicy() override { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); + } + void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on } void onAfterTransmit() override { diff --git a/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.h b/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.h index 0d2b9f472..bf64b19d8 100644 --- a/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.h +++ b/variants/gat562_mesh_evb_pro/GAT562EVBProBoard.h @@ -34,8 +34,13 @@ public: } #if defined(P_LORA_TX_LED) + void applyLedPolicy() override { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); + } + void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on } void onAfterTransmit() override { diff --git a/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.h b/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.h index e694b15f4..33f51043b 100644 --- a/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.h +++ b/variants/gat562_mesh_tracker_pro/GAT562MeshTrackerProBoard.h @@ -34,8 +34,13 @@ public: } #if defined(P_LORA_TX_LED) + void applyLedPolicy() override { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); + } + void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on } void onAfterTransmit() override { diff --git a/variants/heltec_rc32/HeltecRC32Board.cpp b/variants/heltec_rc32/HeltecRC32Board.cpp index 1f3048b51..5fe3036cb 100644 --- a/variants/heltec_rc32/HeltecRC32Board.cpp +++ b/variants/heltec_rc32/HeltecRC32Board.cpp @@ -56,7 +56,7 @@ void HeltecRC32Board::powerOff() { } void HeltecRC32Board::onBeforeTransmit() { - digitalWrite(P_LORA_TX_LED, HIGH); + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); } void HeltecRC32Board::onAfterTransmit() { diff --git a/variants/heltec_t096/T096Board.cpp b/variants/heltec_t096/T096Board.cpp index 2d87d11a9..eaa91d5b1 100644 --- a/variants/heltec_t096/T096Board.cpp +++ b/variants/heltec_t096/T096Board.cpp @@ -56,8 +56,13 @@ void T096Board::begin() { delay(1); } +void T096Board::applyLedPolicy() { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); +} + void T096Board::onBeforeTransmit() { - digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on loRaFEMControl.setTxModeEnable(); } diff --git a/variants/heltec_t096/T096Board.h b/variants/heltec_t096/T096Board.h index 6980e527c..1dd40fafd 100644 --- a/variants/heltec_t096/T096Board.h +++ b/variants/heltec_t096/T096Board.h @@ -25,6 +25,7 @@ public: void attachDynamicPrefs(KeyValueStore* prefs); bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; + void applyLedPolicy() override; void onBeforeTransmit(void) override; void onAfterTransmit(void) override; uint16_t getBattMilliVolts() override; diff --git a/variants/heltec_t1/T1Board.cpp b/variants/heltec_t1/T1Board.cpp index 8804d579b..51d103c6e 100644 --- a/variants/heltec_t1/T1Board.cpp +++ b/variants/heltec_t1/T1Board.cpp @@ -58,8 +58,13 @@ void T1Board::begin() { delay(1); } +void T1Board::applyLedPolicy() { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, !LED_STATE_ON); +} + void T1Board::onBeforeTransmit() { - digitalWrite(P_LORA_TX_LED, LED_STATE_ON); + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LED_STATE_ON); } void T1Board::onAfterTransmit() { diff --git a/variants/heltec_t1/T1Board.h b/variants/heltec_t1/T1Board.h index 3e9afd1b9..812bc7914 100644 --- a/variants/heltec_t1/T1Board.h +++ b/variants/heltec_t1/T1Board.h @@ -18,6 +18,7 @@ public: T1Board() : periph_power(PIN_TFT_VDD_CTL, PIN_TFT_VDD_CTL_ACTIVE), NRF52Board("T1_OTA") {} void begin(); + void applyLedPolicy() override; void onBeforeTransmit() override; void onAfterTransmit() override; uint16_t getBattMilliVolts() override; diff --git a/variants/heltec_t114/T114Board.h b/variants/heltec_t114/T114Board.h index e3ea410e0..e398066c3 100644 --- a/variants/heltec_t114/T114Board.h +++ b/variants/heltec_t114/T114Board.h @@ -20,8 +20,13 @@ public: void begin(); #if defined(P_LORA_TX_LED) + void applyLedPolicy() override { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); + } + void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED off diff --git a/variants/heltec_tower_v2/HeltecTowerV2Board.cpp b/variants/heltec_tower_v2/HeltecTowerV2Board.cpp index 801ba95bd..6e19d2a9d 100644 --- a/variants/heltec_tower_v2/HeltecTowerV2Board.cpp +++ b/variants/heltec_tower_v2/HeltecTowerV2Board.cpp @@ -60,8 +60,13 @@ void HeltecTowerV2Board::begin() { loRaFEMControl.init(); } +void HeltecTowerV2Board::applyLedPolicy() { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, !LED_STATE_ON); +} + void HeltecTowerV2Board::onBeforeTransmit() { - digitalWrite(P_LORA_TX_LED, LED_STATE_ON); + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LED_STATE_ON); loRaFEMControl.setTxModeEnable(); } diff --git a/variants/heltec_tower_v2/HeltecTowerV2Board.h b/variants/heltec_tower_v2/HeltecTowerV2Board.h index d6cc85ef7..375f1a485 100644 --- a/variants/heltec_tower_v2/HeltecTowerV2Board.h +++ b/variants/heltec_tower_v2/HeltecTowerV2Board.h @@ -16,6 +16,7 @@ public: HeltecTowerV2Board() : NRF52Board("TOWER_V2_OTA") {} void begin(); + void applyLedPolicy() override; void onBeforeTransmit() override; void onAfterTransmit() override; uint16_t getBattMilliVolts() override; diff --git a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp index 51a037ea6..c7f0672fc 100644 --- a/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp +++ b/variants/heltec_tracker_v2/HeltecTrackerV2Board.cpp @@ -27,7 +27,7 @@ void HeltecTrackerV2Board::begin() { } void HeltecTrackerV2Board::onBeforeTransmit(void) { - digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on loRaFEMControl.setTxModeEnable(); } diff --git a/variants/heltec_v4/HeltecV4Board.cpp b/variants/heltec_v4/HeltecV4Board.cpp index 24483c5e4..b6984e606 100644 --- a/variants/heltec_v4/HeltecV4Board.cpp +++ b/variants/heltec_v4/HeltecV4Board.cpp @@ -24,7 +24,7 @@ void HeltecV4Board::begin() { } void HeltecV4Board::onBeforeTransmit(void) { - digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on loRaFEMControl.setTxModeEnable(); } diff --git a/variants/heltec_v4_r8/HeltecV4R8Board.cpp b/variants/heltec_v4_r8/HeltecV4R8Board.cpp index f1ce286c4..37f1a18e9 100644 --- a/variants/heltec_v4_r8/HeltecV4R8Board.cpp +++ b/variants/heltec_v4_r8/HeltecV4R8Board.cpp @@ -37,7 +37,7 @@ void HeltecV4R8Board::begin() { } void HeltecV4R8Board::onBeforeTransmit(void) { - digitalWrite(P_LORA_TX_LED, HIGH); + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); loRaFEMControl.setTxModeEnable(); } diff --git a/variants/ikoka_handheld_nrf/IkokaNrf52Board.h b/variants/ikoka_handheld_nrf/IkokaNrf52Board.h index 954b34785..53e27b0d9 100644 --- a/variants/ikoka_handheld_nrf/IkokaNrf52Board.h +++ b/variants/ikoka_handheld_nrf/IkokaNrf52Board.h @@ -12,8 +12,13 @@ public: void begin(); #if defined(P_LORA_TX_LED) + void applyLedPolicy() override { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); + } + void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED off diff --git a/variants/ikoka_nano_nrf/IkokaNanoNRFBoard.h b/variants/ikoka_nano_nrf/IkokaNanoNRFBoard.h index 7d1b6ba00..92bf74627 100644 --- a/variants/ikoka_nano_nrf/IkokaNanoNRFBoard.h +++ b/variants/ikoka_nano_nrf/IkokaNanoNRFBoard.h @@ -12,8 +12,13 @@ public: void begin(); #if defined(P_LORA_TX_LED) + void applyLedPolicy() override { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); + } + void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on #if defined(LED_BLUE) // turn off that annoying blue LED before transmitting digitalWrite(LED_BLUE, HIGH); diff --git a/variants/ikoka_stick_nrf/IkokaStickNRFBoard.h b/variants/ikoka_stick_nrf/IkokaStickNRFBoard.h index 6ab046c77..b664861bb 100644 --- a/variants/ikoka_stick_nrf/IkokaStickNRFBoard.h +++ b/variants/ikoka_stick_nrf/IkokaStickNRFBoard.h @@ -12,8 +12,13 @@ public: void begin(); #if defined(P_LORA_TX_LED) + void applyLedPolicy() override { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); + } + void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on #if defined(LED_BLUE) // turn off that annoying blue LED before transmitting digitalWrite(LED_BLUE, HIGH); diff --git a/variants/keepteen_lt1/KeepteenLT1Board.h b/variants/keepteen_lt1/KeepteenLT1Board.h index 25d4d8676..d32dfcd23 100644 --- a/variants/keepteen_lt1/KeepteenLT1Board.h +++ b/variants/keepteen_lt1/KeepteenLT1Board.h @@ -30,8 +30,13 @@ public: } #if defined(P_LORA_TX_LED) + void applyLedPolicy() override { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); + } + void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off diff --git a/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h b/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h index f7ef536ba..136601d06 100644 --- a/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h +++ b/variants/lilygo_t_impulse_plus/TImpulsePlusBoard.h @@ -13,8 +13,13 @@ public: void begin(); #if defined(P_LORA_TX_LED) + void applyLedPolicy() override { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); + } + void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp b/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp index 4efa9f4a2..86a6573f0 100644 --- a/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.cpp @@ -84,8 +84,12 @@ void TBeam1WBoard::powerCycleRadio() { power_off_level, digitalRead(SX126X_POWER_EN)); } +void TBeam1WBoard::applyLedPolicy() { + if (!mesh::leds::enabled()) digitalWrite(LED_PIN, LOW); +} + void TBeam1WBoard::onBeforeTransmit() { - digitalWrite(LED_PIN, HIGH); // TX LED on + if (mesh::leds::enabled()) digitalWrite(LED_PIN, HIGH); // TX LED on portENTER_CRITICAL(&_fan_mux); _tx_active = true; if (_mode == FAN_AUTO && !_stopped) { diff --git a/variants/lilygo_tbeam_1w/TBeam1WBoard.h b/variants/lilygo_tbeam_1w/TBeam1WBoard.h index 9bd1574fb..9c8db6346 100644 --- a/variants/lilygo_tbeam_1w/TBeam1WBoard.h +++ b/variants/lilygo_tbeam_1w/TBeam1WBoard.h @@ -71,6 +71,7 @@ public: void begin(); void attachDynamicPrefs(KeyValueStore* prefs); bool handleCommand(const char* command, uint32_t sender_timestamp, char* reply) override; + void applyLedPolicy() override; void onBeforeTransmit() override; void onAfterTransmit() override; uint16_t getBattMilliVolts() override; diff --git a/variants/lilygo_tdeck/TDeckBoard.h b/variants/lilygo_tdeck/TDeckBoard.h index e2844360d..48e9dfd03 100644 --- a/variants/lilygo_tdeck/TDeckBoard.h +++ b/variants/lilygo_tdeck/TDeckBoard.h @@ -13,13 +13,17 @@ public: void begin(); #ifdef P_LORA_TX_LED - void onBeforeTransmit() override{ - digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on - invert pin for SX1276 - } + void applyLedPolicy() override { + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); + } - void onAfterTransmit() override{ - digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED off - invert pin for SX1276 - } + void onBeforeTransmit() override{ + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on - invert pin for SX1276 + } + + void onAfterTransmit() override{ + digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED off - invert pin for SX1276 + } #endif uint16_t getBattMilliVolts() { @@ -41,4 +45,4 @@ public: const char* getManufacturerName() const{ return "LilyGo T-Deck"; } -}; \ No newline at end of file +}; diff --git a/variants/lilygo_techo_card/TechoCardBoard.cpp b/variants/lilygo_techo_card/TechoCardBoard.cpp index 8a3a54b16..86dbf53db 100644 --- a/variants/lilygo_techo_card/TechoCardBoard.cpp +++ b/variants/lilygo_techo_card/TechoCardBoard.cpp @@ -57,7 +57,16 @@ uint16_t TechoCardBoard::getBattMilliVolts() { return (uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB); } +void TechoCardBoard::applyLedPolicy() { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) { + Led_A.setPixelColor(0, 0, 0, 0); + Led_A.show(); + } +} + void TechoCardBoard::onBeforeTransmit() { + if (!mesh::leds::enabled()) return; Led_A.setPixelColor(0, 20, 20, 20); // turn TX LED on Led_A.show(); } diff --git a/variants/lilygo_techo_card/TechoCardBoard.h b/variants/lilygo_techo_card/TechoCardBoard.h index a2ee3ea1e..9fd6935df 100644 --- a/variants/lilygo_techo_card/TechoCardBoard.h +++ b/variants/lilygo_techo_card/TechoCardBoard.h @@ -20,6 +20,7 @@ public: TechoCardBoard() : NRF52Board("TECHO_OTA") {} void begin(); uint16_t getBattMilliVolts() override; + void applyLedPolicy() override; void onBeforeTransmit(void) override; void onAfterTransmit(void) override; diff --git a/variants/meshnology_w12/MeshnologyW12Board.cpp b/variants/meshnology_w12/MeshnologyW12Board.cpp index e40912524..d877d2ed1 100644 --- a/variants/meshnology_w12/MeshnologyW12Board.cpp +++ b/variants/meshnology_w12/MeshnologyW12Board.cpp @@ -29,8 +29,12 @@ void MeshnologyW12Board::begin() { } } + void MeshnologyW12Board::applyLedPolicy() { + if (!mesh::leds::enabled()) neopixelWrite(NEOPIXEL_LED, 0, 0, 0); + } + void MeshnologyW12Board::onBeforeTransmit(void) { - neopixelWrite(NEOPIXEL_LED, NEOPIXEL_BRIGHTNESS, NEOPIXEL_BRIGHTNESS, NEOPIXEL_BRIGHTNESS); // turn TX neopixel on (White) + if (mesh::leds::enabled()) neopixelWrite(NEOPIXEL_LED, NEOPIXEL_BRIGHTNESS, NEOPIXEL_BRIGHTNESS, NEOPIXEL_BRIGHTNESS); // turn TX neopixel on (White) } void MeshnologyW12Board::onAfterTransmit(void) { diff --git a/variants/meshnology_w12/MeshnologyW12Board.h b/variants/meshnology_w12/MeshnologyW12Board.h index bf87ca11b..f08acc1e3 100644 --- a/variants/meshnology_w12/MeshnologyW12Board.h +++ b/variants/meshnology_w12/MeshnologyW12Board.h @@ -22,6 +22,7 @@ public: MeshnologyW12Board() : periph_power(PIN_VEXT_EN, PIN_VEXT_EN_ACTIVE) { } void begin(); + void applyLedPolicy() override; void onBeforeTransmit(void) override; void onAfterTransmit(void) override; void enterDeepSleep(uint32_t secs, int pin_wake_btn = -1); diff --git a/variants/meshtiny/MeshtinyBoard.h b/variants/meshtiny/MeshtinyBoard.h index 2d3e5de96..e5841e8b5 100644 --- a/variants/meshtiny/MeshtinyBoard.h +++ b/variants/meshtiny/MeshtinyBoard.h @@ -13,8 +13,13 @@ public: void begin(); #if defined(P_LORA_TX_LED) + void applyLedPolicy() override { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); + } + void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off diff --git a/variants/meshtracker_x1/MeshTrackerX1Board.h b/variants/meshtracker_x1/MeshTrackerX1Board.h index 02df7b683..99ae1a6be 100644 --- a/variants/meshtracker_x1/MeshTrackerX1Board.h +++ b/variants/meshtracker_x1/MeshTrackerX1Board.h @@ -78,7 +78,7 @@ public: // set led on and wait for button release before poweroff #ifdef LED_PIN - digitalWrite(LED_PIN, HIGH); + if (mesh::leds::enabled()) digitalWrite(LED_PIN, HIGH); #endif #ifdef BUTTON_PIN while(digitalRead(BUTTON_PIN)); diff --git a/variants/minewsemi_me25ls01/MinewsemiME25LS01Board.h b/variants/minewsemi_me25ls01/MinewsemiME25LS01Board.h index e4bbc7d06..5ce5cad20 100644 --- a/variants/minewsemi_me25ls01/MinewsemiME25LS01Board.h +++ b/variants/minewsemi_me25ls01/MinewsemiME25LS01Board.h @@ -70,8 +70,13 @@ public: } #if defined(P_LORA_TX_LED) + void applyLedPolicy() override { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); + } + void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, HIGH);// turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH);// turn TX LED on } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off diff --git a/variants/muziworks_r1_neo/R1NeoBoard.h b/variants/muziworks_r1_neo/R1NeoBoard.h index ecd75b717..2e21aa2cb 100644 --- a/variants/muziworks_r1_neo/R1NeoBoard.h +++ b/variants/muziworks_r1_neo/R1NeoBoard.h @@ -26,8 +26,13 @@ public: #endif #if defined(P_LORA_TX_LED) + void applyLedPolicy() override { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); + } + void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on #if defined(LED_BLUE) // turn off that annoying blue LED before transmitting digitalWrite(LED_BLUE, LOW); diff --git a/variants/rak11310/RAK11310Board.h b/variants/rak11310/RAK11310Board.h index 6bef47b14..f58d424d4 100644 --- a/variants/rak11310/RAK11310Board.h +++ b/variants/rak11310/RAK11310Board.h @@ -24,7 +24,11 @@ public: void attachDynamicPrefs(KeyValueStore* prefs) { (void)prefs; } // no-op #ifdef P_LORA_TX_LED - void onBeforeTransmit() override { digitalWrite(P_LORA_TX_LED, HIGH); } + void applyLedPolicy() override { + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); + } + + void onBeforeTransmit() override { if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); } #endif diff --git a/variants/rak_wismesh_tag/RAKWismeshTagBoard.h b/variants/rak_wismesh_tag/RAKWismeshTagBoard.h index 9368d17e8..97aa0261f 100644 --- a/variants/rak_wismesh_tag/RAKWismeshTagBoard.h +++ b/variants/rak_wismesh_tag/RAKWismeshTagBoard.h @@ -14,8 +14,13 @@ public: void begin(); #if defined(P_LORA_TX_LED) && defined(LED_STATE_ON) + void applyLedPolicy() override { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, !LED_STATE_ON); + } + void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, LED_STATE_ON); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LED_STATE_ON); // turn TX LED on } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, !LED_STATE_ON); // turn TX LED off @@ -53,7 +58,7 @@ public: // set led on and wait for button release before poweroff #ifdef LED_PIN - digitalWrite(LED_PIN, HIGH); + if (mesh::leds::enabled()) digitalWrite(LED_PIN, HIGH); #endif #ifdef BUTTON_PIN // wismesh tag uses LOW to indicate button is pressed, wait until it goes HIGH to indicate it was released diff --git a/variants/rpi_picow/PicoWBoard.h b/variants/rpi_picow/PicoWBoard.h index d0037558e..72d8d834a 100644 --- a/variants/rpi_picow/PicoWBoard.h +++ b/variants/rpi_picow/PicoWBoard.h @@ -19,8 +19,12 @@ public: void attachDynamicPrefs(KeyValueStore* prefs) { (void)prefs; } // no-op + void applyLedPolicy() override { + if (!mesh::leds::enabled()) digitalWrite(LED_BUILTIN, LOW); + } + void onBeforeTransmit() override { - digitalWrite(LED_BUILTIN, HIGH); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(LED_BUILTIN, HIGH); // turn TX LED on } void onAfterTransmit() override { diff --git a/variants/sensecap_solar/SenseCapSolarBoard.h b/variants/sensecap_solar/SenseCapSolarBoard.h index 0fc2c5e5c..b2342b963 100644 --- a/variants/sensecap_solar/SenseCapSolarBoard.h +++ b/variants/sensecap_solar/SenseCapSolarBoard.h @@ -15,8 +15,13 @@ public: void begin(); #if defined(P_LORA_TX_LED) + void applyLedPolicy() override { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); + } + void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off diff --git a/variants/t1000-e/T1000eBoard.h b/variants/t1000-e/T1000eBoard.h index a08fc4e4f..f0b9dd7dd 100644 --- a/variants/t1000-e/T1000eBoard.h +++ b/variants/t1000-e/T1000eBoard.h @@ -73,7 +73,7 @@ public: #endif // set led on and wait for button release before poweroff #ifdef LED_PIN - digitalWrite(LED_PIN, HIGH); + if (mesh::leds::enabled()) digitalWrite(LED_PIN, HIGH); #endif #ifdef BUTTON_PIN while(digitalRead(BUTTON_PIN)); diff --git a/variants/thinknode_m1/ThinkNodeM1Board.h b/variants/thinknode_m1/ThinkNodeM1Board.h index e5af3690d..f3eb8ed66 100644 --- a/variants/thinknode_m1/ThinkNodeM1Board.h +++ b/variants/thinknode_m1/ThinkNodeM1Board.h @@ -20,8 +20,13 @@ public: uint16_t getBattMilliVolts() override; #if defined(P_LORA_TX_LED) + void applyLedPolicy() override { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); + } + void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off diff --git a/variants/thinknode_m3/ThinkNodeM3Board.h b/variants/thinknode_m3/ThinkNodeM3Board.h index 496ee2e0e..2584fd4cb 100644 --- a/variants/thinknode_m3/ThinkNodeM3Board.h +++ b/variants/thinknode_m3/ThinkNodeM3Board.h @@ -19,8 +19,13 @@ public: uint16_t getBattMilliVolts() override; #ifdef P_LORA_TX_LED + void applyLedPolicy() override { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, !LED_STATE_ON); + } + void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, LED_STATE_ON); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LED_STATE_ON); // turn TX LED on } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, !LED_STATE_ON); // turn TX LED off diff --git a/variants/thinknode_m4/ThinkNodeM4Board.h b/variants/thinknode_m4/ThinkNodeM4Board.h index c2cae54b6..72a9d97b4 100644 --- a/variants/thinknode_m4/ThinkNodeM4Board.h +++ b/variants/thinknode_m4/ThinkNodeM4Board.h @@ -20,8 +20,13 @@ public: uint16_t getBattMilliVolts() override; #ifdef P_LORA_TX_LED + void applyLedPolicy() override { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, !LED_STATE_ON); + } + void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, LED_STATE_ON); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LED_STATE_ON); // turn TX LED on } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, !LED_STATE_ON); // turn TX LED off @@ -48,7 +53,7 @@ public: NRF52Board::shutdownPeripherals(); #ifdef LED_STATUS - digitalWrite(LED_STATUS, HIGH); + if (mesh::leds::enabled()) digitalWrite(LED_STATUS, HIGH); #endif #ifdef BUTTON_PIN while(!digitalRead(BUTTON_PIN)); diff --git a/variants/thinknode_m5/ThinknodeM5Board.h b/variants/thinknode_m5/ThinknodeM5Board.h index 57ff0d00e..eb2ea3394 100644 --- a/variants/thinknode_m5/ThinknodeM5Board.h +++ b/variants/thinknode_m5/ThinknodeM5Board.h @@ -15,8 +15,12 @@ public: uint16_t getBattMilliVolts() override; const char* getManufacturerName() const override ; + void applyLedPolicy() override { + if (!mesh::leds::enabled()) expander.digitalWrite(EXP_PIN_LED, LOW); + } + void onBeforeTransmit() override { - expander.digitalWrite(EXP_PIN_LED, HIGH); // turn TX LED on + if (mesh::leds::enabled()) expander.digitalWrite(EXP_PIN_LED, HIGH); // turn TX LED on } void onAfterTransmit() override { expander.digitalWrite(EXP_PIN_LED, LOW); // turn TX LED off diff --git a/variants/thinknode_m6/ThinkNodeM6Board.h b/variants/thinknode_m6/ThinkNodeM6Board.h index 3203ab718..6dde0f13c 100644 --- a/variants/thinknode_m6/ThinkNodeM6Board.h +++ b/variants/thinknode_m6/ThinkNodeM6Board.h @@ -24,8 +24,13 @@ public: uint16_t getBattMilliVolts() override; #if defined(P_LORA_TX_LED) + void applyLedPolicy() override { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); + } + void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off diff --git a/variants/thinknode_m7/ThinkNodeM7Board.h b/variants/thinknode_m7/ThinkNodeM7Board.h index 7e705bde7..f80d79c6e 100644 --- a/variants/thinknode_m7/ThinkNodeM7Board.h +++ b/variants/thinknode_m7/ThinkNodeM7Board.h @@ -12,8 +12,12 @@ class ThinkNodeM7Board : public ESP32Board { public: void begin(); const char* getManufacturerName() const override; + void applyLedPolicy() override { + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); + } + void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, LOW); + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, HIGH); diff --git a/variants/thinknode_m8/ThinkNodeM8Board.h b/variants/thinknode_m8/ThinkNodeM8Board.h index 73aeb1f5d..7f38ba53a 100644 --- a/variants/thinknode_m8/ThinkNodeM8Board.h +++ b/variants/thinknode_m8/ThinkNodeM8Board.h @@ -20,8 +20,13 @@ public: uint16_t getBattMilliVolts() override; #if defined(P_LORA_TX_LED) + void applyLedPolicy() override { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); + } + void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off diff --git a/variants/waveshare_rp2040_lora/WaveshareBoard.h b/variants/waveshare_rp2040_lora/WaveshareBoard.h index ecf3bdd23..b1bdca56b 100644 --- a/variants/waveshare_rp2040_lora/WaveshareBoard.h +++ b/variants/waveshare_rp2040_lora/WaveshareBoard.h @@ -36,7 +36,11 @@ public: void attachDynamicPrefs(KeyValueStore* prefs) { (void)prefs; } // no-op #ifdef P_LORA_TX_LED - void onBeforeTransmit() override { digitalWrite(P_LORA_TX_LED, HIGH); } + void applyLedPolicy() override { + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); + } + + void onBeforeTransmit() override { if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); } #endif diff --git a/variants/wio-tracker-l1-1w/WioTrackerL1Board.h b/variants/wio-tracker-l1-1w/WioTrackerL1Board.h index d8a17829c..bb1e314e7 100644 --- a/variants/wio-tracker-l1-1w/WioTrackerL1Board.h +++ b/variants/wio-tracker-l1-1w/WioTrackerL1Board.h @@ -19,8 +19,13 @@ public: bool canControlLoRaFemPaGain() const override { return true; } #if defined(P_LORA_TX_LED) + void applyLedPolicy() override { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); + } + void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off diff --git a/variants/wio-tracker-l1/WioTrackerL1Board.h b/variants/wio-tracker-l1/WioTrackerL1Board.h index 9dbe85642..efa232fa7 100644 --- a/variants/wio-tracker-l1/WioTrackerL1Board.h +++ b/variants/wio-tracker-l1/WioTrackerL1Board.h @@ -13,8 +13,13 @@ public: void begin(); #if defined(P_LORA_TX_LED) + void applyLedPolicy() override { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); + } + void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED off diff --git a/variants/wio-tracker-l2/WioTrackerL2Board.cpp b/variants/wio-tracker-l2/WioTrackerL2Board.cpp index c68a5e1de..f521d3150 100644 --- a/variants/wio-tracker-l2/WioTrackerL2Board.cpp +++ b/variants/wio-tracker-l2/WioTrackerL2Board.cpp @@ -146,7 +146,7 @@ void WioTrackerL2Board::setLed(bool on) { // P10 doubles as GNSS wakeup and idles HIGH; blink = brief LOW pulses so // the GPS never sees a sustained low level if (expander_ok) { - expWritePin(EXP_PIN_USER_LED, !on); + expWritePin(EXP_PIN_USER_LED, !(on && mesh::leds::enabled())); } } diff --git a/variants/wio-tracker-l2/WioTrackerL2Board.h b/variants/wio-tracker-l2/WioTrackerL2Board.h index c5644e2f2..3a7f8ecbe 100644 --- a/variants/wio-tracker-l2/WioTrackerL2Board.h +++ b/variants/wio-tracker-l2/WioTrackerL2Board.h @@ -49,6 +49,9 @@ public: void onBeforeTransmit() override { setLed(true); } void onAfterTransmit() override { setLed(false); } + void applyLedPolicy() override { + if (!mesh::leds::enabled()) setLed(false); + } // Mesh/user LED lives on the IO expander, not a GPIO void setLed(bool on); diff --git a/variants/wio_wm1110/WioWM1110Board.cpp b/variants/wio_wm1110/WioWM1110Board.cpp index 2825e5543..40e374174 100644 --- a/variants/wio_wm1110/WioWM1110Board.cpp +++ b/variants/wio_wm1110/WioWM1110Board.cpp @@ -13,7 +13,7 @@ void WioWM1110Board::begin() { pinMode(LED_RED, OUTPUT); pinMode(SENSOR_POWER_PIN, OUTPUT); - digitalWrite(LED_GREEN, HIGH); + digitalWrite(LED_GREEN, mesh::leds::enabled() ? HIGH : LOW); digitalWrite(LED_RED, LOW); digitalWrite(SENSOR_POWER_PIN, LOW); diff --git a/variants/wio_wm1110/WioWM1110Board.h b/variants/wio_wm1110/WioWM1110Board.h index 26f95c868..310b72255 100644 --- a/variants/wio_wm1110/WioWM1110Board.h +++ b/variants/wio_wm1110/WioWM1110Board.h @@ -17,8 +17,14 @@ public: void begin(); #if defined(LED_GREEN) + void applyLedPolicy() override { + NRF52Board::applyLedPolicy(); + digitalWrite(LED_GREEN, mesh::leds::enabled() ? HIGH : LOW); + if (!mesh::leds::enabled()) digitalWrite(LED_RED, LOW); + } + void onBeforeTransmit() override { - digitalWrite(LED_RED, HIGH); + if (mesh::leds::enabled()) digitalWrite(LED_RED, HIGH); } void onAfterTransmit() override { digitalWrite(LED_RED, LOW); diff --git a/variants/xiao_c3/XiaoC3Board.h b/variants/xiao_c3/XiaoC3Board.h index 61d2b1d01..67826716a 100644 --- a/variants/xiao_c3/XiaoC3Board.h +++ b/variants/xiao_c3/XiaoC3Board.h @@ -42,7 +42,7 @@ public: #if defined(LORA_TX_BOOST_PIN) || defined(P_LORA_TX_LED) void onBeforeTransmit() override { #if defined(P_LORA_TX_LED) - digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED on #endif #if defined(LORA_TX_BOOST_PIN) digitalWrite(LORA_TX_BOOST_PIN, LOW); diff --git a/variants/xiao_nrf52/XiaoNrf52Board.h b/variants/xiao_nrf52/XiaoNrf52Board.h index 0c97a1392..d4892fcbf 100644 --- a/variants/xiao_nrf52/XiaoNrf52Board.h +++ b/variants/xiao_nrf52/XiaoNrf52Board.h @@ -21,8 +21,13 @@ public: void begin(); #if defined(P_LORA_TX_LED) + void applyLedPolicy() override { + NRF52Board::applyLedPolicy(); + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); + } + void onBeforeTransmit() override { - digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on + if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); // turn TX LED on } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, HIGH); // turn TX LED off @@ -43,7 +48,7 @@ public: NRF52Board::shutdownPeripherals(); // set led on and wait for button release before poweroff - digitalWrite(PIN_LED, LOW); + if (mesh::leds::enabled()) digitalWrite(PIN_LED, LOW); #ifdef PIN_USER_BTN while(digitalRead(PIN_USER_BTN) == USER_BTN_PRESSED); #endif diff --git a/variants/xiao_rp2040/XiaoRP2040Board.h b/variants/xiao_rp2040/XiaoRP2040Board.h index bcc2d5cb6..3e4afd5e1 100644 --- a/variants/xiao_rp2040/XiaoRP2040Board.h +++ b/variants/xiao_rp2040/XiaoRP2040Board.h @@ -34,7 +34,11 @@ public: void attachDynamicPrefs(KeyValueStore* prefs) { (void)prefs; } // no-op #ifdef P_LORA_TX_LED - void onBeforeTransmit() override { digitalWrite(P_LORA_TX_LED, HIGH); } + void applyLedPolicy() override { + if (!mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, LOW); + } + + void onBeforeTransmit() override { if (mesh::leds::enabled()) digitalWrite(P_LORA_TX_LED, HIGH); } void onAfterTransmit() override { digitalWrite(P_LORA_TX_LED, LOW); } #endif diff --git a/variants/xiao_s3/platformio.ini b/variants/xiao_s3/platformio.ini index 0dc9ba618..3910f4ce4 100644 --- a/variants/xiao_s3/platformio.ini +++ b/variants/xiao_s3/platformio.ini @@ -18,6 +18,7 @@ build_flags = ${esp32_base.build_flags} -D P_LORA_MOSI=9 ; D10 -D PIN_USER_BTN=-1 ; NC -D PIN_STATUS_LED=21 ; Orange user led, LOW=On + -D LED_STATE_ON=LOW -D PIN_BOARD_SDA=D6 ; D6=43 -D PIN_BOARD_SCL=D7 ; D7=44 -D SX126X_RXEN=6 ; D5 diff --git a/variants/xiao_s3/target.cpp b/variants/xiao_s3/target.cpp index a7ae7dd8e..1b4e11a24 100644 --- a/variants/xiao_s3/target.cpp +++ b/variants/xiao_s3/target.cpp @@ -24,7 +24,8 @@ EnvironmentSensorManager sensors; bool radio_init() { fallback_clock.begin(); rtc_clock.begin(Wire); - pinMode(21, INPUT); + digitalWrite(PIN_STATUS_LED, !LED_STATE_ON); + pinMode(PIN_STATUS_LED, OUTPUT); pinMode(48, OUTPUT); #if defined(P_LORA_SCLK)