Merge remote-tracking branch 'origin/keymindCascade' into keymindCascade

This commit is contained in:
mikecarper committed 2026-10-20 10:42:48 -07:00
commit bc1176c7f4
119 files changed
+3288 -173

No files matched your search

+8
View File
@@ -972,7 +972,14 @@ jobs:
python3 -B test/test_ui_startup_windows.py -v
node test/test_notification_builder.js
python3 -B test/test_notification_settings.py
python3 -B test/test_companion_alert_control.py -v
python3 -B test/test_companion_compact_led_control.py -v
python3 -B test/test_companion_alert_drivers.py -v
python3 -B test/test_notification_led_ownership.py -v
python3 -B test/test_led_settings.py -v
python3 -B test/test_board_led_policy.py -v
python3 -B test/test_status_led_policy.py -v
python3 -B test/test_nrf52_led_policy.py -v
python3 -B test/test_mono_led_rollover.py -v
python3 -B test/test_backlight_poll_rollover.py -v
python3 -B test/test_rgb_led_rollover.py -v
@@ -990,6 +997,7 @@ jobs:
python3 -B test/test_legacy_ui_version_bounds.py -v
python3 -B test/test_infrastructure_ui_version.py -v
python3 -B test/test_ssd1306_power.py -v
python3 -B test/test_oled_display_probe.py -v
python3 -B test/test_companion_inbox.py
python3 -B test/test_heltec_r8_external_power.py
python3 -B test/test_bluetooth_mac_contract.py
+59
View File
@@ -1059,6 +1059,19 @@ set display.rotation 270
The values are clockwise degrees. `0` clears the override and restores the
board's compiled default. Unsupported display drivers return an error.
RAK4631 and RAK3401 images detect SSD1306 or SH1106 OLED controllers when
initializing the screen. This lets the same RAK4631 image serve WisMesh Pocket
v2 and v3. Both retain rotation support and use one 1 KiB framebuffer.
Detection checks the configured `DISPLAY_ADDRESS` (default `0x3C`). Only
repeated SH1106 status readings select that controller; ambiguous responses
keep SSD1306, preserving compatibility with older RAK1921 panels. For panels
that cannot identify themselves, build with `OLED_CONTROLLER_OVERRIDE=1`
(SSD1306) or `=2` (SH1106); `=0` restores automatic detection.
See [the Pocket v3 issue](https://github.com/meshcore-dev/MeshCore/issues/3296)
and [the RAK1921 detection caveat](https://github.com/meshtastic/firmware/pull/11265).
---
## Show touchscreen touch areas
@@ -1958,6 +1971,52 @@ the verified pin assignments, startup fixes, and saved amplifier settings.
---
#### Turn Companion sound and vibration on or off
`get buzzer` and `set buzzer on|off` control the Companion's buzzer. Devices
with a vibration motor also support `get vibration` and `set vibration on|off`.
Changes take effect immediately, are saved across reboots, and produce no
confirmation tone. Muting an output stops an alert already playing.
Fresh settings default to **sound off** and **vibration on**. Existing saved
choices are retained. The same preferences are used by the device buttons and
UI. Unsupported hardware reports an error.
These are the device mute controls. Programmable `notify.sound` and
`notify.vibration` rules also respect them; an enabled rule cannot bypass a
muted output. The notification output masks still select which alert outputs
a rule may use. Explicit button mute/unmute confirmations retain their existing
behavior.
Adapted from [upstream PR #3603](https://github.com/meshcore-dev/MeshCore/pull/3603).
#### 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
@@ -68,6 +68,7 @@ public:
virtual void notificationVibration(bool on) { (void)on; }
virtual void notificationMelody(const char* text) { (void)text; }
virtual void notificationScreen(int8_t mode) { (void)mode; }
virtual void syncAlertPreferences() {}
void setShutdownGuard(UIShutdownGuard* guard) { _shutdownGuard = guard; }
void setHasConnection(bool connected) { _connected = connected; }
bool hasConnection() const { return _interfaceManager->isConnected(); }
+63 -60
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>
@@ -293,7 +294,7 @@ static bool save_filter(const ContactInfo& c);
#define DEFAULT_AUTOADD_CONFIG 0
#endif
#ifndef DEFAULT_BUZZER_QUIET
#define DEFAULT_BUZZER_QUIET 0
#define DEFAULT_BUZZER_QUIET 1
#endif
#ifndef DEFAULT_CAD_ENABLED
// Preserve the tuned Companion behavior that preceded the runtime setting.
@@ -2832,6 +2833,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_prefs.manual_add_contacts = DEFAULT_MANUAL_ADD_CONTACTS;
_prefs.tx_power_dbm = LORA_TX_POWER;
_prefs.buzzer_quiet = DEFAULT_BUZZER_QUIET ? 1 : 0;
_prefs.vibe_quiet = 0;
_prefs.gps_enabled = 0; // GPS disabled by default
_prefs.gps_interval = 0; // Use the default 1-second fix-processing interval
_prefs.gps_sync_interval_hours = 0; // Retain the board's default time-sync cadence
@@ -3319,6 +3321,13 @@ void MyMesh::begin(bool has_display, bool radio_available,
void MyMesh::configureRadioFromPrefs() {
board.attachDynamicPrefs(_prefs.getCustom());
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)
// The reserved preference byte keeps the existing image and filesystem size.
_prefs.leds_quiet = _prefs.leds_quiet == 1 ? 1 : 0;
mesh::leds::setEnabled(!_prefs.leds_quiet);
board.applyLedPolicy();
mesh::leds::applyStatusPolicy();
#endif
if (!_radio_available) {
saved_radio_apply_pending = false;
@@ -10314,17 +10323,17 @@ void MyMesh::handleTerminalCommand(char* command, bool reader_prompt,
terminalOutput().print(
"Commands:\r\n"
" stats-core / stats-radio / stats-radio-diag / stats-packets\r\n"
" get prv.key (when private key export is enabled)\r\n"
" get prv.key (if export enabled)\r\n"
#if defined(ESP32_PLATFORM) && defined(WIFI_SSID)
" password <new-password> (shared setup WiFi, WebConfig and WiFi OTA)\r\n"
" get password (management password status)\r\n"
#else
" get password (this role has no admin password)\r\n"
" get password (no admin password)\r\n"
#endif
" erase (erase stored settings and identity)\r\n"
" erase (settings and identity)\r\n"
" board\r\n"
" version\r\n");
terminalOutput().print(" get storage.layout\r\n");
" version\r\n"
" get storage.layout\r\n");
#if MESH_ENABLE_TX_BEACONS
terminalOutput().print(" get beacon [slot] / set beacon <slot> <meshcore|meshtext> ...\r\n"
" delete beacon <slot|all> / set beacon.interval <12|24|36|48> (default 24h)\r\n");
@@ -10362,58 +10371,52 @@ void MyMesh::handleTerminalCommand(char* command, bool reader_prompt,
terminalOutput().print(" get eth / set eth on|off (physical RAK13800; default off, this boot)\r\n"
" get sdcard [free] / ota cache [on|off] (RAK15002; mutually exclusive with Ethernet)\r\n");
#endif
terminalOutput().print(" get pwrmgt.bootreason\r\n");
terminalOutput().print(" get pwrmgt.bootreason\r\n"
#if COMPANION_FEATURE_MEMORY_DIAGNOSTICS
terminalOutput().print(" memory\r\n");
" memory\r\n"
#endif
" get/set display.rotation <0|90|180|270>\r\n"
#ifdef PIN_BUZZER
" get/set buzzer on|off\r\n"
#endif
#if defined(PIN_VIBRATION) || defined(HAS_DRV2605)
" get/set vibration on|off\r\n"
#endif
terminalOutput().print(" get display.rotation\r\n");
#if COMPANION_FEATURE_NOTIFICATIONS
terminalOutput().print(" get notify / get notify.rules / get notify.gpio.pins\r\n");
terminalOutput().print(" set notify.{sound|vibration|led|screen|gpio} on|off\r\n");
terminalOutput().print(" set notify.<output> <all|contact:key|room:key|channel:slot> <pattern>\r\n");
terminalOutput().print(" set notify.remote <contact:key|room:key> on|off (!notify, max 15s, no GPIO)\r\n");
terminalOutput().print(" notify.test <target> / notify.stop / notify.delete <target>\r\n");
" get notify / get notify.rules / get notify.gpio.pins\r\n"
" set notify.{sound|vibration|led|screen|gpio} on|off\r\n"
" set notify.<output> <all|contact:key|room:key|channel:slot> <pattern>\r\n"
" set notify.remote <contact:key|room:key> on|off (!notify, max 15s, no GPIO)\r\n"
" notify.test <target> / notify.stop / notify.delete <target>\r\n"
#endif
terminalOutput().print(
" get display.inbox\r\n"
" set display.inbox <history|pending|unread>\r\n"
" set display.rotation <0|90|180|270>\r\n"
" get display.touch\r\n"
" set display.touch <on|off> (this boot only)\r\n"
" get public.key\r\n");
terminalOutput().print(
" get/set display.inbox <history|pending|unread>\r\n"
" get/set display.touch <on|off> (this boot)\r\n"
" get public.key\r\n"
" set {name|lat|lon|freq|tx|af} {value}\r\n"
" get wifi|espnow|2.4ghz\r\n"
" set wifi|espnow on|off [force] (force only with off)\r\n"
" set 2.4ghz on [all|force]|off [force] (this boot)\r\n"
" get bluetooth.name\r\n"
" set bluetooth.name <name|default>\r\n");
" get/set bluetooth.name <name|default>\r\n"
#if defined(BLE_PIN_CODE)
terminalOutput().print(" get bluetooth (alias: get ble)\r\n");
terminalOutput().print(" set bluetooth <on|off> [force] (saved; force only with off)\r\n");
terminalOutput().print(" get bluetooth.mac\r\n");
terminalOutput().print(
" set bluetooth.mac <address|random|random-every-boot|random-after-connect|default>\r\n");
terminalOutput().print(" get bluetooth.stealth\r\n");
terminalOutput().print(" set bluetooth.stealth on|off\r\n");
" get bluetooth (alias: get ble)\r\n"
" set bluetooth <on|off> [force] (saved; force only with off)\r\n"
" get bluetooth.mac\r\n"
" set bluetooth.mac <address|random|random-every-boot|random-after-connect|default>\r\n"
" get/set bluetooth.stealth on|off\r\n"
#endif
#if MESH_ENABLE_LOST_REPLY
terminalOutput().print(" get lost.reply\r\n" " set lost.reply <off|no|yes>\r\n");
" get/set lost.reply <off|no|yes>\r\n"
#endif
terminalOutput().print(" set pin <0-999999>\r\n" " powersaving [on|off]\r\n");
" set pin <0-999999>\r\n" " powersaving [on|off]\r\n"
#if MESH_USB_LOGGING_AVAILABLE
terminalOutput().print(
" get usb.logging\r\n"
" set usb.logging <on|off|stream> [reboot]\r\n"
" get usb.debug\r\n"
" set usb.debug <on|off>\r\n"
" get/set usb.logging <on|off|stream> [reboot]\r\n"
" get/set usb.debug <on|off>\r\n"
#if MESH_USB_CONSOLE_COOPERATIVE
" get usb.watchdog\r\n"
" get usb.watchdog.last\r\n"
" set usb.watchdog <off|on|auto> (saved; default auto)\r\n"
" get/set usb.watchdog <off|on|auto> (saved; default auto)\r\n"
#endif
#endif
);
#endif
#if defined(ESP32) && defined(WIFI_SSID)
#if defined(COMPANION_RADIO_FULL)
terminalOutput().print(" start ota [ap] (WiFi firmware update on port 8080)\r\n");
@@ -10446,23 +10449,15 @@ void MyMesh::handleTerminalCommand(char* command, bool reader_prompt,
terminalOutput().print(" get espnow.channel\r\n");
terminalOutput().print(" set espnow.channel <1-13>\r\n");
#endif
// Keep identical help text in bounded chunks. Fewer virtual Print calls
// leave room for USB controls on small-flash boards without cutting CLI.
// Concise help and grouped Print calls preserve small-board flash space.
terminalOutput().print(
" get radio.rxps\r\n"
" get radio.rxps.config\r\n"
" set radio.rxps <off|on|level 1-10 [preamble 16|32]|rx_us sleep_us>\r\n"
" get radio.cad\r\n"
" set radio.cad <on|off>\r\n"
" set radio.cad timings <scan_ms|auto> <retry_ms|auto> <max_ms|auto>\r\n");
terminalOutput().print(
" get radio.rxgain\r\n"
" set radio.rxgain <on|off>\r\n"
" get radio.fem.rxgain\r\n"
" set radio.fem.rxgain <on|off>\r\n"
" get radio.fem.txgain\r\n"
" set radio.fem.txgain <on|off>\r\n");
terminalOutput().print(
" get/set radio.rxps <off|on|level 1-10 [preamble 16|32]|rx_us sleep_us>\r\n"
" get/set radio.cad <on|off>\r\n"
" set radio.cad timings <scan_ms|auto> <retry_ms|auto> <max_ms|auto>\r\n"
" get/set radio.rxgain <on|off>\r\n"
" get/set radio.fem.rxgain <on|off>\r\n"
" get/set radio.fem.txgain <on|off>\r\n"
" get/set flood.retry.count <0-15>\r\n"
" get/set flood.retry.path <0-63|off>\r\n"
" get/set flood.retry.group.path <0-63|off>\r\n"
@@ -10474,13 +10469,13 @@ void MyMesh::handleTerminalCommand(char* command, bool reader_prompt,
terminalOutput().print(
" list [n]\r\n"
" show [adverts|channels|emergency] [on|off]\r\n"
" to [recipient name or prefix]\r\n"
" path [direct|clear|hops separated by spaces or commas]\r\n"
" to [name|prefix]\r\n"
" path [direct|clear|hop,...]\r\n"
" send <text>\r\n"
" login <admin-password>\r\n"
" cmd <remote-command>\r\n");
terminalOutput().print(
" trace [recipient name or prefix]\r\n"
" trace [name|prefix]\r\n"
" trace path <1|2|4> <prefixes...>\r\n"
" advert\r\n"
" reset path\r\n"
@@ -10507,7 +10502,7 @@ void MyMesh::handleTerminalCommand(char* command, bool reader_prompt,
if (_terminal_mode) {
terminalOutput().print(" +++MESHCORE-TERM-STOP\r\n");
} else {
terminalOutput().print(" disconnect (closes the network terminal)\r\n");
terminalOutput().print(" disconnect (network terminal)\r\n");
}
} else if (handleCommand(command, 0, local_reply)) {
// Fill the same safe shared-command surface for getters (`get name`,
@@ -10725,6 +10720,14 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
}
if (mesh::power::handleRegulatorCommand(command, reply, reply_capacity)) return true;
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)
if (handleCompactLedCommand(command, reply, reply_capacity)) return true;
#else
if (mesh::leds::handleCommand(command, reply, reply_capacity)) return true;
#endif
#if defined(PIN_BUZZER) || defined(PIN_VIBRATION) || defined(HAS_DRV2605)
if (handleAlertCommand(command, reply, reply_capacity)) return true;
#endif
#if COMPANION_FEATURE_READER
if (sender_timestamp == 0 && mesh::morseTimingActive(_radio)
+4
View File
@@ -295,6 +295,10 @@ public:
void setNotificationOutputMute(mesh::notify::Output, bool) {}
#endif
bool handleNotificationCommand(const char* command, char* reply, size_t size);
bool handleAlertCommand(const char* command, char* reply, size_t size);
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)
bool handleCompactLedCommand(const char* command, char* reply, size_t size);
#endif
#if defined(STM32_PLATFORM)
// Keep this large dispatcher separate from contact streaming on flash-
// constrained STM32 targets; merging their branches increases code size.
+5 -1
View File
@@ -49,8 +49,12 @@ public:
float rx_delay_base = 0;
uint32_t ble_pin = 0;
uint8_t advert_loc_policy = 0;
uint8_t buzzer_quiet = 0;
uint8_t buzzer_quiet = 1;
uint8_t gps_enabled = 0; // GPS enabled flag (0=disabled, 1=enabled)
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)
// Uses existing alignment space; stored in the reserved wire byte at 79.
uint8_t leds_quiet = 0;
#endif
uint32_t gps_interval = 0; // seconds; 0 uses the 1-second default
uint8_t autoadd_config = 0; // bitmask for auto-add contacts config
uint8_t rx_boosted_gain = 0; // SX126x RX boosted gain mode
@@ -2,6 +2,88 @@
#include "target.h"
#include "NotificationSettingsFile.h"
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)
bool MyMesh::handleCompactLedCommand(const char* command, char* reply, size_t size) {
if (!command || !reply || !size) return false;
if (strcmp(command, "get leds") == 0) {
snprintf(reply, size, "> %s", mesh::leds::enabled() ? "on" : "off");
return true;
}
if (strncmp(command, "set leds", 8) != 0
|| (command[8] && command[8] != ' ' && command[8] != '\t')) return false;
const char* value = command + 8;
while (*value == ' ' || *value == '\t') ++value;
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
snprintf(reply, size, "Error: usage set leds on|off");
return true;
}
const bool on = strcmp(value, "on") == 0;
const uint8_t previous = _prefs.leds_quiet;
_prefs.leds_quiet = !on;
if (_prefs.leds_quiet != previous) {
if (!savePrefs()) {
_prefs.leds_quiet = previous;
snprintf(reply, size, "Error: LED setting save unconfirmed; reboot may differ");
return true;
}
}
mesh::leds::setEnabled(on);
board.applyLedPolicy();
mesh::leds::applyStatusPolicy();
snprintf(reply, size, "OK");
return true;
}
#endif
bool MyMesh::handleAlertCommand(const char* command, char* reply, size_t size) {
if (!command || !reply || !size) return false;
const bool get = strncmp(command, "get ", 4) == 0;
if (!get && strncmp(command, "set ", 4) != 0) return false;
const char* name = command + 4;
uint8_t* quiet = nullptr;
size_t length = 0;
bool supported = false;
if (strncmp(name, "buzzer", 6) == 0) {
length = 6;
quiet = &_prefs.buzzer_quiet;
#ifdef PIN_BUZZER
supported = true;
#endif
} else if (strncmp(name, "vibration", 9) == 0) {
length = 9;
quiet = &_prefs.vibe_quiet;
#if defined(PIN_VIBRATION) || defined(HAS_DRV2605)
supported = true;
#endif
} else return false;
const char* value = name + length;
if (*value && *value != ' ' && *value != '\t') return false;
if (!supported) {
snprintf(reply, size, "Error: %.*s unavailable on this build", int(length), name);
return true;
}
if (get) {
if (*value) snprintf(reply, size, "Error: usage get %.*s", int(length), name);
else snprintf(reply, size, "> %s", *quiet ? "off" : "on");
return true;
}
while (*value == ' ' || *value == '\t') ++value;
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
snprintf(reply, size, "Error: usage set %.*s on|off", int(length), name);
return true;
}
const uint8_t previous = *quiet;
*quiet = strcmp(value, "off") == 0;
if (*quiet != previous && !savePrefs()) {
*quiet = previous;
snprintf(reply, size, "Error: failed to save %.*s setting", int(length), name);
return true;
}
if (_ui) _ui->syncAlertPreferences();
snprintf(reply, size, "OK");
return true;
}
#if COMPANION_FEATURE_NOTIFICATIONS
// The UI's STATUS_LED_RGB define is local to its own translation unit.
uint8_t MyMesh::capabilities() const {
@@ -20,6 +102,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);
@@ -67,7 +67,11 @@ static constexpr PrefField PREF_FIELDS[] = {
PREF_FIELD(advert_loc_policy, 1),
PREF_FIELD(multi_acks, 1),
PREF_FIELD(path_hash_mode, 1),
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)
PREF_FIELD(leds_quiet, 1),
#else
prefSpecial<PrefsPad, 1>(),
#endif
PREF_FIELD(ble_pin, 4),
PREF_FIELD(buzzer_quiet, 1),
PREF_FIELD(gps_enabled, 1),
+4
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>
@@ -3633,6 +3634,9 @@ void setup() {
#if defined(NRF52_PLATFORM)
if (!mesh::storage::internalPrimaryFilesystemReady(primary_fs_boot)
|| store.isVolatilePrimaryFS()) regulator_prefs_fs = nullptr;
#endif
#if !defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)
mesh::leds::beginSettings(board, regulator_prefs_fs);
#endif
if (!mesh::power::beginRegulatorSettings(board, radio_driver, regulator_prefs_fs)) {
MESH_DEBUG_PRINTLN("Regulator selection could not be applied");
+18 -6
View File
@@ -1965,6 +1965,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, CompanionNode
#if defined(PIN_VIBRATION) || defined(HAS_DRV2605)
vibration.begin();
vibration.quiet(_node_prefs->vibe_quiet);
#endif
ui_started_at = millis();
@@ -2051,7 +2052,7 @@ if (_notification_outputs & mesh::notify::Sound) switch(t){
}
#endif
#ifdef PIN_VIBRATION
#if defined(PIN_VIBRATION) || defined(HAS_DRV2605)
// Trigger vibration for all UI events except none
if ((_notification_outputs & mesh::notify::Vibration) && t != UIEventType::none) {
vibration.trigger();
@@ -2105,6 +2106,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();
@@ -2275,7 +2277,20 @@ bool UITask::isButtonGesturePending() const {
#endif
}
void UITask::syncAlertPreferences() {
if (!_node_prefs) return;
#ifdef PIN_BUZZER
if (buzzer.isQuiet() != bool(_node_prefs->buzzer_quiet))
buzzer.quiet(_node_prefs->buzzer_quiet);
#endif
#if defined(PIN_VIBRATION) || defined(HAS_DRV2605)
if (vibration.isQuiet() != bool(_node_prefs->vibe_quiet))
vibration.quiet(_node_prefs->vibe_quiet);
#endif
}
void UITask::loop() {
syncAlertPreferences();
// Observe startup expiry even while display/Morse/input defers rendering.
if (!_startup_complete && static_cast<uint32_t>(millis() - ui_started_at) >= 8000u) {
_startup_complete = true;
@@ -2729,7 +2744,7 @@ uint8_t UITask::notificationCapabilities() const {
}
void UITask::notificationMelody(const char* text) {
#ifdef PIN_BUZZER
if (text) buzzer.playNotification(text); else buzzer.stop();
if (text) buzzer.playNotification(text); else buzzer.stopNotification();
#endif
}
void UITask::notificationScreen(int8_t mode) {
@@ -2738,10 +2753,7 @@ void UITask::notificationScreen(int8_t mode) {
_next_refresh = millis();
}
void UITask::notificationVibration(bool on) {
#ifdef HAS_DRV2605
#if defined(PIN_VIBRATION) || defined(HAS_DRV2605)
vibration.pulse(on);
#elif defined(PIN_VIBRATION)
vibration.stop();
digitalWrite(PIN_VIBRATION, on ? HIGH : LOW);
#endif
}
+2 -1
View File
@@ -43,7 +43,7 @@ class UITask : public AbstractUITask {
unsigned long _pairing_screen_until;
bool _deferred_msg_preview;
bool _startup_complete = false;
CompanionNodePrefs* _node_prefs;
CompanionNodePrefs* _node_prefs = nullptr;
char _alert[80];
uint32_t _alert_expiry;
int _msgcount;
@@ -200,6 +200,7 @@ public:
void notificationVibration(bool on) override;
void notificationMelody(const char* text) override;
void notificationScreen(int8_t mode) override;
void syncAlertPreferences() override;
void loop() override;
void shutdown(bool restart = false);
+19 -10
View File
@@ -124,11 +124,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, CompanionNode
buzzer.startup();
#endif
#ifdef HAS_DRV2605
#if defined(PIN_VIBRATION) || defined(HAS_DRV2605)
vibration.begin();
vibration.quiet(_node_prefs->vibe_quiet);
#elif defined(PIN_VIBRATION)
vibration.begin();
#endif
// Initialize digital button if available
@@ -451,6 +449,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 +469,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 };
@@ -717,13 +717,26 @@ void UITask::servicePairingState() {
}
}
void UITask::syncAlertPreferences() {
if (!_node_prefs) return;
#ifdef PIN_BUZZER
if (buzzer.isQuiet() != bool(_node_prefs->buzzer_quiet))
buzzer.quiet(_node_prefs->buzzer_quiet);
#endif
#if defined(PIN_VIBRATION) || defined(HAS_DRV2605)
if (vibration.isQuiet() != bool(_node_prefs->vibe_quiet))
vibration.quiet(_node_prefs->vibe_quiet);
#endif
}
void UITask::loop() {
syncAlertPreferences();
// Observe startup expiry before button callbacks and optional rendering.
if (!_startup_complete && static_cast<uint32_t>(millis() - ui_started_at) >= 8000u) {
_startup_complete = true;
}
const bool morse_timing = mesh::morseTimingActive(&radio_driver);
#ifdef PIN_VIBRATION
#if defined(PIN_VIBRATION) || defined(HAS_DRV2605)
vibration.loop();
#endif
servicePairingState();
@@ -1130,18 +1143,14 @@ void UITask::notificationScreen(int8_t mode) {
_need_refresh = true;
}
void UITask::notificationVibration(bool on) {
#ifdef HAS_DRV2605
#if defined(PIN_VIBRATION) || defined(HAS_DRV2605)
#if defined(PIN_BUZZER) && MESH_GPS_VOICE
// The saved physical alert mode stays authoritative even if the separate
// notification-mask write fails. Off callbacks must still stop the driver.
// Keep the committed physical mode authoritative if a mask save failed.
if (on && _node_prefs && _node_prefs->vibe_quiet) {
vibration.stop();
return;
}
#endif
vibration.pulse(on);
#elif defined(PIN_VIBRATION)
vibration.stop();
digitalWrite(PIN_VIBRATION, on ? HIGH : LOW);
#endif
}
+2 -1
View File
@@ -32,7 +32,7 @@ class UITask : public AbstractUITask {
GenericVibration vibration;
#endif
uint32_t _next_refresh;
CompanionNodePrefs* _node_prefs;
CompanionNodePrefs* _node_prefs = nullptr;
char _version_info[32];
char _origin[62];
#if UI_SMALL_MESSAGE_FONT == 1
@@ -125,6 +125,7 @@ public:
void notificationVibration(bool on) override;
void notificationMelody(const char* text) override;
void notificationScreen(int8_t mode) override;
void syncAlertPreferences() override;
void loop() override;
#ifdef MESH_BUTTON_AUDIO_HIL
bool handleButtonAudioTest(const char* command, char* reply, size_t size) override;
+18 -6
View File
@@ -666,6 +666,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, CompanionNode
#if defined(PIN_VIBRATION) || defined(HAS_DRV2605)
vibration.begin();
vibration.quiet(_node_prefs->vibe_quiet);
#endif
ui_started_at = millis();
@@ -708,7 +709,7 @@ if (_notification_outputs & mesh::notify::Sound) switch(t){
}
#endif
#ifdef PIN_VIBRATION
#if defined(PIN_VIBRATION) || defined(HAS_DRV2605)
// Trigger vibration for all UI events except none
if ((_notification_outputs & mesh::notify::Vibration) && t != UIEventType::none) {
vibration.trigger();
@@ -751,6 +752,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();
@@ -866,7 +868,20 @@ bool UITask::isButtonPressed() const {
#endif
}
void UITask::syncAlertPreferences() {
if (!_node_prefs) return;
#ifdef PIN_BUZZER
if (buzzer.isQuiet() != bool(_node_prefs->buzzer_quiet))
buzzer.quiet(_node_prefs->buzzer_quiet);
#endif
#if defined(PIN_VIBRATION) || defined(HAS_DRV2605)
if (vibration.isQuiet() != bool(_node_prefs->vibe_quiet))
vibration.quiet(_node_prefs->vibe_quiet);
#endif
}
void UITask::loop() {
syncAlertPreferences();
// Observe startup expiry even while display/Morse/input defers rendering.
if (!_startup_complete && static_cast<uint32_t>(millis() - ui_started_at) >= 8000u) {
_startup_complete = true;
@@ -1166,7 +1181,7 @@ uint8_t UITask::notificationCapabilities() const {
}
void UITask::notificationMelody(const char* text) {
#ifdef PIN_BUZZER
if (text) buzzer.playNotification(text); else buzzer.stop();
if (text) buzzer.playNotification(text); else buzzer.stopNotification();
#endif
}
void UITask::notificationScreen(int8_t mode) {
@@ -1175,10 +1190,7 @@ void UITask::notificationScreen(int8_t mode) {
_next_refresh = millis();
}
void UITask::notificationVibration(bool on) {
#ifdef HAS_DRV2605
#if defined(PIN_VIBRATION) || defined(HAS_DRV2605)
vibration.pulse(on);
#elif defined(PIN_VIBRATION)
vibration.stop();
digitalWrite(PIN_VIBRATION, on ? HIGH : LOW);
#endif
}
+2 -1
View File
@@ -40,7 +40,7 @@ class UITask : public AbstractUITask {
GenericVibration vibration;
#endif
uint32_t _next_refresh;
CompanionNodePrefs* _node_prefs;
CompanionNodePrefs* _node_prefs = nullptr;
char _alert[80];
uint32_t _alert_expiry;
unsigned long _pairing_screen_until;
@@ -137,6 +137,7 @@ public:
void notificationVibration(bool on) override;
void notificationMelody(const char* text) override;
void notificationScreen(int8_t mode) override;
void syncAlertPreferences() override;
void loop() override;
void shutdown(bool restart = false);
+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
+102
View File
@@ -0,0 +1,102 @@
#pragma once
#include <Adafruit_SSD1306.h>
#include <stdlib.h>
#include "OLEDDisplayProbe.h"
// Keep Adafruit's drawing, rotation and single 1 KiB framebuffer. SH1106 uses
// page writes into 132-column RAM, with the visible 128 columns starting at 2.
class AutoOLEDBackend : public Adafruit_SSD1306 {
mesh::ui::OLEDController _controller = mesh::ui::OLEDController::Absent;
bool _ready = false;
bool writeCommands(const uint8_t* commands, uint8_t count) {
wire->beginTransmission(i2caddr);
const bool control = wire->write(uint8_t(0x00)) == 1;
const bool payload = wire->write(commands, count) == count;
return wire->endTransmission() == 0 && control && payload;
}
public:
using Adafruit_SSD1306::Adafruit_SSD1306;
bool begin(uint8_t vcc, uint8_t address, bool reset, bool periphBegin) {
_ready = false;
i2caddr = address;
if (periphBegin) wire->begin();
if (reset && rstPin >= 0) {
pinMode(rstPin, OUTPUT);
digitalWrite(rstPin, HIGH);
delay(1);
digitalWrite(rstPin, LOW);
delay(10);
digitalWrite(rstPin, HIGH);
}
#if defined(OLED_CONTROLLER_OVERRIDE) && OLED_CONTROLLER_OVERRIDE != 0
static_assert(OLED_CONTROLLER_OVERRIDE == 1 || OLED_CONTROLLER_OVERRIDE == 2,
"OLED_CONTROLLER_OVERRIDE must be 0 (auto), 1 (SSD1306) or 2 (SH1106)");
wire->beginTransmission(address);
_controller = wire->endTransmission() != 0 ? mesh::ui::OLEDController::Absent
: OLED_CONTROLLER_OVERRIDE == 2 ? mesh::ui::OLEDController::SH1106
: mesh::ui::OLEDController::SSD1306;
#else
_controller = mesh::ui::detectOLEDController(*wire, address);
#endif
if (_controller == mesh::ui::OLEDController::Absent) return false;
if (_controller == mesh::ui::OLEDController::SSD1306) {
_ready = Adafruit_SSD1306::begin(vcc, address, false, false);
return _ready;
}
if (!buffer && !(buffer = static_cast<uint8_t*>(malloc(WIDTH * ((HEIGHT + 7) / 8)))))
return false;
vccstate = vcc;
clearDisplay();
// SH1106G initialization, matching Adafruit_SH1106G::begin().
static const uint8_t init[] = {
0xAE, 0xD5, 0x80, 0xA8, 0x3F, 0xD3, 0x00, 0x40,
0xAD, 0x8B, 0xA1, 0xC8, 0xDA, 0x12, 0x81, 0xFF,
0xD9, 0x1F, 0xDB, 0x40, 0x33, 0xA6, 0x20, 0x10, 0xA4
};
wire->setClock(wireClk);
const bool initialized = writeCommands(init, sizeof(init));
wire->setClock(restoreClk);
if (!initialized) return false;
delay(100);
const uint8_t on = 0xAF;
wire->setClock(wireClk);
_ready = writeCommands(&on, 1);
wire->setClock(restoreClk);
return _ready;
}
mesh::ui::OLEDController controller() const { return _controller; }
void ssd1306_command(uint8_t command) {
if (_ready) Adafruit_SSD1306::ssd1306_command(command);
}
void display() {
if (!_ready) return;
if (_controller == mesh::ui::OLEDController::SSD1306) {
Adafruit_SSD1306::display();
return;
}
wire->setClock(wireClk);
bool ok = true;
for (uint8_t page = 0; ok && page < (HEIGHT + 7) / 8; ++page) {
const uint8_t commands[] = {uint8_t(0xB0 | page), 0x02, 0x10};
ok = writeCommands(commands, sizeof(commands));
for (uint16_t column = 0; ok && column < WIDTH;) {
const uint8_t count = WIDTH - column > 31 ? 31 : WIDTH - column;
wire->beginTransmission(i2caddr);
const bool control = wire->write(uint8_t(0x40)) == 1;
const bool payload = wire->write(buffer + page * WIDTH + column, count) == count;
ok = wire->endTransmission() == 0 && control && payload;
column += count;
}
}
wire->setClock(restoreClk);
}
};
+1 -1
View File
@@ -77,7 +77,7 @@ void DRV2605Vibration::stop() {
void DRV2605Vibration::pulse(bool on) {
if (!_ready) return;
if (!on) {
if (!on || _quiet) {
stop();
return;
}
+1 -1
View File
@@ -27,7 +27,7 @@ public:
bool isVibrating();
void stop(); // stop vibration immediately
void pulse(bool on); // continuous RTP amplitude for scheduled ms pulses
void quiet(bool q) { _quiet = q; }
void quiet(bool q) { if (q) stop(); _quiet = q; }
bool isQuiet() const { return _quiet; }
private:
+6
View File
@@ -8,6 +8,7 @@ void GenericVibration::begin() {
}
void GenericVibration::trigger() {
if (_quiet) return;
duration = millis();
digitalWrite(PIN_VIBRATION, HIGH);
}
@@ -35,4 +36,9 @@ void GenericVibration::stop() {
digitalWrite(PIN_VIBRATION, LOW);
}
void GenericVibration::pulse(bool on) {
stop();
if (on && !_quiet) digitalWrite(PIN_VIBRATION, HIGH);
}
#endif // ifdef PIN_VIBRATION
+5 -1
View File
@@ -25,9 +25,13 @@ public:
void loop(); // non-blocking timer handling
bool isVibrating(); // returns true if currently vibrating
void stop(); // stop vibration immediately
void pulse(bool on);
void quiet(bool q) { if (q) stop(); _quiet = q; }
bool isQuiet() const { return _quiet; }
private:
unsigned long duration;
unsigned long duration = 0;
bool _quiet = false;
};
#endif // ifdef PIN_VIBRATION
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <stdint.h>
namespace mesh { namespace ui {
enum class OLEDController : uint8_t { Absent, SSD1306, SH1106 };
// Status-byte detection is used by ss_oled and Meshtastic. Only 0x08 is
// evidence of SH1106: some RAK1921 SSD1306 panels return the ambiguous 0x00.
// See https://github.com/meshtastic/firmware/pull/11265.
template <typename WireBus>
OLEDController detectOLEDController(WireBus& wire, uint8_t address) {
wire.beginTransmission(address);
if (wire.endTransmission() != 0) return OLEDController::Absent;
int previous = -1;
for (unsigned attempt = 0; attempt < 4; ++attempt) {
wire.beginTransmission(address);
const bool written = wire.write(uint8_t(0x00)) == 1;
if (wire.endTransmission() != 0 || !written) break;
if (wire.requestFrom(address, uint8_t(1)) != 1 || wire.available() != 1) break;
const int status = wire.read();
if (status < 0) break;
const int nibble = status & 0x0f;
if (nibble == previous)
return nibble == 0x08 ? OLEDController::SH1106 : OLEDController::SSD1306;
previous = nibble;
}
return OLEDController::SSD1306;
}
}} // namespace mesh::ui
+7
View File
@@ -6,6 +6,9 @@
#define SSD1306_NO_SPLASH
#include <Adafruit_SSD1306.h>
#include <helpers/RefCountedDigitalPin.h>
#if defined(OLED_AUTO_DETECT) && OLED_AUTO_DETECT
#include "AutoOLEDBackend.h"
#endif
#ifndef PIN_OLED_RESET
#define PIN_OLED_RESET 21 // Reset pin # (or -1 if sharing Arduino reset pin)
@@ -21,7 +24,11 @@ public:
static constexpr int16_t PANEL_HEIGHT = 64;
static constexpr size_t FRAMEBUFFER_BYTES = PANEL_WIDTH * ((PANEL_HEIGHT + 7) / 8);
private:
#if defined(OLED_AUTO_DETECT) && OLED_AUTO_DETECT
AutoOLEDBackend display;
#else
Adafruit_SSD1306 display;
#endif
bool _isOn;
bool _panel_ready = false;
bool _flipped = false;
+9 -9
View File
@@ -48,16 +48,14 @@ static mesh::audio::VoiceClip buttonVoiceClip(ButtonVoicePrompt prompt) {
#endif
void genericBuzzer::begin() {
// Serial.print("DBG: Setting up buzzer on pin ");
// Serial.println(PIN_BUZZER);
#ifdef PIN_BUZZER_EN
pinMode(PIN_BUZZER_EN, OUTPUT);
digitalWrite(PIN_BUZZER_EN, HIGH);
#endif
quiet(false);
digitalWrite(PIN_BUZZER, LOW);
pinMode(PIN_BUZZER, OUTPUT);
digitalWrite(PIN_BUZZER, LOW); // need to pull low by default to avoid extreme power draw
#ifdef PIN_BUZZER_EN
digitalWrite(PIN_BUZZER_EN, LOW);
pinMode(PIN_BUZZER_EN, OUTPUT);
#endif
// Start muted until the UI applies the saved preference.
quiet(_is_quiet);
}
void genericBuzzer::play(const char *melody) {
@@ -79,6 +77,8 @@ void genericBuzzer::play(const char *melody) {
}
void genericBuzzer::playNotification(const char* melody) {
// Muted background alerts must not interrupt an explicit button prompt.
if (_is_quiet) return;
stop();
#ifdef PIN_BUZZER_EN
digitalWrite(PIN_BUZZER_EN, HIGH);
+1
View File
@@ -71,6 +71,7 @@ struct UITask {
unsigned battery_reads = 0, shutdowns = 0, profile_services = 0;
char _alert[80] = {}, _origin[80] = {}, _msg[80] = {};
void servicePairingState() {}
void syncAlertPreferences() {}
void serviceWiFiToggleButton() {}
bool isPairingScreenActive() const { return false; }
bool isButtonGesturePending() const { return gesture_pending; }
+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()
+6 -4
View File
@@ -40,7 +40,7 @@ int main(){
{ButtonVoicePrompt::AlertsSilent,alertsSilentClip,nullptr},
#endif
};
genericBuzzer buzzer;buzzer.begin();
genericBuzzer buzzer;buzzer.begin();buzzer.quiet(false);
auto finish=[&]{
assert(!registers.ENABLE);
rtttl::finish();buzzer.loop();
@@ -548,7 +548,7 @@ NOTIFICATION_STOPS = r'''
#include <helpers/ui/buzzer.cpp>
uint32_t now_ms=0;
int main(){
genericBuzzer buzzer;buzzer.begin();
genericBuzzer buzzer;buzzer.begin();buzzer.quiet(false);
buzzer.playNotification("ordinary:d=4,o=5,b=120:c");
assert(buzzer.isPlaying()&&rtttl::isPlaying());
buzzer.stopNotification();assert(!buzzer.isPlaying()&&!rtttl::isPlaying());
@@ -590,11 +590,13 @@ int main(){
assert(!buzzer.isPlaying()&&!rtttl::isPlaying());
buzzer.quiet(true);
#endif
buzzer.playNotification("ordinary:d=4,o=5,b=120:c");
assert(!buzzer.isPlaying()&&buzzer.isQuiet()&&pin_states[PIN_BUZZER_EN]==LOW);
buzzer.quiet(false);
buzzer.playNotification("ordinary:d=4,o=5,b=120:c");
assert(buzzer.isPlaying()&&pin_states[PIN_BUZZER_EN]==HIGH);
buzzer.stopNotification();
assert(!buzzer.isPlaying()&&!rtttl::isPlaying()&&buzzer.isQuiet());
assert(pin_states[PIN_BUZZER_EN]==LOW);
assert(!buzzer.isPlaying()&&!rtttl::isPlaying()&&!buzzer.isQuiet());
}
'''
+217
View File
@@ -0,0 +1,217 @@
"""Run the Companion alert CLI against storage and live-output fixtures."""
from pathlib import Path
import os
import re
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
def alert_handler():
source = (ROOT / "examples/companion_radio/Notifications.cpp").read_text()
match = re.search(r"\bbool\s+MyMesh::handleAlertCommand\([^)]*\)\s*\{", source)
if not match:
raise AssertionError("production Companion alert handler is missing")
end, depth = match.end(), 1
while depth:
depth += (source[end] == "{") - (source[end] == "}")
end += 1
return source[match.start():end]
STUBS = r"""
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <string>
#include <initializer_list>
struct Prefs {
uint8_t buzzer_quiet = 1, vibe_quiet = 0;
uint32_t unrelated = 0x12345678;
};
struct UI {
Prefs* prefs;
Prefs* persisted;
unsigned syncs = 0;
bool sound_quiet = true, vibration_quiet = false;
UI(Prefs* p, Prefs* saved) : prefs(p), persisted(saved) {}
void syncAlertPreferences() {
assert(prefs->buzzer_quiet == persisted->buzzer_quiet);
assert(prefs->vibe_quiet == persisted->vibe_quiet);
++syncs;
sound_quiet = prefs->buzzer_quiet;
vibration_quiet = prefs->vibe_quiet;
}
};
class MyMesh {
public:
Prefs _prefs, saved;
UI ui{&_prefs, &saved};
UI* _ui = &ui;
unsigned saves = 0;
bool save_ok = true;
bool savePrefs() {
++saves;
if (!save_ok) return false;
saved = _prefs;
return true;
}
bool handleAlertCommand(const char*, char*, size_t);
};
"""
SCENARIOS = r"""
std::string run(MyMesh& mesh, const char* command) {
char reply[160] = {};
assert(mesh.handleAlertCommand(command, reply, sizeof(reply)));
return reply;
}
void error(MyMesh& mesh, const char* command) {
const auto before = mesh._prefs;
const unsigned saves = mesh.saves, syncs = mesh.ui.syncs;
assert(run(mesh, command).find("Error") == 0);
assert(mesh._prefs.buzzer_quiet == before.buzzer_quiet);
assert(mesh._prefs.vibe_quiet == before.vibe_quiet);
assert(mesh.saves == saves && mesh.ui.syncs == syncs);
}
void supported(MyMesh& mesh, const char* name, bool initial_on) {
const std::string get = std::string("get ") + name;
const std::string set = std::string("set ") + name;
const bool sound = std::string(name) == "buzzer";
const uint8_t other = sound ? mesh._prefs.vibe_quiet : mesh._prefs.buzzer_quiet;
assert(run(mesh, get.c_str()) == (initial_on ? "> on" : "> off"));
assert(mesh.saves == 0 && mesh.ui.syncs == 0);
for (const char* value : {"", " ", "on extra", "off extra", "1", "0", "ON", "OFF",
"true", "false", "onward", "officer"}) {
error(mesh, (set + " " + value).c_str());
}
error(mesh, set.c_str());
error(mesh, (get + " on").c_str());
// An unchanged value acknowledges the command without rewriting flash.
mesh.ui.sound_quiet = false;
mesh.ui.vibration_quiet = true;
assert(run(mesh, (set + (initial_on ? " on" : " off")).c_str()) == "OK");
assert(mesh.saves == 0 && mesh.ui.syncs == 1);
assert(mesh.ui.sound_quiet == bool(mesh._prefs.buzzer_quiet));
assert(mesh.ui.vibration_quiet == bool(mesh._prefs.vibe_quiet));
// Persist first, then apply to the running UI. Changing one output must not
// overwrite the independent preference or unrelated node settings.
assert(run(mesh, (set + (initial_on ? " off" : " on")).c_str()) == "OK");
assert(mesh.saves == 1 && mesh.ui.syncs == 2);
assert(run(mesh, get.c_str()) == (initial_on ? "> off" : "> on"));
assert((sound ? mesh._prefs.buzzer_quiet : mesh._prefs.vibe_quiet) == initial_on);
assert((sound ? mesh._prefs.vibe_quiet : mesh._prefs.buzzer_quiet) == other);
assert(mesh.saved.buzzer_quiet == mesh._prefs.buzzer_quiet);
assert(mesh.saved.vibe_quiet == mesh._prefs.vibe_quiet);
assert(mesh.saved.unrelated == 0x12345678);
assert(mesh.ui.sound_quiet == bool(mesh._prefs.buzzer_quiet));
assert(mesh.ui.vibration_quiet == bool(mesh._prefs.vibe_quiet));
const Prefs previous = mesh._prefs, persisted = mesh.saved;
mesh.save_ok = false;
assert(run(mesh, (set + (initial_on ? " on" : " off")).c_str()).find("Error") == 0);
assert(mesh.saves == 2 && mesh.ui.syncs == 2);
assert(mesh._prefs.buzzer_quiet == previous.buzzer_quiet);
assert(mesh._prefs.vibe_quiet == previous.vibe_quiet);
assert(mesh.saved.buzzer_quiet == persisted.buzzer_quiet);
assert(mesh.saved.vibe_quiet == persisted.vibe_quiet);
assert(run(mesh, get.c_str()) == (initial_on ? "> off" : "> on"));
// A build without an attached UI can still persist the board preference.
mesh._ui = nullptr;
mesh.save_ok = true;
assert(run(mesh, (set + (initial_on ? " on" : " off")).c_str()) == "OK");
assert(mesh.saves == 3 && mesh.ui.syncs == 2);
assert(run(mesh, get.c_str()) == (initial_on ? "> on" : "> off"));
}
int main() {
MyMesh buzzer, vibration;
#if EXPECT_BUZZER
supported(buzzer, "buzzer", false);
#else
error(buzzer, "get buzzer");
error(buzzer, "set buzzer on");
error(buzzer, "set buzzer off");
#endif
#if EXPECT_VIBRATION
supported(vibration, "vibration", true);
#else
error(vibration, "get vibration");
error(vibration, "set vibration on");
error(vibration, "set vibration off");
#endif
MyMesh mesh;
for (const char* command : {"get buzzerx", "get buzzer.extra", "get vibrationx",
"set buzzerx on", "set vibrationx off", "get leds", "get radio", "get notify",
"get sound", "set vibe on", "buzzer", "vibration", "get", "", "set"}) {
char reply[16] = "unchanged";
assert(!mesh.handleAlertCommand(command, reply, sizeof(reply)));
assert(strcmp(reply, "unchanged") == 0);
assert(mesh.saves == 0 && mesh.ui.syncs == 0);
}
for (const char* command : {"get buzzer", "get vibration", "set buzzer bad",
"set vibration bad", "set buzzer on", "set vibration off"}) {
for (size_t capacity = 1; capacity <= 160; ++capacity) {
char bounded[164]; memset(bounded, '#', sizeof(bounded));
memcpy(bounded, "ab|", 3);
assert(mesh.handleAlertCommand(command, bounded + 3, capacity));
assert(memcmp(bounded, "ab|", 3) == 0);
assert(bounded[3 + capacity] == '#');
assert(memchr(bounded + 3, 0, capacity));
}
}
char reply[16];
assert(!mesh.handleAlertCommand(nullptr, reply, sizeof(reply)));
assert(!mesh.handleAlertCommand("get buzzer", nullptr, sizeof(reply)));
assert(!mesh.handleAlertCommand("get buzzer", reply, 0));
}
"""
class CompanionAlertControlTests(unittest.TestCase):
def test_cli_storage_and_hardware_matrix(self):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
if not compiler:
self.skipTest("C++ compiler unavailable")
cases = (
("none", (), False, False),
("buzzer", ("PIN_BUZZER=1",), True, False),
("gpio_vibration", ("PIN_VIBRATION=2",), False, True),
("drv2605", ("HAS_DRV2605=1",), False, True),
("buzzer_gpio", ("PIN_BUZZER=1", "PIN_VIBRATION=2"), True, True),
("buzzer_drv2605", ("PIN_BUZZER=1", "HAS_DRV2605=1"), True, True),
)
source = STUBS + alert_handler() + SCENARIOS
with tempfile.TemporaryDirectory() as directory:
directory = Path(directory)
cpp = directory / "alert_cli.cpp"
cpp.write_text(source, encoding="ascii")
for name, macros, buzzer, vibration in cases:
with self.subTest(hardware=name):
exe = directory / (name + (".exe" if os.name == "nt" else ""))
flags = [f"-D{macro}" for macro in macros]
if os.name != "nt":
flags += ["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-omit-frame-pointer", "-fno-pie", "-no-pie"]
result = subprocess.run(
[compiler, "-std=c++11", "-Wall", "-Wextra", *flags,
f"-DEXPECT_BUZZER={int(buzzer)}",
f"-DEXPECT_VIBRATION={int(vibration)}",
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)
if __name__ == "__main__":
unittest.main()
+248
View File
@@ -0,0 +1,248 @@
"""Run Companion master mute and live UI synchronization with inert drivers."""
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
import unittest
import test_gps_voice_buttons as voice
from test_radio_receive_contract import method
ROOT = Path(__file__).resolve().parents[1]
ARDUINO = voice.ARDUINO.replace(
"inline unsigned pin_writes[64]={};",
"inline unsigned pin_writes[64]={}, high_writes[64]={};").replace(
"pin_states[pin]=value;++pin_writes[pin];",
"pin_states[pin]=value;++pin_writes[pin];if(value==HIGH)++high_writes[pin];")
ARDUINO += "\ninline void delay(unsigned) {}\n"
WIRE = "#pragma once\nstruct TwoWire {};\ninline TwoWire Wire;\n"
ADAFRUIT = r'''
#pragma once
#include <Arduino.h>
#include <Wire.h>
#define DRV2605_MODE_INTTRIG 0
#define DRV2605_MODE_REALTIME 5
#define DRV2605_REG_FEEDBACK 0x1a
#define DRV2605_REG_GO 0x0c
struct MockDriver {
bool found=true;
uint8_t registers[32]={};
unsigned writes=0;
};
inline MockDriver hardware;
class Adafruit_DRV2605 {
public:
bool begin(TwoWire*) { return hardware.found; }
void writeRegister8(uint8_t reg,uint8_t value) {
hardware.registers[reg]=value;++hardware.writes;
}
uint8_t readRegister8(uint8_t reg) { return hardware.registers[reg]; }
void selectLibrary(uint8_t value) { writeRegister8(3,value); }
void setMode(uint8_t value) { writeRegister8(1,value); }
void setWaveform(uint8_t slot,uint8_t value) { writeRegister8(4+slot,value); }
void setRealtimeValue(uint8_t value) { writeRegister8(2,value); }
void go() { writeRegister8(DRV2605_REG_GO,1); }
void stop() { writeRegister8(DRV2605_REG_GO,0); }
};
'''
BUZZER = r'''
#include <cassert>
#define PIN_BUZZER 25
#define PIN_BUZZER_EN 37
#define MESH_GPS_VOICE @VOICE@
#include <helpers/ui/buzzer.cpp>
uint32_t now_ms=100;
int main() {
genericBuzzer buzzer;buzzer.begin();
assert(buzzer.isQuiet()&&pin_states[PIN_BUZZER]==LOW&&pin_states[PIN_BUZZER_EN]==LOW);
assert(high_writes[PIN_BUZZER]==0&&high_writes[PIN_BUZZER_EN]==0);
buzzer.quiet(false);
buzzer.playNotification("alert:d=4,o=5,b=120:c");
assert(buzzer.isPlaying());
buzzer.quiet(true);
assert(!buzzer.isPlaying()&&pin_states[PIN_BUZZER_EN]==LOW);
auto starts=rtttl::begins;
buzzer.playNotification("alert:d=4,o=5,b=120:c");
assert(!buzzer.isPlaying()&&rtttl::begins==starts);
assert(pin_states[PIN_BUZZER_EN]==LOW);
#if MESH_GPS_VOICE
// A rejected background melody cannot cancel explicit courtesy speech.
assert(buzzer.speakButton(ButtonVoicePrompt::SoundOff,true));
buzzer.playNotification("alert:d=4,o=5,b=120:c");
buzzer.stopNotification();
assert(buzzer.isQuiet()&&buzzer.isPlaying());
auto cancelled=gps_voice.cancelled();
buzzer.loop();now_ms+=100;buzzer.loop();
assert(registers.ENABLE);
buzzer.playNotification("alert:d=4,o=5,b=120:c");
assert(registers.ENABLE&&gps_voice.cancelled()==cancelled);
buzzer.stop();
#endif
buzzer.quiet(false);
buzzer.playNotification("alert:d=4,o=5,b=120:c");
assert(buzzer.isPlaying()&&rtttl::begins==starts+1);
buzzer.stopNotification();assert(!buzzer.isPlaying());
}
'''
VIBRATION = r'''
#include <cassert>
@FLAGS@
#include <helpers/ui/GenericVibration.cpp>
#include <helpers/ui/DRV2605Vibration.cpp>
uint32_t now_ms=100;
#ifdef HAS_DRV2605
using Vibration = DRV2605Vibration;
bool active() { return hardware.registers[1]!=0x40; }
#else
using Vibration = GenericVibration;
bool active() { return pin_states[PIN_VIBRATION]==HIGH; }
#endif
int main() {
Vibration motor;motor.begin();
motor.trigger();assert(active());
motor.quiet(true);assert(!active()&&!motor.isVibrating());
motor.trigger();motor.pulse(true);motor.loop();
assert(motor.isQuiet()&&!active()&&!motor.isVibrating());
motor.pulse(false);assert(!active());
motor.quiet(false);
motor.pulse(true);assert(active());
motor.quiet(true);assert(!active());
motor.quiet(false);
now_ms+=6000;motor.trigger();assert(active());
motor.stop();assert(!active());
#ifdef HAS_DRV2605
hardware.found=false;motor.begin();
auto writes=hardware.writes;
motor.quiet(true);motor.trigger(true);motor.pulse(true);motor.pulse(false);
assert(hardware.writes==writes&&!motor.isVibrating());
#endif
}
'''
UI = r'''
#include <cassert>
#define MESH_GPS_VOICE 0
@FLAGS@
#include <helpers/ui/buzzer.cpp>
#include <helpers/ui/GenericVibration.cpp>
#include <helpers/ui/DRV2605Vibration.cpp>
uint32_t now_ms=100;
struct CompanionNodePrefs { uint8_t buzzer_quiet=0,vibe_quiet=0; };
struct UITask {
CompanionNodePrefs* _node_prefs=nullptr;
#ifdef PIN_BUZZER
genericBuzzer buzzer;
#endif
#ifdef HAS_DRV2605
DRV2605Vibration vibration;
#elif defined(PIN_VIBRATION)
GenericVibration vibration;
#endif
void syncAlertPreferences();
void notificationMelody(const char*);
void notificationVibration(bool);
};
@METHODS@
int main() {
UITask ui;ui.syncAlertPreferences();
CompanionNodePrefs prefs;ui._node_prefs=&prefs;
#ifdef PIN_BUZZER
ui.buzzer.begin();ui.buzzer.quiet(false);ui.notificationMelody("alert:d=4,o=5,b=120:c");
assert(ui.buzzer.isPlaying());
#endif
#if defined(HAS_DRV2605) || defined(PIN_VIBRATION)
ui.vibration.begin();ui.notificationVibration(true);
#endif
prefs.buzzer_quiet=prefs.vibe_quiet=1;
ui.syncAlertPreferences();
#ifdef PIN_BUZZER
assert(ui.buzzer.isQuiet()&&!ui.buzzer.isPlaying());
ui.notificationMelody("alert:d=4,o=5,b=120:c");
assert(!ui.buzzer.isPlaying());
#endif
#if defined(HAS_DRV2605) || defined(PIN_VIBRATION)
assert(ui.vibration.isQuiet()&&!ui.vibration.isVibrating());
ui.notificationVibration(true);
#ifdef HAS_DRV2605
assert(hardware.registers[1]==0x40);
auto writes=hardware.writes;
ui.syncAlertPreferences();ui.syncAlertPreferences();
assert(hardware.writes==writes); // unchanged prefs must not add I2C traffic
#else
assert(pin_states[PIN_VIBRATION]==LOW);
#endif
#endif
prefs.buzzer_quiet=prefs.vibe_quiet=0;
ui.syncAlertPreferences();
#ifdef PIN_BUZZER
ui.notificationMelody("alert:d=4,o=5,b=120:c");
assert(ui.buzzer.isPlaying());
ui.notificationMelody(nullptr);assert(!ui.buzzer.isPlaying());
#endif
#if defined(HAS_DRV2605) || defined(PIN_VIBRATION)
ui.notificationVibration(true);
#ifdef HAS_DRV2605
assert(hardware.registers[1]==DRV2605_MODE_REALTIME);
#else
assert(pin_states[PIN_VIBRATION]==HIGH);
#endif
ui.notificationVibration(false);
#endif
}
'''
class CompanionAlertDriversTests(unittest.TestCase):
def build(self, source):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
self.assertIsNotNone(compiler, "a host C++ compiler is required")
with tempfile.TemporaryDirectory(prefix="meshcore-alerts-") as directory:
directory = Path(directory)
for name, content in (("Arduino.h", ARDUINO), ("Wire.h", WIRE),
("Adafruit_DRV2605.h", ADAFRUIT),
("HardwarePWM.h", voice.PWM),
("NonBlockingRtttl.h", voice.RTTTL)):
(directory / name).write_text(content)
cpp = directory / "test.cpp"
exe = directory / ("test.exe" if os.name == "nt" else "test")
cpp.write_text(source)
result = subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-Wno-unused-parameter", "-I", str(directory),
"-I", str(ROOT / "src"), str(cpp), "-o", str(exe)],
capture_output=True, text=True, timeout=30)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
result = subprocess.run([str(exe)], capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_programmable_sound_obeys_mute_and_preserves_courtesy_speech(self):
for speech in (0, 1):
with self.subTest(speech=speech):
self.build(BUZZER.replace("@VOICE@", str(speech)))
def test_gpio_and_drv2605_mute_stops_active_effects_and_blocks_every_entry(self):
for flags in ("#define PIN_VIBRATION 12", "#define HAS_DRV2605 1"):
with self.subTest(flags=flags):
self.build(VIBRATION.replace("@FLAGS@", flags))
def test_each_ui_applies_saved_changes_without_reboot_or_repeated_driver_writes(self):
for ui in ("ui-orig", "ui-new", "ui-tiny"):
source = (ROOT / "examples/companion_radio" / ui / "UITask.cpp").read_text()
methods = "\n".join(method(source, signature) for signature in (
"void UITask::syncAlertPreferences()",
"void UITask::notificationMelody(const char* text)",
"void UITask::notificationVibration(bool on)",
))
for flags in ("#define PIN_BUZZER 25\n#define PIN_VIBRATION 12",
"#define PIN_BUZZER 25", "#define HAS_DRV2605 1", ""):
with self.subTest(ui=ui, flags=flags):
self.build(UI.replace("@FLAGS@", flags).replace("@METHODS@", methods))
loop = method(source, "void UITask::loop()")
self.assertRegex(loop, r"\{\s*syncAlertPreferences\(\);")
if __name__ == "__main__":
unittest.main()
+227
View File
@@ -0,0 +1,227 @@
"""Exercise the compact Companion LED CLI and its existing preference store."""
from pathlib import Path
import os
import re
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
def method(source, name):
match = re.search(rf"\bbool\s+MyMesh::{name}\([^)]*\)\s*\{{", source)
if not match:
raise AssertionError(f"production {name} is missing")
end, depth = match.end(), 1
while depth:
depth += (source[end] == "{") - (source[end] == "}")
end += 1
return source[match.start():end]
def startup_loader():
source = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
setup = source.split("void MyMesh::configureRadioFromPrefs() {", 1)[1]
match = re.search(
r"#if defined\(WIO_E5_MINI_HEADLESS_COMPANION_UI\)\n(.*?)#endif", setup, re.S)
if not match:
raise AssertionError("compact LED startup restore is missing")
return "void MyMesh::loadCompactLedSetting() {\n" + match.group(1) + "}\n"
STUBS = r"""
#include <helpers/LedPolicy.h>
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <initializer_list>
#include <string>
#include <vector>
std::vector<std::string> events;
struct Prefs {
uint8_t leds_quiet = 0;
uint32_t unrelated = 0x12345678;
};
struct Board {
unsigned applies = 0;
bool level = true;
void applyLedPolicy() {
events.push_back("board");
++applies;
level = mesh::leds::enabled();
}
} board;
unsigned status_applies = 0;
bool status_level = true;
namespace mesh { namespace leds {
void applyStatusPolicy() {
events.push_back("status");
++status_applies;
status_level = enabled();
}
}}
class MyMesh {
public:
Prefs _prefs;
Prefs saved;
bool save_ok = true;
unsigned saves = 0;
bool savePrefs() {
events.push_back("save");
++saves;
if (!save_ok) return false;
saved = _prefs;
return true;
}
bool handleCompactLedCommand(const char*, char*, size_t);
void loadCompactLedSetting();
};
void reset() {
mesh::leds::setEnabled(true);
board.applies = status_applies = 0;
board.level = status_level = true;
events.clear();
}
"""
SCENARIOS = r"""
std::string run(MyMesh& mesh, const char* command) {
char reply[160] = {};
assert(mesh.handleCompactLedCommand(command, reply, sizeof(reply)));
return reply;
}
int main() {
reset();
MyMesh mesh;
assert(run(mesh, "get leds") == "> on");
assert(mesh.saves == 0 && board.applies == 0 && status_applies == 0);
for (const char* command : {"set leds", "set leds ",
"set leds 1", "set leds 0", "set leds ON", "set leds OFF", "set leds true",
"set leds off extra", "set leds on extra"}) {
assert(run(mesh, command).find("Error") == 0);
assert(mesh.saves == 0 && board.applies == 0 && status_applies == 0);
assert(mesh::leds::enabled());
}
for (const char* command : {"get leds on", "get leds ", "get led", "get leds.extra", "get ledsx", "set ledsx on",
"set led on", "get buzzer", "get radio", "get", "set", ""}) {
char reply[16] = "unchanged";
assert(!mesh.handleCompactLedCommand(command, reply, sizeof(reply)));
assert(strcmp(reply, "unchanged") == 0);
}
assert(run(mesh, "set leds on") == "OK");
assert(mesh.saves == 0);
assert(mesh._prefs.leds_quiet == 0);
// An unchanged stored preference can repair stale live policy without flash.
mesh::leds::setEnabled(false);
assert(run(mesh, "set leds on") == "OK");
assert(mesh.saves == 0 && mesh::leds::enabled() && board.level && status_level);
events.clear();
const unsigned applied = board.applies, status = status_applies;
// A failed preference transaction restores the byte and leaves live output.
mesh.save_ok = false;
assert(run(mesh, "set leds off").find("Error") == 0);
assert(mesh.saves == 1 && mesh._prefs.leds_quiet == 0);
assert(mesh::leds::enabled() && mesh.saved.leds_quiet == 0);
assert(board.applies == applied && status_applies == status);
assert(events == std::vector<std::string>{"save"});
mesh.save_ok = true;
events.clear();
assert(run(mesh, "set leds off") == "OK");
assert(mesh.saves == 2 && !mesh::leds::enabled());
assert(mesh._prefs.leds_quiet == 1 && mesh.saved.leds_quiet == 1);
assert(mesh._prefs.unrelated == 0x12345678 && mesh.saved.unrelated == 0x12345678);
assert(board.applies == applied + 1 && !board.level);
assert(status_applies == status + 1 && !status_level);
assert((events == std::vector<std::string>{"save", "board", "status"}));
assert(run(mesh, "get leds") == "> off");
assert(run(mesh, "set leds off") == "OK" && mesh.saves == 2);
// Reload durable preference bytes through the actual compact startup block.
MyMesh reboot;
reboot._prefs = mesh.saved;
reset();
reboot.loadCompactLedSetting();
assert(!mesh::leds::enabled());
assert(board.applies == 1 && !board.level && status_applies == 1 && !status_level);
assert(run(reboot, "get leds") == "> off");
assert(run(reboot, "set leds off") == "OK" && reboot.saves == 0);
reboot.save_ok = false;
const unsigned before_failed_apply = board.applies, before_failed_status = status_applies;
assert(run(reboot, "set leds on").find("Error") == 0 && reboot.saves == 1);
assert(reboot._prefs.leds_quiet == 1 && !mesh::leds::enabled());
assert(board.applies == before_failed_apply && status_applies == before_failed_status);
reboot.save_ok = true;
assert(run(reboot, "set leds on") == "OK" && reboot.saves == 2);
assert(reboot.saved.leds_quiet == 0 && reboot.saved.unrelated == 0x12345678);
assert(mesh::leds::enabled() && board.level && status_level);
MyMesh restored_on;
restored_on._prefs = reboot.saved;
mesh::leds::setEnabled(false);
restored_on.loadCompactLedSetting();
assert(mesh::leds::enabled() && board.level && status_level);
// Historic reserved zero means on; only exactly one requests quiet LEDs.
for (unsigned value = 0; value < 256; ++value) {
MyMesh fresh;
fresh._prefs.leds_quiet = value;
reset();
mesh::leds::setEnabled(false);
fresh.loadCompactLedSetting();
const bool on = value != 1;
assert(mesh::leds::enabled() == on && fresh._prefs.leds_quiet == !on);
assert(board.applies == 1 && board.level == on);
assert(status_applies == 1 && status_level == on);
assert(fresh.saves == 0);
}
for (const char* command : {"get leds", "set leds invalid", "set leds off", "set leds on"}) {
for (size_t capacity = 1; capacity <= 160; ++capacity) {
char reply[164]; memset(reply, '#', sizeof(reply));
memcpy(reply, "ab|", 3);
assert(mesh.handleCompactLedCommand(command, reply + 3, capacity));
assert(memcmp(reply, "ab|", 3) == 0 && reply[3 + capacity] == '#');
assert(memchr(reply + 3, 0, capacity));
}
}
char reply[16];
assert(!mesh.handleCompactLedCommand(nullptr, reply, sizeof(reply)));
assert(!mesh.handleCompactLedCommand("get leds", nullptr, sizeof(reply)));
assert(!mesh.handleCompactLedCommand("get leds", reply, 0));
}
"""
class CompanionCompactLedControlTests(unittest.TestCase):
def test_preference_byte_storage_and_policy(self):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
if not compiler:
self.skipTest("C++ compiler unavailable")
source = (ROOT / "examples/companion_radio/Notifications.cpp").read_text()
source = STUBS + method(source, "handleCompactLedCommand") + startup_loader() + SCENARIOS
with tempfile.TemporaryDirectory() as directory:
directory = Path(directory)
cpp = directory / "compact_led_cli.cpp"
exe = directory / ("compact_led_cli.exe" if os.name == "nt" else "compact_led_cli")
cpp.write_text(source, encoding="ascii")
flags = [] if os.name == "nt" else [
"-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-omit-frame-pointer", "-fno-pie", "-no-pie"]
result = subprocess.run(
[compiler, "-std=c++11", "-Wall", "-Wextra", *flags,
"-DWIO_E5_MINI_HEADLESS_COMPANION_UI=1", "-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)
if __name__ == "__main__":
unittest.main()
@@ -51,6 +51,8 @@ bool contactPathPresence(FILESYSTEM* fs,const char* path,bool& present){
int main(int argc,char** argv){
const int scenario=argc>1?atoi(argv[1]):0;
DataStore store;CompanionNodePrefs original;
assert(original.buzzer_quiet==1&&original.vibe_quiet==0);
original.buzzer_quiet=0;original.vibe_quiet=1;
strcpy(original.node_name,"durable node");original.freq=910.525f;
original.ble_pin=876543;original.bluetooth_stealth_mode=2;
original.gps_interval=123;original.sf=7;original.cr=7;original.bw=62.5f;
@@ -103,6 +105,7 @@ int main(int argc,char** argv){
CompanionNodePrefs loaded;double lat=0,lon=0;
assert(store.loadPrefs(loaded,lat,lon));
assert(loaded.freq==changed.freq&&loaded.ble_pin==original.ble_pin);
assert(loaded.buzzer_quiet==0&&loaded.vibe_quiet==1);
assert(loaded.tx_delay_factor==1.25f&&loaded.direct_tx_delay_factor==0.75f);
assert(loaded.interference_threshold==8&&loaded.agc_reset_interval==15&&loaded.tz_offset==-7);
assert(loaded.flood_retry_attempts==4&&loaded.flood_retry_max_path==3);
@@ -153,6 +156,7 @@ int main(int argc,char** argv){
}
DataStore fresh;CompanionNodePrefs defaults;double lat=0,lon=0;
assert(fresh.loadPrefs(defaults,lat,lon));
assert(defaults.buzzer_quiet==1&&defaults.vibe_quiet==0);
assert(defaults.wifi_enabled==0); // Fresh installs never enable WiFi implicitly.
assert(fresh.savePrefs(defaults,lat,lon));
} else if(scenario==2){
+101 -3
View File
@@ -11,7 +11,8 @@ from test_radio_receive_contract import method
ROOT = Path(__file__).resolve().parents[1]
# Published field order and sizes, independent of the implementation's emitter
# and the class's in-memory member order or padding. None represents zero pad.
# and the class's in-memory member order or padding. None represents reserved
# padding; compact Wio Companion claims the single byte at wire offset 79.
WIRE_FIELDS = (
('airtime_factor', 4), ('node_name', 32), (None, 4),
('@lat', 8), ('@lon', 8), ('freq', 4), ('sf', 1), ('cr', 1),
@@ -147,17 +148,32 @@ int main() {
#endif
DataStore reference;
assert(reference.savePrefsLegacy(prefs, 47.125, -122.25));
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)
// The compact target claims exactly the former reserved byte. Its frozen
// predecessor still emits zero there; every other byte stays identical.
auto historical = store.fs.files.at("/new_prefs");
assert(historical[79] == prefs.leds_quiet);
historical[79] = 0;
assert(reference.fs.files.at("/new_prefs") == historical);
#else
assert(reference.fs.files.at("/new_prefs") == store.fs.files.at("/new_prefs"));
#endif
++comparisons;
}
CompanionNodePrefs original;
assert(original.buzzer_quiet == 1 && original.vibe_quiet == 0);
assert(original.lost_reply == 0);
assert(original.tx_media == mesh::RADIO_TX_AUTO && original.tx_ssh == mesh::RADIO_TX_AUTO);
strcpy(original.node_name, "durable image");
original.freq = 915.25f; original.ble_pin = 123456;
original.bluetooth_enabled = 1; original.usb_debug_enabled = 1;
original.gps_sync_interval_hours = 336;
original.buzzer_quiet = 0; original.vibe_quiet = 1;
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)
assert(original.leds_quiet == 0);
original.leds_quiet = 1;
#endif
original.lost_reply = 2;
original.tx_media = mesh::RADIO_TX_SECONDARY;
original.tx_ssh = mesh::RADIO_TX_OFF;
@@ -199,6 +215,11 @@ int main() {
double lat = 1, lon = 2;
assert(reboot.loadPrefsInt("/new_prefs", live, lat, lon));
assert(live.lost_reply == 0 && live.ble_pin == original.ble_pin);
assert(live.buzzer_quiet == (length >= 85 ? 0 : 1));
assert(live.vibe_quiet == (length >= 143 ? 1 : 0));
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)
assert(live.leds_quiet == 1);
#endif
assert(live.tx_media==mesh::RADIO_TX_AUTO&&live.tx_ssh==mesh::RADIO_TX_AUTO);
assert(live.freq == original.freq && !strcmp(live.node_name, original.node_name));
assert(lat == 47.125 && lon == -122.25);
@@ -232,6 +253,8 @@ int main() {
assert(reboot.loadPrefsInt("/new_prefs", live, lat, lon) == expected);
if (expected) {
assert(live.freq == original.freq);
assert(live.buzzer_quiet == (length >= 85 ? 0 : 1));
assert(live.vibe_quiet == (length >= 143 ? 1 : 0));
assert(live.lost_reply == (length >= historical_size ? 2 : 0));
assert(live.tx_media == (length == before.size() ? mesh::RADIO_TX_SECONDARY : mesh::RADIO_TX_AUTO));
assert(live.tx_ssh == (length == before.size() ? mesh::RADIO_TX_OFF : mesh::RADIO_TX_AUTO));
@@ -291,8 +314,75 @@ int main() {
changed.freq = 910.5f; changed.usb_debug_enabled = 0;
changed.gps_sync_interval_hours = 24;
changed.lost_reply = 1;
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)
changed.leds_quiet = 0;
#endif
const auto after = golden(changed, 42.25, -121.125);
// Saved choices remain independent of the new fresh-install defaults.
for (unsigned sound_quiet = 0; sound_quiet < 2; ++sound_quiet) {
for (unsigned vibration_quiet = 0; vibration_quiet < 2; ++vibration_quiet) {
CompanionNodePrefs saved;
assert(saved.copyPersistedValuesFrom(original));
saved.buzzer_quiet = sound_quiet;
saved.vibe_quiet = vibration_quiet;
DataStore writer;
assert(writer.savePrefs(saved, 47.125, -122.25));
const auto disk = writer.fs.files.at("/new_prefs");
assert(disk[84] == sound_quiet && disk[142] == vibration_quiet);
CompanionNodePrefs reboot;
reboot.buzzer_quiet = !sound_quiet;
reboot.vibe_quiet = !vibration_quiet;
double lat = 0, lon = 0;
assert(writer.loadPrefsInt("/new_prefs", reboot, lat, lon));
assert(reboot.buzzer_quiet == sound_quiet);
assert(reboot.vibe_quiet == vibration_quiet);
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)
assert(disk[79] == 1 && reboot.leds_quiet == 1);
#endif
assert(writer.fs.files.at("/new_prefs") == disk);
++comparisons;
}
}
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)
// The reserved byte is part of the atomic image, at its original offset.
// Old firmware emitted zero; raw invalid encodings are clamped by startup.
for (unsigned quiet : {0u, 1u, 2u, 255u}) {
CompanionNodePrefs saved;
assert(saved.copyPersistedValuesFrom(original));
saved.leds_quiet = quiet;
DataStore writer;
assert(writer.savePrefs(saved, 47.125, -122.25));
const auto disk = writer.fs.files.at("/new_prefs");
assert(disk.size() == before.size() && disk[79] == quiet);
auto expected = before; expected[79] = quiet;
assert(disk == expected);
CompanionNodePrefs reboot;
reboot.leds_quiet = !quiet;
double lat = 0, lon = 0;
assert(writer.loadPrefsInt("/new_prefs", reboot, lat, lon));
assert(reboot.leds_quiet == quiet);
assert(reboot.buzzer_quiet == 0 && reboot.vibe_quiet == 1);
assert(writer.fs.files.at("/new_prefs") == disk);
++comparisons;
}
for (size_t length : {size_t(84), historical_size}) {
auto historical = before;
historical[79] = 0;
historical.resize(length);
DataStore old;
old.fs.files["/new_prefs"] = historical;
CompanionNodePrefs live;
live.leds_quiet = 1;
double lat = 0, lon = 0;
assert(old.loadPrefsInt("/new_prefs", live, lat, lon));
assert(live.leds_quiet == 0); // Old reserved zero restores default-on LEDs.
assert(live.ble_pin == original.ble_pin && live.freq == original.freq);
assert(old.fs.files.at("/new_prefs") == historical);
++comparisons;
}
#endif
// Every possible short write and readback, including every tail byte,
// must preserve the old durable image. STM32 issues only one image write;
// the other supported platforms preserve their scalar-write path.
@@ -424,7 +514,14 @@ int main() {
def field_code(rows, seed=False):
result = []
for name, size in rows:
if name == 'tz_offset':
if name is None and size == 1:
result.append('#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)')
result.append('fill(&prefs.leds_quiet, sizeof(prefs.leds_quiet));' if seed
else 'append(&prefs.leds_quiet, 1);')
if not seed:
result.append('#else\nbytes.push_back(0);')
result.append('#endif')
elif name == 'tz_offset':
if seed:
result.append('prefs.tz_offset = float(int(random % 27) - 12);')
else:
@@ -530,7 +627,8 @@ inline char* utoa(unsigned int value,char* output,int base){
(work / 'helpers/IdentityStore.h').write_text(fs)
cpp = work / 'test.cpp'
cpp.write_text(code + tests)
for platform in ('STM32_PLATFORM', 'NRF52_PLATFORM', 'NRF52_PLATFORM,RAK_4631', 'ESP32_PLATFORM',
for platform in ('STM32_PLATFORM', 'STM32_PLATFORM,WIO_E5_MINI_HEADLESS_COMPANION_UI',
'NRF52_PLATFORM', 'NRF52_PLATFORM,RAK_4631', 'ESP32_PLATFORM',
'RP2040_PLATFORM', 'ESP32_PLATFORM,TBEAM_1W',
'RP2040_PLATFORM,ENABLE_WIFI_INTERFACE'):
with self.subTest(platform=platform):
+2
View File
@@ -272,11 +272,13 @@ 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 {
Radio radio;
Radio* _radio = &radio;
bool handleAlertCommand(const char*, char*, size_t) { return false; }
bool handleCommand(const char*, uint32_t, char*);
};
@HANDLER@
+3 -1
View File
@@ -152,6 +152,8 @@ int main() {
auto count=events.size();
vibration.trigger();assert(events.size()==count); // cooldown suppresses wake
vibration.quiet(true);
assert(standby() && !vibration.isVibrating());
count=events.size();
vibration.trigger(true);assert(events.size()==count);
vibration.stop();assert(standby() && !vibration.isVibrating());
assert(hardware.registers[DRV2605_REG_GO]==0 && hardware.registers[2]==0);
@@ -216,7 +218,7 @@ class Drv2605PowerTests(unittest.TestCase):
source = source.replace("(drv.readRegister8(DRV2605_REG_GO) & 1) == 0",
"(drv.readRegister8(DRV2605_REG_GO) & 1) != 0")
elif mutation == "no-pulse-off":
source = source.replace("if (!on) {", "if (false && !on) {")
source = source.replace("if (!on || _quiet) {", "if (false && !on) {")
elif mutation == "poll-idle":
source = source.replace("if (!_ready || !_effect_active) return;",
"if (!_ready) return;")
+2 -2
View File
@@ -313,7 +313,7 @@ BUZZER_SEQUENCE = r'''
#include <helpers/ui/buzzer.cpp>
uint32_t now_ms=0;
int main(){
genericBuzzer buzzer;buzzer.begin();
genericBuzzer buzzer;buzzer.begin();buzzer.quiet(false);
char status[180];
auto check=[&](const char* expected){buzzer.voiceStatus(status,sizeof(status));assert(strstr(status,expected));};
auto finish_tone=[&]{
@@ -363,7 +363,7 @@ BUZZER_TONES = r'''
#include <helpers/ui/buzzer.cpp>
uint32_t now_ms=0;
int main(){
genericBuzzer buzzer;buzzer.begin();
genericBuzzer buzzer;buzzer.begin();buzzer.quiet(false);
assert(buzzer.speakGps(true));assert(buzzer.isPlaying());
assert(rtttl::melody=="Startup:d=4,o=5,b=160:16c6,16e6,8g6");
rtttl::finish();buzzer.loop();assert(!buzzer.isPlaying());
+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()
+571
View File
@@ -0,0 +1,571 @@
#!/usr/bin/env python3
"""Run the real SSD1306/SH1106 status probe against scripted I2C replies."""
from pathlib import Path
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
HARNESS = r'''
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <initializer_list>
#include <limits>
#include <vector>
#include <helpers/ui/OLEDDisplayProbe.h>
struct Sample {
uint8_t status;
uint8_t count = 1;
bool available = true;
int read = -2; // -2 supplies the status byte, -1 simulates a failed read.
};
struct ScriptedWire {
uint8_t expected_address = 0x3C;
uint8_t probe_error = 0;
std::vector<Sample> samples;
std::vector<uint8_t> status_errors;
std::vector<std::vector<uint8_t>> writes;
unsigned requests = 0, reads = 0, ends = 0, write_calls = 0;
unsigned failed_write = std::numeric_limits<unsigned>::max();
Sample current{};
void beginTransmission(uint8_t address) {
assert(address == expected_address);
writes.push_back({});
}
size_t write(uint8_t byte) {
assert(!writes.empty());
writes.back().push_back(byte);
return write_calls++ == failed_write ? 0 : 1;
}
uint8_t endTransmission(bool = true) {
const unsigned transaction = ends++;
if (transaction == 0) return probe_error;
const unsigned sample = transaction - 1;
return sample < status_errors.size() ? status_errors[sample] : 0;
}
uint8_t requestFrom(uint8_t address, uint8_t count) {
assert(address == expected_address);
assert(count == 1);
assert(requests < 4);
current = requests < samples.size() ? samples[requests] : Sample{0};
++requests;
return current.count;
}
int available() { return current.available ? current.count : 0; }
int read() {
++reads;
return current.read == -2 ? current.status : current.read;
}
void checkBus() const {
assert(ends <= 5);
assert(requests <= 4);
assert(reads <= requests);
assert(!writes.empty());
assert(writes.front().empty()); // Address probe does not alter the panel.
for (size_t i = 1; i < writes.size(); ++i) {
assert(writes[i] == std::vector<uint8_t>{0x00});
}
}
};
using mesh::ui::OLEDController;
static OLEDController detect(ScriptedWire& wire) {
const auto controller = mesh::ui::detectOLEDController(wire, wire.expected_address);
wire.checkBus();
return controller;
}
static ScriptedWire status(std::initializer_list<uint8_t> values) {
ScriptedWire wire;
for (uint8_t value : values) wire.samples.push_back({value});
return wire;
}
int main() {
for (uint8_t address : {0x3C, 0x3D}) {
for (uint8_t error : {1, 2, 3, 4, 5}) {
ScriptedWire wire;
wire.expected_address = address;
wire.probe_error = error;
assert(detect(wire) == OLEDController::Absent);
assert(wire.requests == 0 && wire.reads == 0 && wire.ends == 1);
}
auto wire = status({0x08, 0x08});
wire.expected_address = address;
assert(detect(wire) == OLEDController::SH1106);
assert(wire.requests == 2);
}
// Only the status low nibble identifies the controller. Busy bit 0x80
// alone is also seen on SSD1306 hardware and must not select SH1106.
for (unsigned value = 0; value <= 255; ++value) {
auto wire = status({uint8_t(value), uint8_t(value),
uint8_t(value), uint8_t(value)});
const auto expected = (value & 0x0F) == 0x08
? OLEDController::SH1106 : OLEDController::SSD1306;
assert(detect(wire) == expected);
assert(wire.requests == 2);
}
for (auto values : {
std::initializer_list<uint8_t>{8, 0, 8, 0},
{0, 8, 0, 8}, {8, 0x80, 8, 0x80}, {0xFF, 8, 0xFF, 8}}) {
auto wire = status(values);
assert(detect(wire) == OLEDController::SSD1306);
assert(wire.requests == 4);
}
for (auto values : {
std::initializer_list<uint8_t>{0, 8, 0x88},
{0, 1, 8, 8}, {8, 0, 8, 8}}) {
auto wire = status(values);
assert(detect(wire) == OLEDController::SH1106);
assert(wire.requests >= 3 && wire.requests <= 4);
}
// Two ordinary stable statuses identify SSD1306 without wasting all four
// reads, and must not be overturned by later unrelated reply bytes.
auto stable_ssd = status({0x00, 0x80, 8, 8});
assert(detect(stable_ssd) == OLEDController::SSD1306);
assert(stable_ssd.requests == 2);
// Incomplete and failed samples may never complete a matching pair.
for (unsigned failure = 0; failure < 4; ++failure) {
auto wire = status({8, 8, 0, 0});
if (failure == 0) wire.samples[1].count = 0;
if (failure == 1) wire.samples[1].available = false;
if (failure == 2) wire.samples[1].read = -1;
if (failure == 3) wire.samples[1].count = 2;
assert(detect(wire) == OLEDController::SSD1306);
assert(wire.requests == 2);
}
for (unsigned failed_sample = 0; failed_sample < 4; ++failed_sample) {
auto wire = status({0, 1, 2, 3});
wire.status_errors.resize(4, 0);
wire.status_errors[failed_sample] = 2;
assert(detect(wire) == OLEDController::SSD1306);
assert(wire.requests == failed_sample);
assert(wire.ends == failed_sample + 2);
}
for (unsigned failed_sample = 0; failed_sample < 4; ++failed_sample) {
auto wire = status({0, 1, 2, 3});
wire.failed_write = failed_sample;
assert(detect(wire) == OLEDController::SSD1306);
assert(wire.requests == failed_sample);
}
// No detection state carries between boots, addresses, or missing panels.
auto sh = status({8, 8});
auto ssd = status({0x80, 0x80, 0x80, 0x80});
ScriptedWire missing;
missing.probe_error = 2;
assert(detect(sh) == OLEDController::SH1106);
assert(detect(ssd) == OLEDController::SSD1306);
assert(detect(missing) == OLEDController::Absent);
std::puts("PASS: OLED controller status detection, errors and sample bounds");
}
'''
ARDUINO = r'''
#pragma once
#include <cstdint>
#include <utility>
#include <vector>
#define OUTPUT 1
#define HIGH 1
#define LOW 0
inline std::vector<std::pair<int, int>> pin_modes, pin_levels;
inline std::vector<unsigned> delays;
inline void pinMode(int pin, int mode) { pin_modes.push_back({pin, mode}); }
inline void digitalWrite(int pin, int level) { pin_levels.push_back({pin, level}); }
inline void delay(unsigned ms) { delays.push_back(ms); }
'''
WIRE = r'''
#pragma once
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <map>
#include <vector>
struct TwoWire {
struct Transaction { uint8_t address; std::vector<uint8_t> data; };
std::vector<Transaction> transactions;
std::vector<uint32_t> clocks;
std::map<uint8_t, bool> present{{0x3C, true}, {0x3D, false}};
std::map<uint8_t, std::vector<int>> status{{0x3C, {8, 8, 8, 8}}};
std::map<uint8_t, unsigned> requests;
unsigned begun = 0, byte_writes = 0, block_writes = 0;
unsigned failed_tx = std::numeric_limits<unsigned>::max();
unsigned failed_byte = std::numeric_limits<unsigned>::max();
unsigned failed_block = std::numeric_limits<unsigned>::max();
int current_read = 0;
void begin() { ++begun; }
void setClock(uint32_t value) { clocks.push_back(value); }
void beginTransmission(uint8_t address) { transactions.push_back({address, {}}); }
size_t write(uint8_t byte) {
assert(!transactions.empty());
transactions.back().data.push_back(byte);
return byte_writes++ == failed_byte ? 0 : 1;
}
size_t write(const uint8_t* bytes, size_t count) {
assert(!transactions.empty());
transactions.back().data.insert(transactions.back().data.end(), bytes, bytes + count);
assert(transactions.back().data.size() <= 32);
return block_writes++ == failed_block ? count - 1 : count;
}
uint8_t endTransmission(bool = true) {
const auto& tx = transactions.back();
return !present[tx.address] || transactions.size() - 1 == failed_tx ? 2 : 0;
}
uint8_t requestFrom(uint8_t address, uint8_t count) {
assert(count == 1);
const unsigned index = requests[address]++;
current_read = index < status[address].size() ? status[address][index] : 0;
return present[address] ? 1 : 0;
}
int available() { return 1; }
int read() { return current_read; }
};
inline TwoWire Wire;
'''
VENDOR = r'''
#pragma once
#include <Arduino.h>
#include <Wire.h>
#include <cstdlib>
#include <cstring>
#define SSD1306_SWITCHCAPVCC 2
void* test_malloc(size_t count);
void test_free(void* pointer);
class Adafruit_SSD1306 {
protected:
const uint16_t WIDTH, HEIGHT;
TwoWire* wire;
uint8_t* buffer = nullptr;
int8_t rstPin, i2caddr = 0, vccstate = 0;
uint32_t wireClk, restoreClk;
public:
unsigned base_begin_calls = 0, base_display_calls = 0;
bool fail_begin = false, last_reset = true, last_periph_begin = true;
Adafruit_SSD1306(uint8_t width, uint8_t height, TwoWire* bus = &Wire,
int8_t reset = -1, uint32_t during = 400000, uint32_t after = 100000)
: WIDTH(width), HEIGHT(height), wire(bus), rstPin(reset),
wireClk(during), restoreClk(after) {}
~Adafruit_SSD1306() { test_free(buffer); }
bool begin(uint8_t vcc, uint8_t address, bool reset, bool periph_begin) {
++base_begin_calls;
last_reset = reset;
last_periph_begin = periph_begin;
if (fail_begin) return false;
if (!buffer) buffer = static_cast<uint8_t*>(test_malloc(WIDTH * ((HEIGHT + 7) / 8)));
if (!buffer) return false;
i2caddr = address;
vccstate = vcc;
clearDisplay();
return true;
}
void clearDisplay() { std::memset(buffer, 0, WIDTH * ((HEIGHT + 7) / 8)); }
uint8_t* getBuffer() { return buffer; }
void display() { ++base_display_calls; }
void ssd1306_command(uint8_t command) {
wire->beginTransmission(uint8_t(i2caddr));
wire->write(uint8_t(0));
wire->write(command);
wire->endTransmission();
}
};
'''
BACKEND_HARNESS = r'''
#include <cassert>
#include <cstdlib>
#include <stdlib.h>
#include <cstdio>
#include <algorithm>
#include <Adafruit_SSD1306.h>
static unsigned active_allocations = 0, total_allocations = 0;
static bool allocation_fails = false;
void* test_malloc(size_t count) {
assert(count == 1024);
if (allocation_fails) return nullptr;
void* result = std::malloc(count);
assert(result);
++active_allocations;
++total_allocations;
return result;
}
void test_free(void* pointer) {
if (!pointer) return;
assert(active_allocations == 1);
--active_allocations;
std::free(pointer);
}
#define malloc test_malloc
#include <helpers/ui/AutoOLEDBackend.h>
#undef malloc
using mesh::ui::OLEDController;
static void resetPins() { pin_modes.clear(); pin_levels.clear(); delays.clear(); }
static unsigned totalRequests(const TwoWire& wire) {
unsigned count = 0;
for (const auto& entry : wire.requests) count += entry.second;
return count;
}
[[maybe_unused]] static void beginSH(AutoOLEDBackend& display, TwoWire& wire) {
assert(display.begin(SSD1306_SWITCHCAPVCC, 0x3C, true, false));
assert(display.controller() == OLEDController::SH1106);
assert(display.base_begin_calls == 0);
assert(display.getBuffer() != nullptr);
assert(active_allocations == 1);
assert(wire.begun == 0);
assert(wire.clocks.back() == 100000);
}
[[maybe_unused]] static void verifyFrame(AutoOLEDBackend& display, TwoWire& wire) {
uint8_t* buffer = display.getBuffer();
for (unsigned i = 0; i < 1024; ++i) buffer[i] = uint8_t((i * 37 + i / 128) & 255);
const size_t first = wire.transactions.size();
display.display();
assert(display.base_display_calls == 0);
size_t index = first;
for (unsigned page = 0; page < 8; ++page) {
const auto& command = wire.transactions.at(index++);
assert(command.address == 0x3C);
assert(command.data == (std::vector<uint8_t>{0x00, uint8_t(0xB0 | page), 0x02, 0x10}));
unsigned column = 0;
while (column < 128) {
const auto& data = wire.transactions.at(index++);
assert(data.address == 0x3C);
const unsigned count = std::min(31u, 128u - column);
assert(data.data.size() == count + 1);
assert(data.data[0] == 0x40);
for (unsigned i = 0; i < count; ++i)
assert(data.data[i + 1] == buffer[page * 128 + column + i]);
column += count;
}
}
assert(index == wire.transactions.size());
assert(wire.clocks.back() == 100000);
}
int main() {
resetPins();
#if defined(OLED_CONTROLLER_OVERRIDE) && OLED_CONTROLLER_OVERRIDE == 1
{
TwoWire wire;
AutoOLEDBackend display(128, 64, &wire, -1);
assert(display.begin(2, 0x3C, true, true));
assert(display.controller() == OLEDController::SSD1306);
assert(display.base_begin_calls == 1);
assert(!display.last_reset && !display.last_periph_begin);
assert(wire.begun == 1 && totalRequests(wire) == 0);
display.display();
assert(display.base_display_calls == 1);
}
#else
{
TwoWire wire;
#if defined(OLED_CONTROLLER_OVERRIDE) && OLED_CONTROLLER_OVERRIDE == 2
wire.status[0x3C] = {0, 0};
#endif
AutoOLEDBackend display(128, 64, &wire, 9);
beginSH(display, wire);
assert(pin_modes == (std::vector<std::pair<int, int>>{{9, OUTPUT}}));
assert(pin_levels == (std::vector<std::pair<int, int>>{{9, HIGH}, {9, LOW}, {9, HIGH}}));
assert(delays == (std::vector<unsigned>{1, 10, 100}));
assert(totalRequests(wire) ==
#if defined(OLED_CONTROLLER_OVERRIDE) && OLED_CONTROLLER_OVERRIDE == 2
0
#else
2
#endif
);
const std::vector<uint8_t> init = {
0x00, 0xAE, 0xD5, 0x80, 0xA8, 0x3F, 0xD3, 0x00, 0x40,
0xAD, 0x8B, 0xA1, 0xC8, 0xDA, 0x12, 0x81, 0xFF,
0xD9, 0x1F, 0xDB, 0x40, 0x33, 0xA6, 0x20, 0x10, 0xA4
};
assert(wire.transactions[wire.transactions.size() - 2].data == init);
assert(wire.transactions.back().data == (std::vector<uint8_t>{0, 0xAF}));
verifyFrame(display, wire);
const auto* original = display.getBuffer();
const auto allocations = total_allocations;
resetPins();
assert(display.begin(2, 0x3C, false, false));
assert(display.getBuffer() == original && total_allocations == allocations);
assert(pin_modes.empty() && pin_levels.empty());
verifyFrame(display, wire);
}
assert(active_allocations == 0);
// Initialization and frame errors stop further writes, restore the bus
// clock, and leave a bounded framebuffer available for a later retry.
for (unsigned failure = 0; failure < 8; ++failure) {
TwoWire wire;
AutoOLEDBackend display(128, 64, &wire, -1);
const unsigned init =
#if defined(OLED_CONTROLLER_OVERRIDE) && OLED_CONTROLLER_OVERRIDE == 2
1;
#else
3;
#endif
if (failure < 2) wire.failed_tx = init + failure;
if (failure == 2) wire.failed_byte = init - 1;
if (failure == 3) wire.failed_block = 0;
if (failure < 4) {
assert(!display.begin(2, 0x3C, false, false));
assert(wire.clocks.back() == 100000);
const auto transfers = wire.transactions.size();
display.display();
display.ssd1306_command(0xAF);
assert(wire.transactions.size() == transfers);
} else {
beginSH(display, wire);
const auto transfers = wire.transactions.size();
if (failure == 4) wire.failed_tx = transfers;
if (failure == 5) wire.failed_block = wire.block_writes + 1;
if (failure == 6) wire.failed_tx = transfers + 1;
if (failure == 7) wire.failed_byte = wire.byte_writes + 1;
display.display();
assert(wire.transactions.size() == transfers + (failure == 4 ? 1 : 2));
assert(wire.clocks.back() == 100000);
}
wire.failed_tx = wire.failed_byte = wire.failed_block = ~0u;
wire.requests.clear();
const auto* original = display.getBuffer();
const auto allocations = total_allocations;
assert(display.begin(2, 0x3C, false, false));
assert(display.getBuffer() == original && total_allocations == allocations);
verifyFrame(display, wire);
}
assert(active_allocations == 0);
{
TwoWire wire;
AutoOLEDBackend display(128, 64, &wire, -1);
allocation_fails = true;
assert(!display.begin(2, 0x3C, false, false));
assert(display.getBuffer() == nullptr && active_allocations == 0);
const auto transfers = wire.transactions.size();
display.display();
display.ssd1306_command(0xAF);
assert(wire.transactions.size() == transfers);
allocation_fails = false;
wire.requests.clear();
beginSH(display, wire);
}
#endif
assert(active_allocations == 0);
#if !defined(OLED_CONTROLLER_OVERRIDE) || OLED_CONTROLLER_OVERRIDE == 0
{
TwoWire wire;
wire.status[0x3C] = {0, 0};
AutoOLEDBackend display(128, 64, &wire, -1);
assert(display.begin(2, 0x3C, false, false));
const auto* original = display.getBuffer();
const auto allocations = total_allocations;
wire = TwoWire{};
assert(display.begin(2, 0x3C, false, false));
assert(display.controller() == OLEDController::SH1106);
assert(display.getBuffer() == original && total_allocations == allocations);
verifyFrame(display, wire);
wire = TwoWire{};
wire.present[0x3C] = false;
assert(!display.begin(2, 0x3C, false, false));
assert(display.controller() == OLEDController::Absent);
const auto transfers = wire.transactions.size();
display.display();
assert(wire.transactions.size() == transfers);
wire = TwoWire{};
wire.status[0x3C] = {0x80, 0x80};
assert(display.begin(2, 0x3C, false, false));
assert(display.controller() == OLEDController::SSD1306);
assert(display.getBuffer() == original && total_allocations == allocations);
display.display();
assert(display.base_display_calls == 1 && display.base_begin_calls == 2);
}
assert(active_allocations == 0);
for (bool fail : {false, true}) {
TwoWire wire;
wire.status[0x3C] = {0x00, 0x80};
AutoOLEDBackend display(128, 64, &wire, -1);
display.fail_begin = fail;
assert(display.begin(2, 0x3C, true, true) == !fail);
assert(display.controller() == OLEDController::SSD1306);
assert(display.base_begin_calls == 1);
assert(!display.last_reset && !display.last_periph_begin);
assert(wire.begun == 1);
display.display();
assert(display.base_display_calls == unsigned(!fail));
}
#endif
{
TwoWire wire;
wire.present[0x3C] = false;
AutoOLEDBackend display(128, 64, &wire, -1);
display.ssd1306_command(0xAE);
assert(wire.transactions.empty());
assert(!display.begin(2, 0x3C, true, false));
assert(display.controller() == OLEDController::Absent);
assert(display.getBuffer() == nullptr);
assert(totalRequests(wire) == 0);
const auto transfers = wire.transactions.size();
display.display();
display.ssd1306_command(0xAE);
assert(wire.transactions.size() == transfers);
}
assert(active_allocations == 0);
std::puts("PASS: OLED backend selection, single buffer, page writes and retry");
}
'''
class OledDisplayProbeTests(unittest.TestCase):
def compile_run(self, source_text, definitions=(), mocks=None):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
if compiler is None:
self.skipTest("a host C++17 compiler is required")
with tempfile.TemporaryDirectory(prefix="meshcore-oled-probe-") as directory:
work = Path(directory)
source = work / "probe.cpp"
source.write_text(source_text, encoding="ascii")
for name, contents in (mocks or {}).items():
(work / name).write_text(contents, encoding="ascii")
binary = work / "probe"
sanitizer = (["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-pie", "-no-pie"] if sys.platform.startswith("linux") else [])
built = subprocess.run([
compiler, "-std=c++17", "-O1", "-Wall", "-Wextra", "-Werror",
*sanitizer, *definitions, "-I", str(work),
"-I", str(ROOT / "src"), str(source), "-o", str(binary),
], text=True, capture_output=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
checked = subprocess.run([str(binary)], text=True, capture_output=True, timeout=15)
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
return checked.stdout
def test_real_detector_selects_only_repeated_sh1106_status(self):
self.assertIn("PASS: OLED controller status detection", self.compile_run(HARNESS))
def test_real_backend_transfers_and_failure_recovery(self):
mocks = {"Arduino.h": ARDUINO, "Wire.h": WIRE, "Adafruit_SSD1306.h": VENDOR}
for definitions in ((), ("-DOLED_CONTROLLER_OVERRIDE=1",),
("-DOLED_CONTROLLER_OVERRIDE=2",)):
with self.subTest(definitions=definitions):
output = self.compile_run(BACKEND_HARNESS, definitions, mocks)
self.assertIn("PASS: OLED backend selection", output)
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
+1
View File
@@ -16,6 +16,7 @@ build_flags = ${nrf52_base.build_flags}
; UICR and reset once before the external SX1262 can be reached.
-D CONFIG_NFCT_PINS_AS_GPIOS
-D NRF52_POWER_MANAGEMENT
-D OLED_AUTO_DETECT=1 ; shared WisBlock SSD1306 / SH1106 screen support
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
-D LORA_TX_POWER=22
+1
View File
@@ -19,6 +19,7 @@ build_flags = ${nrf52_base.build_flags}
; The boot power policy automatically protects Ethernet/PoE and external NOR.
-D PIN_GPS_EN=-1
-D PIN_OLED_RESET=-1
-D OLED_AUTO_DETECT=1 ; SSD1306 / SH1106, including WisMesh Pocket v2 and v3
-D USE_SX1262
-D RADIO_CLASS=CustomSX1262
-D WRAPPER_CLASS=CustomSX1262Wrapper
@@ -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));
Loaded 100 of 119 files, more files were not shown because too many files have changed in this diff. Show more