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)