Merge remote-tracking branch 'origin/keymindCascade' into keymindCascade
This commit is contained in:
commit
bc1176c7f4
119 files changed
+3288
-173
No files matched your search
@@ -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
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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")])
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -77,7 +77,7 @@ void DRV2605Vibration::stop() {
|
||||
|
||||
void DRV2605Vibration::pulse(bool on) {
|
||||
if (!_ready) return;
|
||||
if (!on) {
|
||||
if (!on || _quiet) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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];
|
||||
|
||||
@@ -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()
|
||||
@@ -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());
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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){
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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@
|
||||
|
||||
@@ -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;")
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
@@ -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()
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in new issue
Block a user