Add saved LED controls across supported boards

Add get leds and set leds on|off for Companion, repeater, room server and
sensor builds. Preserve the setting across reboots and cover GPIO, RGB,
Bluetooth, notification and expander indicators without changing radio
amplifier controls or screen backlights.

RAK Companions blink while advertising and keep the Bluetooth LED off once
connected, including late timer callbacks.

Adapted from meshcore-dev/MeshCore#3605 and #2694.
Validated with host regressions and firmware builds for SenseCAP Solar,
RAK4631, T096, Heltec V4, T-Beam, Xiao S3, Pico W and Wio E5 Mini.

Co-authored-by: R0ck <develop@hectospark.co.uk>
Co-authored-by: Nick Dunklee <nick@dunklee.org>
This commit is contained in:
committed 2026-10-20 08:59:39 -07:00
1 parent c48e679284
commit 9d52b1fd68
91 files changed
+1485 -65

No files matched your search

+27
View File
@@ -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
+2
View File
@@ -1,4 +1,5 @@
#include <helpers/RegulatorSettings.h>
#include <helpers/LedSettings.h>
#include <helpers/ui/DisplayPowerSettings.h>
#include <helpers/BatteryChargeCLI.h>
#include <helpers/MorseCLI.h>
@@ -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)
@@ -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);
+2
View File
@@ -1,4 +1,5 @@
#include <helpers/RegulatorSettings.h>
#include <helpers/LedSettings.h>
#include <Arduino.h> // needed for PlatformIO
#include <helpers/ui/StartupScreen.h>
#include <helpers/ui/DisplayPowerSettings.h>
@@ -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");
}
@@ -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();
@@ -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 };
@@ -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();
+1 -1
View File
@@ -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
+2
View File
@@ -1,4 +1,5 @@
#include <helpers/RegulatorSettings.h>
#include <helpers/LedSettings.h>
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#include <helpers/IdentityGeneration.h>
@@ -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");
}
+2
View File
@@ -1,4 +1,5 @@
#include <helpers/RegulatorSettings.h>
#include <helpers/LedSettings.h>
#include <Arduino.h> // needed for PlatformIO
#include <Mesh.h>
#include <helpers/IdentityGeneration.h>
@@ -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");
}
+2
View File
@@ -1,4 +1,5 @@
#include <helpers/RegulatorSettings.h>
#include <helpers/LedSettings.h>
#include "SensorMesh.h"
#include <math.h>
#include <helpers/IdentityGeneration.h>
@@ -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");
}
+45
View File
@@ -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 <helpers/LedPolicy.h>\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")])
+3
View File
@@ -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 <Arduino.h>
#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 {
+2
View File
@@ -1,4 +1,5 @@
#include <helpers/RegulatorSettings.h>
#include <helpers/LedSettings.h>
#include <Arduino.h>
#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;
+13 -2
View File
@@ -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
+24
View File
@@ -0,0 +1,24 @@
#include "LedPolicy.h"
#include <Arduino.h>
#include <target.h>
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
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <atomic>
namespace mesh { namespace leds {
inline std::atomic<bool>& policy() {
static std::atomic<bool> 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
+107
View File
@@ -0,0 +1,107 @@
#include "LedSettings.h"
#include <MeshCore.h>
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#include <helpers/AtomicFileWriter.h>
#else
#include <helpers/ContactFileTransaction.h>
#endif
#include <stdio.h>
#include <string.h>
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<uint8_t>(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
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <helpers/IdentityStore.h>
#include <stddef.h>
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
+7
View File
@@ -6,6 +6,7 @@
#include "nrf52/SoftDeviceState.h"
#include "nrf52/SleepCycleState.h"
#include "nrf52/ShutdownLeds.h"
#include "nrf52/BluetoothLed.h"
#include <target.h>
#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();
+1
View File
@@ -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
+15 -1
View File
@@ -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
+50
View File
@@ -0,0 +1,50 @@
#pragma once
#include "../LedPolicy.h"
#include "ShutdownLeds.h"
#include <bluefruit.h>
namespace mesh_nrf52 {
inline bool& bluetoothLedReady() {
static bool ready = false;
return ready;
}
inline void applyBluetoothLedPolicy() {
#ifdef LED_BLUE
if (LED_BLUE < 0 || static_cast<uint32_t>(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
+3 -5
View File
@@ -4,6 +4,7 @@
#include "SoftDeviceState.h"
#include "SleepCycleState.h"
#include "BleDebugTrace.h"
#include "BluetoothLed.h"
#include <stdio.h>
#include <string.h>
#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) {
+5 -1
View File
@@ -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
+19
View File
@@ -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);
+1
View File
@@ -31,6 +31,7 @@
#define pdMS_TO_TICKS(ms) (ms)
#include <helpers/nrf52/ShutdownLeds.h>
#include <helpers/LedPolicy.h>
std::vector<Event> events;
int levels[PINS_COUNT];
+188
View File
@@ -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 <cassert>
#include <helpers/LedPolicy.h>
#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()
+1
View File
@@ -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 {
+2
View File
@@ -30,12 +30,14 @@ inline uint32_t analogReadMilliVolts(int) { return 830; }
BASE_BOARD = r'''
#pragma once
#include <Arduino.h>
#include <helpers/LedPolicy.h>
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() {}
+229
View File
@@ -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 <cstdint>
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 <cassert>
#include <cstring>
#include <string>
#include <helpers/IdentityStore.h>
#include <MeshCore.h>
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
#define ATOMIC_FILE_WRITER_IMPLEMENTATION
#include <helpers/AtomicFileWriter.h>
#undef ATOMIC_FILE_WRITER_IMPLEMENTATION
#endif
#include <helpers/LedSettings.cpp>
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()
+2 -1
View File
@@ -24,6 +24,7 @@ RGB = "defined(PIN_STATUS_LED_R) && defined(PIN_STATUS_LED_G) && defined(PIN_STA
HARNESS = r'''
#pragma once
#include <helpers/CompanionNotificationPolicy.h>
#include <helpers/LedPolicy.h>
#include <cstdint>
#include <cstring>
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")
+14
View File
@@ -13,6 +13,13 @@ ROOT = Path(__file__).resolve().parents[1]
RTOS = r'''
#pragma once
#include <cstdint>
#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<class... T> void clearBonds(T...) {}
template<class... T> void setMITM(T...) {}
template<class... T> 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<class T> void setEventCallback(T) {}
} Bluefruit;
#include <helpers/nrf52/BluetoothLed.h>
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"
+301
View File
@@ -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 <cassert>
#include <cstdint>
#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 <Arduino.h>
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 <helpers/nrf52/BluetoothLed.h>
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 <helpers/LedPolicy.h>\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()
+134
View File
@@ -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 <cassert>
#include <vector>
#define LOW 0
#define HIGH 1
#define INPUT 0
#define OUTPUT 1
struct Event { int kind, pin, value; };
extern std::vector<Event> 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 <Arduino.h>
#include <target.h>
#include <helpers/LedPolicy.h>
std::vector<Event> 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 <Arduino.h>
#include <target.h>
std::vector<Event> events;
struct Clock { template<class... T> 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()
+3
View File
@@ -231,6 +231,7 @@ MOCK_SDK = r'''
#pragma once
#include <cassert>
#include <cstdint>
#include <helpers/LedPolicy.h>
#include <map>
#include <string>
#include <vector>
@@ -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;
+2
View File
@@ -114,9 +114,11 @@ inline TwoWire Wire;
ESP_BOARD = r'''
#pragma once
#include <Arduino.h>
#include <helpers/LedPolicy.h>
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() {}
@@ -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 {
@@ -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 {
@@ -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 {
+1 -1
View File
@@ -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() {
+6 -1
View File
@@ -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();
}
+1
View File
@@ -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;
+6 -1
View File
@@ -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() {
+1
View File
@@ -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;
+6 -1
View File
@@ -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
@@ -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();
}
@@ -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;
@@ -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();
}
+1 -1
View File
@@ -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();
}
+1 -1
View File
@@ -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();
}
@@ -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
+6 -1
View File
@@ -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);
@@ -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);
+6 -1
View File
@@ -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
@@ -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
+5 -1
View File
@@ -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) {
+1
View File
@@ -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;
+11 -7
View File
@@ -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";
}
};
};
@@ -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();
}
@@ -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;
@@ -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) {
@@ -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);
+6 -1
View File
@@ -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
+1 -1
View File
@@ -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));
@@ -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
+6 -1
View File
@@ -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);
+5 -1
View File
@@ -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
@@ -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
+5 -1
View File
@@ -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 {
+6 -1
View File
@@ -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
+1 -1
View File
@@ -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));
+6 -1
View File
@@ -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
+6 -1
View File
@@ -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
+7 -2
View File
@@ -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));
+5 -1
View File
@@ -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
+6 -1
View File
@@ -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
+5 -1
View File
@@ -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);
+6 -1
View File
@@ -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
@@ -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
@@ -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
+6 -1
View File
@@ -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
@@ -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()));
}
}
@@ -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);
+1 -1
View File
@@ -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);
+7 -1
View File
@@ -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);
+1 -1
View File
@@ -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);
+7 -2
View File
@@ -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
+5 -1
View File
@@ -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
+1
View File
@@ -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
+2 -1
View File
@@ -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)