Add saved Companion sound and vibration controls
Default fresh settings to sound off and vibration on, and keep existing saved choices. Apply changes immediately across all three UIs and make custom notification patterns respect the same mute controls. Keep the Wio Mini Companion within its existing flash boundary by storing its LED switch in a reserved preference byte and shortening CLI help without removing commands. Add coverage for live output changes, persistence, and storage failures. Adapted from https://github.com/meshcore-dev/MeshCore/pull/3603. Co-authored-by: R0ck <develop@hectospark.co.uk>
This commit is contained in:
1 parent
9d52b1fd68
commit
f57dffa2be
30 files changed
+1074
-108
No files matched your search
@@ -971,7 +971,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
|
||||
|
||||
@@ -1958,6 +1958,25 @@ 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
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
@@ -294,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.
|
||||
@@ -2833,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
|
||||
@@ -3320,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;
|
||||
@@ -10315,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");
|
||||
@@ -10363,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");
|
||||
@@ -10447,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"
|
||||
@@ -10475,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"
|
||||
@@ -10508,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`,
|
||||
@@ -10726,7 +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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -3635,7 +3635,9 @@ void setup() {
|
||||
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();
|
||||
@@ -2276,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;
|
||||
@@ -2730,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) {
|
||||
@@ -2739,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
|
||||
@@ -719,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();
|
||||
@@ -1132,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();
|
||||
@@ -867,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;
|
||||
@@ -1167,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) {
|
||||
@@ -1176,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);
|
||||
|
||||
@@ -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
|
||||
@@ -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; }
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -278,6 +278,7 @@ namespace leds { bool handleCommand(const char*,char*,size_t) { return false; }
|
||||
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());
|
||||
|
||||
Reference in new issue
Block a user