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:
mikecarperandR0ck committed 2026-10-20 09:33:43 -07:00
1 parent 9d52b1fd68
commit f57dffa2be
30 files changed
+1074 -108

No files matched your search

+7
View File
@@ -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
+19
View File
@@ -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(); }
+61 -60
View File
@@ -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)
+4
View File
@@ -295,6 +295,10 @@ public:
void setNotificationOutputMute(mesh::notify::Output, bool) {}
#endif
bool handleNotificationCommand(const char* command, char* reply, size_t size);
bool handleAlertCommand(const char* command, char* reply, size_t size);
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)
bool handleCompactLedCommand(const char* command, char* reply, size_t size);
#endif
#if defined(STM32_PLATFORM)
// Keep this large dispatcher separate from contact streaming on flash-
// constrained STM32 targets; merging their branches increases code size.
+5 -1
View File
@@ -49,8 +49,12 @@ public:
float rx_delay_base = 0;
uint32_t ble_pin = 0;
uint8_t advert_loc_policy = 0;
uint8_t buzzer_quiet = 0;
uint8_t buzzer_quiet = 1;
uint8_t gps_enabled = 0; // GPS enabled flag (0=disabled, 1=enabled)
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)
// Uses existing alignment space; stored in the reserved wire byte at 79.
uint8_t leds_quiet = 0;
#endif
uint32_t gps_interval = 0; // seconds; 0 uses the 1-second default
uint8_t autoadd_config = 0; // bitmask for auto-add contacts config
uint8_t rx_boosted_gain = 0; // SX126x RX boosted gain mode
@@ -2,6 +2,88 @@
#include "target.h"
#include "NotificationSettingsFile.h"
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)
bool MyMesh::handleCompactLedCommand(const char* command, char* reply, size_t size) {
if (!command || !reply || !size) return false;
if (strcmp(command, "get leds") == 0) {
snprintf(reply, size, "> %s", mesh::leds::enabled() ? "on" : "off");
return true;
}
if (strncmp(command, "set leds", 8) != 0
|| (command[8] && command[8] != ' ' && command[8] != '\t')) return false;
const char* value = command + 8;
while (*value == ' ' || *value == '\t') ++value;
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
snprintf(reply, size, "Error: usage set leds on|off");
return true;
}
const bool on = strcmp(value, "on") == 0;
const uint8_t previous = _prefs.leds_quiet;
_prefs.leds_quiet = !on;
if (_prefs.leds_quiet != previous) {
if (!savePrefs()) {
_prefs.leds_quiet = previous;
snprintf(reply, size, "Error: LED setting save unconfirmed; reboot may differ");
return true;
}
}
mesh::leds::setEnabled(on);
board.applyLedPolicy();
mesh::leds::applyStatusPolicy();
snprintf(reply, size, "OK");
return true;
}
#endif
bool MyMesh::handleAlertCommand(const char* command, char* reply, size_t size) {
if (!command || !reply || !size) return false;
const bool get = strncmp(command, "get ", 4) == 0;
if (!get && strncmp(command, "set ", 4) != 0) return false;
const char* name = command + 4;
uint8_t* quiet = nullptr;
size_t length = 0;
bool supported = false;
if (strncmp(name, "buzzer", 6) == 0) {
length = 6;
quiet = &_prefs.buzzer_quiet;
#ifdef PIN_BUZZER
supported = true;
#endif
} else if (strncmp(name, "vibration", 9) == 0) {
length = 9;
quiet = &_prefs.vibe_quiet;
#if defined(PIN_VIBRATION) || defined(HAS_DRV2605)
supported = true;
#endif
} else return false;
const char* value = name + length;
if (*value && *value != ' ' && *value != '\t') return false;
if (!supported) {
snprintf(reply, size, "Error: %.*s unavailable on this build", int(length), name);
return true;
}
if (get) {
if (*value) snprintf(reply, size, "Error: usage get %.*s", int(length), name);
else snprintf(reply, size, "> %s", *quiet ? "off" : "on");
return true;
}
while (*value == ' ' || *value == '\t') ++value;
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
snprintf(reply, size, "Error: usage set %.*s on|off", int(length), name);
return true;
}
const uint8_t previous = *quiet;
*quiet = strcmp(value, "off") == 0;
if (*quiet != previous && !savePrefs()) {
*quiet = previous;
snprintf(reply, size, "Error: failed to save %.*s setting", int(length), name);
return true;
}
if (_ui) _ui->syncAlertPreferences();
snprintf(reply, size, "OK");
return true;
}
#if COMPANION_FEATURE_NOTIFICATIONS
// The UI's STATUS_LED_RGB define is local to its own translation unit.
uint8_t MyMesh::capabilities() const {
@@ -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),
+2
View File
@@ -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");
}
+17 -6
View File
@@ -1965,6 +1965,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, CompanionNode
#if defined(PIN_VIBRATION) || defined(HAS_DRV2605)
vibration.begin();
vibration.quiet(_node_prefs->vibe_quiet);
#endif
ui_started_at = millis();
@@ -2051,7 +2052,7 @@ if (_notification_outputs & mesh::notify::Sound) switch(t){
}
#endif
#ifdef PIN_VIBRATION
#if defined(PIN_VIBRATION) || defined(HAS_DRV2605)
// Trigger vibration for all UI events except none
if ((_notification_outputs & mesh::notify::Vibration) && t != UIEventType::none) {
vibration.trigger();
@@ -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
}
+2 -1
View File
@@ -43,7 +43,7 @@ class UITask : public AbstractUITask {
unsigned long _pairing_screen_until;
bool _deferred_msg_preview;
bool _startup_complete = false;
CompanionNodePrefs* _node_prefs;
CompanionNodePrefs* _node_prefs = nullptr;
char _alert[80];
uint32_t _alert_expiry;
int _msgcount;
@@ -200,6 +200,7 @@ public:
void notificationVibration(bool on) override;
void notificationMelody(const char* text) override;
void notificationScreen(int8_t mode) override;
void syncAlertPreferences() override;
void loop() override;
void shutdown(bool restart = false);
+17 -10
View File
@@ -124,11 +124,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, CompanionNode
buzzer.startup();
#endif
#ifdef HAS_DRV2605
#if defined(PIN_VIBRATION) || defined(HAS_DRV2605)
vibration.begin();
vibration.quiet(_node_prefs->vibe_quiet);
#elif defined(PIN_VIBRATION)
vibration.begin();
#endif
// Initialize digital button if available
@@ -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
}
+2 -1
View File
@@ -32,7 +32,7 @@ class UITask : public AbstractUITask {
GenericVibration vibration;
#endif
uint32_t _next_refresh;
CompanionNodePrefs* _node_prefs;
CompanionNodePrefs* _node_prefs = nullptr;
char _version_info[32];
char _origin[62];
#if UI_SMALL_MESSAGE_FONT == 1
@@ -125,6 +125,7 @@ public:
void notificationVibration(bool on) override;
void notificationMelody(const char* text) override;
void notificationScreen(int8_t mode) override;
void syncAlertPreferences() override;
void loop() override;
#ifdef MESH_BUTTON_AUDIO_HIL
bool handleButtonAudioTest(const char* command, char* reply, size_t size) override;
+17 -6
View File
@@ -666,6 +666,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, CompanionNode
#if defined(PIN_VIBRATION) || defined(HAS_DRV2605)
vibration.begin();
vibration.quiet(_node_prefs->vibe_quiet);
#endif
ui_started_at = millis();
@@ -708,7 +709,7 @@ if (_notification_outputs & mesh::notify::Sound) switch(t){
}
#endif
#ifdef PIN_VIBRATION
#if defined(PIN_VIBRATION) || defined(HAS_DRV2605)
// Trigger vibration for all UI events except none
if ((_notification_outputs & mesh::notify::Vibration) && t != UIEventType::none) {
vibration.trigger();
@@ -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
}
+2 -1
View File
@@ -40,7 +40,7 @@ class UITask : public AbstractUITask {
GenericVibration vibration;
#endif
uint32_t _next_refresh;
CompanionNodePrefs* _node_prefs;
CompanionNodePrefs* _node_prefs = nullptr;
char _alert[80];
uint32_t _alert_expiry;
unsigned long _pairing_screen_until;
@@ -137,6 +137,7 @@ public:
void notificationVibration(bool on) override;
void notificationMelody(const char* text) override;
void notificationScreen(int8_t mode) override;
void syncAlertPreferences() override;
void loop() override;
void shutdown(bool restart = false);
+1 -1
View File
@@ -77,7 +77,7 @@ void DRV2605Vibration::stop() {
void DRV2605Vibration::pulse(bool on) {
if (!_ready) return;
if (!on) {
if (!on || _quiet) {
stop();
return;
}
+1 -1
View File
@@ -27,7 +27,7 @@ public:
bool isVibrating();
void stop(); // stop vibration immediately
void pulse(bool on); // continuous RTP amplitude for scheduled ms pulses
void quiet(bool q) { _quiet = q; }
void quiet(bool q) { if (q) stop(); _quiet = q; }
bool isQuiet() const { return _quiet; }
private:
+6
View File
@@ -8,6 +8,7 @@ void GenericVibration::begin() {
}
void GenericVibration::trigger() {
if (_quiet) return;
duration = millis();
digitalWrite(PIN_VIBRATION, HIGH);
}
@@ -35,4 +36,9 @@ void GenericVibration::stop() {
digitalWrite(PIN_VIBRATION, LOW);
}
void GenericVibration::pulse(bool on) {
stop();
if (on && !_quiet) digitalWrite(PIN_VIBRATION, HIGH);
}
#endif // ifdef PIN_VIBRATION
+5 -1
View File
@@ -25,9 +25,13 @@ public:
void loop(); // non-blocking timer handling
bool isVibrating(); // returns true if currently vibrating
void stop(); // stop vibration immediately
void pulse(bool on);
void quiet(bool q) { if (q) stop(); _quiet = q; }
bool isQuiet() const { return _quiet; }
private:
unsigned long duration;
unsigned long duration = 0;
bool _quiet = false;
};
#endif // ifdef PIN_VIBRATION
+9 -9
View File
@@ -48,16 +48,14 @@ static mesh::audio::VoiceClip buttonVoiceClip(ButtonVoicePrompt prompt) {
#endif
void genericBuzzer::begin() {
// Serial.print("DBG: Setting up buzzer on pin ");
// Serial.println(PIN_BUZZER);
#ifdef PIN_BUZZER_EN
pinMode(PIN_BUZZER_EN, OUTPUT);
digitalWrite(PIN_BUZZER_EN, HIGH);
#endif
quiet(false);
digitalWrite(PIN_BUZZER, LOW);
pinMode(PIN_BUZZER, OUTPUT);
digitalWrite(PIN_BUZZER, LOW); // need to pull low by default to avoid extreme power draw
#ifdef PIN_BUZZER_EN
digitalWrite(PIN_BUZZER_EN, LOW);
pinMode(PIN_BUZZER_EN, OUTPUT);
#endif
// Start muted until the UI applies the saved preference.
quiet(_is_quiet);
}
void genericBuzzer::play(const char *melody) {
@@ -79,6 +77,8 @@ void genericBuzzer::play(const char *melody) {
}
void genericBuzzer::playNotification(const char* melody) {
// Muted background alerts must not interrupt an explicit button prompt.
if (_is_quiet) return;
stop();
#ifdef PIN_BUZZER_EN
digitalWrite(PIN_BUZZER_EN, HIGH);
+1
View File
@@ -71,6 +71,7 @@ struct UITask {
unsigned battery_reads = 0, shutdowns = 0, profile_services = 0;
char _alert[80] = {}, _origin[80] = {}, _msg[80] = {};
void servicePairingState() {}
void syncAlertPreferences() {}
void serviceWiFiToggleButton() {}
bool isPairingScreenActive() const { return false; }
bool isButtonGesturePending() const { return gesture_pending; }
+6 -4
View File
@@ -40,7 +40,7 @@ int main(){
{ButtonVoicePrompt::AlertsSilent,alertsSilentClip,nullptr},
#endif
};
genericBuzzer buzzer;buzzer.begin();
genericBuzzer buzzer;buzzer.begin();buzzer.quiet(false);
auto finish=[&]{
assert(!registers.ENABLE);
rtttl::finish();buzzer.loop();
@@ -548,7 +548,7 @@ NOTIFICATION_STOPS = r'''
#include <helpers/ui/buzzer.cpp>
uint32_t now_ms=0;
int main(){
genericBuzzer buzzer;buzzer.begin();
genericBuzzer buzzer;buzzer.begin();buzzer.quiet(false);
buzzer.playNotification("ordinary:d=4,o=5,b=120:c");
assert(buzzer.isPlaying()&&rtttl::isPlaying());
buzzer.stopNotification();assert(!buzzer.isPlaying()&&!rtttl::isPlaying());
@@ -590,11 +590,13 @@ int main(){
assert(!buzzer.isPlaying()&&!rtttl::isPlaying());
buzzer.quiet(true);
#endif
buzzer.playNotification("ordinary:d=4,o=5,b=120:c");
assert(!buzzer.isPlaying()&&buzzer.isQuiet()&&pin_states[PIN_BUZZER_EN]==LOW);
buzzer.quiet(false);
buzzer.playNotification("ordinary:d=4,o=5,b=120:c");
assert(buzzer.isPlaying()&&pin_states[PIN_BUZZER_EN]==HIGH);
buzzer.stopNotification();
assert(!buzzer.isPlaying()&&!rtttl::isPlaying()&&buzzer.isQuiet());
assert(pin_states[PIN_BUZZER_EN]==LOW);
assert(!buzzer.isPlaying()&&!rtttl::isPlaying()&&!buzzer.isQuiet());
}
'''
+217
View File
@@ -0,0 +1,217 @@
"""Run the Companion alert CLI against storage and live-output fixtures."""
from pathlib import Path
import os
import re
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
def alert_handler():
source = (ROOT / "examples/companion_radio/Notifications.cpp").read_text()
match = re.search(r"\bbool\s+MyMesh::handleAlertCommand\([^)]*\)\s*\{", source)
if not match:
raise AssertionError("production Companion alert handler is missing")
end, depth = match.end(), 1
while depth:
depth += (source[end] == "{") - (source[end] == "}")
end += 1
return source[match.start():end]
STUBS = r"""
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <string>
#include <initializer_list>
struct Prefs {
uint8_t buzzer_quiet = 1, vibe_quiet = 0;
uint32_t unrelated = 0x12345678;
};
struct UI {
Prefs* prefs;
Prefs* persisted;
unsigned syncs = 0;
bool sound_quiet = true, vibration_quiet = false;
UI(Prefs* p, Prefs* saved) : prefs(p), persisted(saved) {}
void syncAlertPreferences() {
assert(prefs->buzzer_quiet == persisted->buzzer_quiet);
assert(prefs->vibe_quiet == persisted->vibe_quiet);
++syncs;
sound_quiet = prefs->buzzer_quiet;
vibration_quiet = prefs->vibe_quiet;
}
};
class MyMesh {
public:
Prefs _prefs, saved;
UI ui{&_prefs, &saved};
UI* _ui = &ui;
unsigned saves = 0;
bool save_ok = true;
bool savePrefs() {
++saves;
if (!save_ok) return false;
saved = _prefs;
return true;
}
bool handleAlertCommand(const char*, char*, size_t);
};
"""
SCENARIOS = r"""
std::string run(MyMesh& mesh, const char* command) {
char reply[160] = {};
assert(mesh.handleAlertCommand(command, reply, sizeof(reply)));
return reply;
}
void error(MyMesh& mesh, const char* command) {
const auto before = mesh._prefs;
const unsigned saves = mesh.saves, syncs = mesh.ui.syncs;
assert(run(mesh, command).find("Error") == 0);
assert(mesh._prefs.buzzer_quiet == before.buzzer_quiet);
assert(mesh._prefs.vibe_quiet == before.vibe_quiet);
assert(mesh.saves == saves && mesh.ui.syncs == syncs);
}
void supported(MyMesh& mesh, const char* name, bool initial_on) {
const std::string get = std::string("get ") + name;
const std::string set = std::string("set ") + name;
const bool sound = std::string(name) == "buzzer";
const uint8_t other = sound ? mesh._prefs.vibe_quiet : mesh._prefs.buzzer_quiet;
assert(run(mesh, get.c_str()) == (initial_on ? "> on" : "> off"));
assert(mesh.saves == 0 && mesh.ui.syncs == 0);
for (const char* value : {"", " ", "on extra", "off extra", "1", "0", "ON", "OFF",
"true", "false", "onward", "officer"}) {
error(mesh, (set + " " + value).c_str());
}
error(mesh, set.c_str());
error(mesh, (get + " on").c_str());
// An unchanged value acknowledges the command without rewriting flash.
mesh.ui.sound_quiet = false;
mesh.ui.vibration_quiet = true;
assert(run(mesh, (set + (initial_on ? " on" : " off")).c_str()) == "OK");
assert(mesh.saves == 0 && mesh.ui.syncs == 1);
assert(mesh.ui.sound_quiet == bool(mesh._prefs.buzzer_quiet));
assert(mesh.ui.vibration_quiet == bool(mesh._prefs.vibe_quiet));
// Persist first, then apply to the running UI. Changing one output must not
// overwrite the independent preference or unrelated node settings.
assert(run(mesh, (set + (initial_on ? " off" : " on")).c_str()) == "OK");
assert(mesh.saves == 1 && mesh.ui.syncs == 2);
assert(run(mesh, get.c_str()) == (initial_on ? "> off" : "> on"));
assert((sound ? mesh._prefs.buzzer_quiet : mesh._prefs.vibe_quiet) == initial_on);
assert((sound ? mesh._prefs.vibe_quiet : mesh._prefs.buzzer_quiet) == other);
assert(mesh.saved.buzzer_quiet == mesh._prefs.buzzer_quiet);
assert(mesh.saved.vibe_quiet == mesh._prefs.vibe_quiet);
assert(mesh.saved.unrelated == 0x12345678);
assert(mesh.ui.sound_quiet == bool(mesh._prefs.buzzer_quiet));
assert(mesh.ui.vibration_quiet == bool(mesh._prefs.vibe_quiet));
const Prefs previous = mesh._prefs, persisted = mesh.saved;
mesh.save_ok = false;
assert(run(mesh, (set + (initial_on ? " on" : " off")).c_str()).find("Error") == 0);
assert(mesh.saves == 2 && mesh.ui.syncs == 2);
assert(mesh._prefs.buzzer_quiet == previous.buzzer_quiet);
assert(mesh._prefs.vibe_quiet == previous.vibe_quiet);
assert(mesh.saved.buzzer_quiet == persisted.buzzer_quiet);
assert(mesh.saved.vibe_quiet == persisted.vibe_quiet);
assert(run(mesh, get.c_str()) == (initial_on ? "> off" : "> on"));
// A build without an attached UI can still persist the board preference.
mesh._ui = nullptr;
mesh.save_ok = true;
assert(run(mesh, (set + (initial_on ? " on" : " off")).c_str()) == "OK");
assert(mesh.saves == 3 && mesh.ui.syncs == 2);
assert(run(mesh, get.c_str()) == (initial_on ? "> on" : "> off"));
}
int main() {
MyMesh buzzer, vibration;
#if EXPECT_BUZZER
supported(buzzer, "buzzer", false);
#else
error(buzzer, "get buzzer");
error(buzzer, "set buzzer on");
error(buzzer, "set buzzer off");
#endif
#if EXPECT_VIBRATION
supported(vibration, "vibration", true);
#else
error(vibration, "get vibration");
error(vibration, "set vibration on");
error(vibration, "set vibration off");
#endif
MyMesh mesh;
for (const char* command : {"get buzzerx", "get buzzer.extra", "get vibrationx",
"set buzzerx on", "set vibrationx off", "get leds", "get radio", "get notify",
"get sound", "set vibe on", "buzzer", "vibration", "get", "", "set"}) {
char reply[16] = "unchanged";
assert(!mesh.handleAlertCommand(command, reply, sizeof(reply)));
assert(strcmp(reply, "unchanged") == 0);
assert(mesh.saves == 0 && mesh.ui.syncs == 0);
}
for (const char* command : {"get buzzer", "get vibration", "set buzzer bad",
"set vibration bad", "set buzzer on", "set vibration off"}) {
for (size_t capacity = 1; capacity <= 160; ++capacity) {
char bounded[164]; memset(bounded, '#', sizeof(bounded));
memcpy(bounded, "ab|", 3);
assert(mesh.handleAlertCommand(command, bounded + 3, capacity));
assert(memcmp(bounded, "ab|", 3) == 0);
assert(bounded[3 + capacity] == '#');
assert(memchr(bounded + 3, 0, capacity));
}
}
char reply[16];
assert(!mesh.handleAlertCommand(nullptr, reply, sizeof(reply)));
assert(!mesh.handleAlertCommand("get buzzer", nullptr, sizeof(reply)));
assert(!mesh.handleAlertCommand("get buzzer", reply, 0));
}
"""
class CompanionAlertControlTests(unittest.TestCase):
def test_cli_storage_and_hardware_matrix(self):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
if not compiler:
self.skipTest("C++ compiler unavailable")
cases = (
("none", (), False, False),
("buzzer", ("PIN_BUZZER=1",), True, False),
("gpio_vibration", ("PIN_VIBRATION=2",), False, True),
("drv2605", ("HAS_DRV2605=1",), False, True),
("buzzer_gpio", ("PIN_BUZZER=1", "PIN_VIBRATION=2"), True, True),
("buzzer_drv2605", ("PIN_BUZZER=1", "HAS_DRV2605=1"), True, True),
)
source = STUBS + alert_handler() + SCENARIOS
with tempfile.TemporaryDirectory() as directory:
directory = Path(directory)
cpp = directory / "alert_cli.cpp"
cpp.write_text(source, encoding="ascii")
for name, macros, buzzer, vibration in cases:
with self.subTest(hardware=name):
exe = directory / (name + (".exe" if os.name == "nt" else ""))
flags = [f"-D{macro}" for macro in macros]
if os.name != "nt":
flags += ["-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-omit-frame-pointer", "-fno-pie", "-no-pie"]
result = subprocess.run(
[compiler, "-std=c++11", "-Wall", "-Wextra", *flags,
f"-DEXPECT_BUZZER={int(buzzer)}",
f"-DEXPECT_VIBRATION={int(vibration)}",
str(cpp), "-o", str(exe)], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
result = subprocess.run([str(exe)], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
if __name__ == "__main__":
unittest.main()
+248
View File
@@ -0,0 +1,248 @@
"""Run Companion master mute and live UI synchronization with inert drivers."""
from pathlib import Path
import os
import shutil
import subprocess
import tempfile
import unittest
import test_gps_voice_buttons as voice
from test_radio_receive_contract import method
ROOT = Path(__file__).resolve().parents[1]
ARDUINO = voice.ARDUINO.replace(
"inline unsigned pin_writes[64]={};",
"inline unsigned pin_writes[64]={}, high_writes[64]={};").replace(
"pin_states[pin]=value;++pin_writes[pin];",
"pin_states[pin]=value;++pin_writes[pin];if(value==HIGH)++high_writes[pin];")
ARDUINO += "\ninline void delay(unsigned) {}\n"
WIRE = "#pragma once\nstruct TwoWire {};\ninline TwoWire Wire;\n"
ADAFRUIT = r'''
#pragma once
#include <Arduino.h>
#include <Wire.h>
#define DRV2605_MODE_INTTRIG 0
#define DRV2605_MODE_REALTIME 5
#define DRV2605_REG_FEEDBACK 0x1a
#define DRV2605_REG_GO 0x0c
struct MockDriver {
bool found=true;
uint8_t registers[32]={};
unsigned writes=0;
};
inline MockDriver hardware;
class Adafruit_DRV2605 {
public:
bool begin(TwoWire*) { return hardware.found; }
void writeRegister8(uint8_t reg,uint8_t value) {
hardware.registers[reg]=value;++hardware.writes;
}
uint8_t readRegister8(uint8_t reg) { return hardware.registers[reg]; }
void selectLibrary(uint8_t value) { writeRegister8(3,value); }
void setMode(uint8_t value) { writeRegister8(1,value); }
void setWaveform(uint8_t slot,uint8_t value) { writeRegister8(4+slot,value); }
void setRealtimeValue(uint8_t value) { writeRegister8(2,value); }
void go() { writeRegister8(DRV2605_REG_GO,1); }
void stop() { writeRegister8(DRV2605_REG_GO,0); }
};
'''
BUZZER = r'''
#include <cassert>
#define PIN_BUZZER 25
#define PIN_BUZZER_EN 37
#define MESH_GPS_VOICE @VOICE@
#include <helpers/ui/buzzer.cpp>
uint32_t now_ms=100;
int main() {
genericBuzzer buzzer;buzzer.begin();
assert(buzzer.isQuiet()&&pin_states[PIN_BUZZER]==LOW&&pin_states[PIN_BUZZER_EN]==LOW);
assert(high_writes[PIN_BUZZER]==0&&high_writes[PIN_BUZZER_EN]==0);
buzzer.quiet(false);
buzzer.playNotification("alert:d=4,o=5,b=120:c");
assert(buzzer.isPlaying());
buzzer.quiet(true);
assert(!buzzer.isPlaying()&&pin_states[PIN_BUZZER_EN]==LOW);
auto starts=rtttl::begins;
buzzer.playNotification("alert:d=4,o=5,b=120:c");
assert(!buzzer.isPlaying()&&rtttl::begins==starts);
assert(pin_states[PIN_BUZZER_EN]==LOW);
#if MESH_GPS_VOICE
// A rejected background melody cannot cancel explicit courtesy speech.
assert(buzzer.speakButton(ButtonVoicePrompt::SoundOff,true));
buzzer.playNotification("alert:d=4,o=5,b=120:c");
buzzer.stopNotification();
assert(buzzer.isQuiet()&&buzzer.isPlaying());
auto cancelled=gps_voice.cancelled();
buzzer.loop();now_ms+=100;buzzer.loop();
assert(registers.ENABLE);
buzzer.playNotification("alert:d=4,o=5,b=120:c");
assert(registers.ENABLE&&gps_voice.cancelled()==cancelled);
buzzer.stop();
#endif
buzzer.quiet(false);
buzzer.playNotification("alert:d=4,o=5,b=120:c");
assert(buzzer.isPlaying()&&rtttl::begins==starts+1);
buzzer.stopNotification();assert(!buzzer.isPlaying());
}
'''
VIBRATION = r'''
#include <cassert>
@FLAGS@
#include <helpers/ui/GenericVibration.cpp>
#include <helpers/ui/DRV2605Vibration.cpp>
uint32_t now_ms=100;
#ifdef HAS_DRV2605
using Vibration = DRV2605Vibration;
bool active() { return hardware.registers[1]!=0x40; }
#else
using Vibration = GenericVibration;
bool active() { return pin_states[PIN_VIBRATION]==HIGH; }
#endif
int main() {
Vibration motor;motor.begin();
motor.trigger();assert(active());
motor.quiet(true);assert(!active()&&!motor.isVibrating());
motor.trigger();motor.pulse(true);motor.loop();
assert(motor.isQuiet()&&!active()&&!motor.isVibrating());
motor.pulse(false);assert(!active());
motor.quiet(false);
motor.pulse(true);assert(active());
motor.quiet(true);assert(!active());
motor.quiet(false);
now_ms+=6000;motor.trigger();assert(active());
motor.stop();assert(!active());
#ifdef HAS_DRV2605
hardware.found=false;motor.begin();
auto writes=hardware.writes;
motor.quiet(true);motor.trigger(true);motor.pulse(true);motor.pulse(false);
assert(hardware.writes==writes&&!motor.isVibrating());
#endif
}
'''
UI = r'''
#include <cassert>
#define MESH_GPS_VOICE 0
@FLAGS@
#include <helpers/ui/buzzer.cpp>
#include <helpers/ui/GenericVibration.cpp>
#include <helpers/ui/DRV2605Vibration.cpp>
uint32_t now_ms=100;
struct CompanionNodePrefs { uint8_t buzzer_quiet=0,vibe_quiet=0; };
struct UITask {
CompanionNodePrefs* _node_prefs=nullptr;
#ifdef PIN_BUZZER
genericBuzzer buzzer;
#endif
#ifdef HAS_DRV2605
DRV2605Vibration vibration;
#elif defined(PIN_VIBRATION)
GenericVibration vibration;
#endif
void syncAlertPreferences();
void notificationMelody(const char*);
void notificationVibration(bool);
};
@METHODS@
int main() {
UITask ui;ui.syncAlertPreferences();
CompanionNodePrefs prefs;ui._node_prefs=&prefs;
#ifdef PIN_BUZZER
ui.buzzer.begin();ui.buzzer.quiet(false);ui.notificationMelody("alert:d=4,o=5,b=120:c");
assert(ui.buzzer.isPlaying());
#endif
#if defined(HAS_DRV2605) || defined(PIN_VIBRATION)
ui.vibration.begin();ui.notificationVibration(true);
#endif
prefs.buzzer_quiet=prefs.vibe_quiet=1;
ui.syncAlertPreferences();
#ifdef PIN_BUZZER
assert(ui.buzzer.isQuiet()&&!ui.buzzer.isPlaying());
ui.notificationMelody("alert:d=4,o=5,b=120:c");
assert(!ui.buzzer.isPlaying());
#endif
#if defined(HAS_DRV2605) || defined(PIN_VIBRATION)
assert(ui.vibration.isQuiet()&&!ui.vibration.isVibrating());
ui.notificationVibration(true);
#ifdef HAS_DRV2605
assert(hardware.registers[1]==0x40);
auto writes=hardware.writes;
ui.syncAlertPreferences();ui.syncAlertPreferences();
assert(hardware.writes==writes); // unchanged prefs must not add I2C traffic
#else
assert(pin_states[PIN_VIBRATION]==LOW);
#endif
#endif
prefs.buzzer_quiet=prefs.vibe_quiet=0;
ui.syncAlertPreferences();
#ifdef PIN_BUZZER
ui.notificationMelody("alert:d=4,o=5,b=120:c");
assert(ui.buzzer.isPlaying());
ui.notificationMelody(nullptr);assert(!ui.buzzer.isPlaying());
#endif
#if defined(HAS_DRV2605) || defined(PIN_VIBRATION)
ui.notificationVibration(true);
#ifdef HAS_DRV2605
assert(hardware.registers[1]==DRV2605_MODE_REALTIME);
#else
assert(pin_states[PIN_VIBRATION]==HIGH);
#endif
ui.notificationVibration(false);
#endif
}
'''
class CompanionAlertDriversTests(unittest.TestCase):
def build(self, source):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
self.assertIsNotNone(compiler, "a host C++ compiler is required")
with tempfile.TemporaryDirectory(prefix="meshcore-alerts-") as directory:
directory = Path(directory)
for name, content in (("Arduino.h", ARDUINO), ("Wire.h", WIRE),
("Adafruit_DRV2605.h", ADAFRUIT),
("HardwarePWM.h", voice.PWM),
("NonBlockingRtttl.h", voice.RTTTL)):
(directory / name).write_text(content)
cpp = directory / "test.cpp"
exe = directory / ("test.exe" if os.name == "nt" else "test")
cpp.write_text(source)
result = subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-Wno-unused-parameter", "-I", str(directory),
"-I", str(ROOT / "src"), str(cpp), "-o", str(exe)],
capture_output=True, text=True, timeout=30)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
result = subprocess.run([str(exe)], capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_programmable_sound_obeys_mute_and_preserves_courtesy_speech(self):
for speech in (0, 1):
with self.subTest(speech=speech):
self.build(BUZZER.replace("@VOICE@", str(speech)))
def test_gpio_and_drv2605_mute_stops_active_effects_and_blocks_every_entry(self):
for flags in ("#define PIN_VIBRATION 12", "#define HAS_DRV2605 1"):
with self.subTest(flags=flags):
self.build(VIBRATION.replace("@FLAGS@", flags))
def test_each_ui_applies_saved_changes_without_reboot_or_repeated_driver_writes(self):
for ui in ("ui-orig", "ui-new", "ui-tiny"):
source = (ROOT / "examples/companion_radio" / ui / "UITask.cpp").read_text()
methods = "\n".join(method(source, signature) for signature in (
"void UITask::syncAlertPreferences()",
"void UITask::notificationMelody(const char* text)",
"void UITask::notificationVibration(bool on)",
))
for flags in ("#define PIN_BUZZER 25\n#define PIN_VIBRATION 12",
"#define PIN_BUZZER 25", "#define HAS_DRV2605 1", ""):
with self.subTest(ui=ui, flags=flags):
self.build(UI.replace("@FLAGS@", flags).replace("@METHODS@", methods))
loop = method(source, "void UITask::loop()")
self.assertRegex(loop, r"\{\s*syncAlertPreferences\(\);")
if __name__ == "__main__":
unittest.main()
+227
View File
@@ -0,0 +1,227 @@
"""Exercise the compact Companion LED CLI and its existing preference store."""
from pathlib import Path
import os
import re
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
def method(source, name):
match = re.search(rf"\bbool\s+MyMesh::{name}\([^)]*\)\s*\{{", source)
if not match:
raise AssertionError(f"production {name} is missing")
end, depth = match.end(), 1
while depth:
depth += (source[end] == "{") - (source[end] == "}")
end += 1
return source[match.start():end]
def startup_loader():
source = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
setup = source.split("void MyMesh::configureRadioFromPrefs() {", 1)[1]
match = re.search(
r"#if defined\(WIO_E5_MINI_HEADLESS_COMPANION_UI\)\n(.*?)#endif", setup, re.S)
if not match:
raise AssertionError("compact LED startup restore is missing")
return "void MyMesh::loadCompactLedSetting() {\n" + match.group(1) + "}\n"
STUBS = r"""
#include <helpers/LedPolicy.h>
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <initializer_list>
#include <string>
#include <vector>
std::vector<std::string> events;
struct Prefs {
uint8_t leds_quiet = 0;
uint32_t unrelated = 0x12345678;
};
struct Board {
unsigned applies = 0;
bool level = true;
void applyLedPolicy() {
events.push_back("board");
++applies;
level = mesh::leds::enabled();
}
} board;
unsigned status_applies = 0;
bool status_level = true;
namespace mesh { namespace leds {
void applyStatusPolicy() {
events.push_back("status");
++status_applies;
status_level = enabled();
}
}}
class MyMesh {
public:
Prefs _prefs;
Prefs saved;
bool save_ok = true;
unsigned saves = 0;
bool savePrefs() {
events.push_back("save");
++saves;
if (!save_ok) return false;
saved = _prefs;
return true;
}
bool handleCompactLedCommand(const char*, char*, size_t);
void loadCompactLedSetting();
};
void reset() {
mesh::leds::setEnabled(true);
board.applies = status_applies = 0;
board.level = status_level = true;
events.clear();
}
"""
SCENARIOS = r"""
std::string run(MyMesh& mesh, const char* command) {
char reply[160] = {};
assert(mesh.handleCompactLedCommand(command, reply, sizeof(reply)));
return reply;
}
int main() {
reset();
MyMesh mesh;
assert(run(mesh, "get leds") == "> on");
assert(mesh.saves == 0 && board.applies == 0 && status_applies == 0);
for (const char* command : {"set leds", "set leds ",
"set leds 1", "set leds 0", "set leds ON", "set leds OFF", "set leds true",
"set leds off extra", "set leds on extra"}) {
assert(run(mesh, command).find("Error") == 0);
assert(mesh.saves == 0 && board.applies == 0 && status_applies == 0);
assert(mesh::leds::enabled());
}
for (const char* command : {"get leds on", "get leds ", "get led", "get leds.extra", "get ledsx", "set ledsx on",
"set led on", "get buzzer", "get radio", "get", "set", ""}) {
char reply[16] = "unchanged";
assert(!mesh.handleCompactLedCommand(command, reply, sizeof(reply)));
assert(strcmp(reply, "unchanged") == 0);
}
assert(run(mesh, "set leds on") == "OK");
assert(mesh.saves == 0);
assert(mesh._prefs.leds_quiet == 0);
// An unchanged stored preference can repair stale live policy without flash.
mesh::leds::setEnabled(false);
assert(run(mesh, "set leds on") == "OK");
assert(mesh.saves == 0 && mesh::leds::enabled() && board.level && status_level);
events.clear();
const unsigned applied = board.applies, status = status_applies;
// A failed preference transaction restores the byte and leaves live output.
mesh.save_ok = false;
assert(run(mesh, "set leds off").find("Error") == 0);
assert(mesh.saves == 1 && mesh._prefs.leds_quiet == 0);
assert(mesh::leds::enabled() && mesh.saved.leds_quiet == 0);
assert(board.applies == applied && status_applies == status);
assert(events == std::vector<std::string>{"save"});
mesh.save_ok = true;
events.clear();
assert(run(mesh, "set leds off") == "OK");
assert(mesh.saves == 2 && !mesh::leds::enabled());
assert(mesh._prefs.leds_quiet == 1 && mesh.saved.leds_quiet == 1);
assert(mesh._prefs.unrelated == 0x12345678 && mesh.saved.unrelated == 0x12345678);
assert(board.applies == applied + 1 && !board.level);
assert(status_applies == status + 1 && !status_level);
assert((events == std::vector<std::string>{"save", "board", "status"}));
assert(run(mesh, "get leds") == "> off");
assert(run(mesh, "set leds off") == "OK" && mesh.saves == 2);
// Reload durable preference bytes through the actual compact startup block.
MyMesh reboot;
reboot._prefs = mesh.saved;
reset();
reboot.loadCompactLedSetting();
assert(!mesh::leds::enabled());
assert(board.applies == 1 && !board.level && status_applies == 1 && !status_level);
assert(run(reboot, "get leds") == "> off");
assert(run(reboot, "set leds off") == "OK" && reboot.saves == 0);
reboot.save_ok = false;
const unsigned before_failed_apply = board.applies, before_failed_status = status_applies;
assert(run(reboot, "set leds on").find("Error") == 0 && reboot.saves == 1);
assert(reboot._prefs.leds_quiet == 1 && !mesh::leds::enabled());
assert(board.applies == before_failed_apply && status_applies == before_failed_status);
reboot.save_ok = true;
assert(run(reboot, "set leds on") == "OK" && reboot.saves == 2);
assert(reboot.saved.leds_quiet == 0 && reboot.saved.unrelated == 0x12345678);
assert(mesh::leds::enabled() && board.level && status_level);
MyMesh restored_on;
restored_on._prefs = reboot.saved;
mesh::leds::setEnabled(false);
restored_on.loadCompactLedSetting();
assert(mesh::leds::enabled() && board.level && status_level);
// Historic reserved zero means on; only exactly one requests quiet LEDs.
for (unsigned value = 0; value < 256; ++value) {
MyMesh fresh;
fresh._prefs.leds_quiet = value;
reset();
mesh::leds::setEnabled(false);
fresh.loadCompactLedSetting();
const bool on = value != 1;
assert(mesh::leds::enabled() == on && fresh._prefs.leds_quiet == !on);
assert(board.applies == 1 && board.level == on);
assert(status_applies == 1 && status_level == on);
assert(fresh.saves == 0);
}
for (const char* command : {"get leds", "set leds invalid", "set leds off", "set leds on"}) {
for (size_t capacity = 1; capacity <= 160; ++capacity) {
char reply[164]; memset(reply, '#', sizeof(reply));
memcpy(reply, "ab|", 3);
assert(mesh.handleCompactLedCommand(command, reply + 3, capacity));
assert(memcmp(reply, "ab|", 3) == 0 && reply[3 + capacity] == '#');
assert(memchr(reply + 3, 0, capacity));
}
}
char reply[16];
assert(!mesh.handleCompactLedCommand(nullptr, reply, sizeof(reply)));
assert(!mesh.handleCompactLedCommand("get leds", nullptr, sizeof(reply)));
assert(!mesh.handleCompactLedCommand("get leds", reply, 0));
}
"""
class CompanionCompactLedControlTests(unittest.TestCase):
def test_preference_byte_storage_and_policy(self):
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
if not compiler:
self.skipTest("C++ compiler unavailable")
source = (ROOT / "examples/companion_radio/Notifications.cpp").read_text()
source = STUBS + method(source, "handleCompactLedCommand") + startup_loader() + SCENARIOS
with tempfile.TemporaryDirectory() as directory:
directory = Path(directory)
cpp = directory / "compact_led_cli.cpp"
exe = directory / ("compact_led_cli.exe" if os.name == "nt" else "compact_led_cli")
cpp.write_text(source, encoding="ascii")
flags = [] if os.name == "nt" else [
"-fsanitize=address,undefined", "-fno-sanitize-recover=all",
"-fno-omit-frame-pointer", "-fno-pie", "-no-pie"]
result = subprocess.run(
[compiler, "-std=c++11", "-Wall", "-Wextra", *flags,
"-DWIO_E5_MINI_HEADLESS_COMPANION_UI=1", "-I", str(ROOT / "src"),
str(cpp), "-o", str(exe)], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
result = subprocess.run([str(exe)], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
if __name__ == "__main__":
unittest.main()
@@ -51,6 +51,8 @@ bool contactPathPresence(FILESYSTEM* fs,const char* path,bool& present){
int main(int argc,char** argv){
const int scenario=argc>1?atoi(argv[1]):0;
DataStore store;CompanionNodePrefs original;
assert(original.buzzer_quiet==1&&original.vibe_quiet==0);
original.buzzer_quiet=0;original.vibe_quiet=1;
strcpy(original.node_name,"durable node");original.freq=910.525f;
original.ble_pin=876543;original.bluetooth_stealth_mode=2;
original.gps_interval=123;original.sf=7;original.cr=7;original.bw=62.5f;
@@ -103,6 +105,7 @@ int main(int argc,char** argv){
CompanionNodePrefs loaded;double lat=0,lon=0;
assert(store.loadPrefs(loaded,lat,lon));
assert(loaded.freq==changed.freq&&loaded.ble_pin==original.ble_pin);
assert(loaded.buzzer_quiet==0&&loaded.vibe_quiet==1);
assert(loaded.tx_delay_factor==1.25f&&loaded.direct_tx_delay_factor==0.75f);
assert(loaded.interference_threshold==8&&loaded.agc_reset_interval==15&&loaded.tz_offset==-7);
assert(loaded.flood_retry_attempts==4&&loaded.flood_retry_max_path==3);
@@ -153,6 +156,7 @@ int main(int argc,char** argv){
}
DataStore fresh;CompanionNodePrefs defaults;double lat=0,lon=0;
assert(fresh.loadPrefs(defaults,lat,lon));
assert(defaults.buzzer_quiet==1&&defaults.vibe_quiet==0);
assert(defaults.wifi_enabled==0); // Fresh installs never enable WiFi implicitly.
assert(fresh.savePrefs(defaults,lat,lon));
} else if(scenario==2){
+101 -3
View File
@@ -11,7 +11,8 @@ from test_radio_receive_contract import method
ROOT = Path(__file__).resolve().parents[1]
# Published field order and sizes, independent of the implementation's emitter
# and the class's in-memory member order or padding. None represents zero pad.
# and the class's in-memory member order or padding. None represents reserved
# padding; compact Wio Companion claims the single byte at wire offset 79.
WIRE_FIELDS = (
('airtime_factor', 4), ('node_name', 32), (None, 4),
('@lat', 8), ('@lon', 8), ('freq', 4), ('sf', 1), ('cr', 1),
@@ -147,17 +148,32 @@ int main() {
#endif
DataStore reference;
assert(reference.savePrefsLegacy(prefs, 47.125, -122.25));
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)
// The compact target claims exactly the former reserved byte. Its frozen
// predecessor still emits zero there; every other byte stays identical.
auto historical = store.fs.files.at("/new_prefs");
assert(historical[79] == prefs.leds_quiet);
historical[79] = 0;
assert(reference.fs.files.at("/new_prefs") == historical);
#else
assert(reference.fs.files.at("/new_prefs") == store.fs.files.at("/new_prefs"));
#endif
++comparisons;
}
CompanionNodePrefs original;
assert(original.buzzer_quiet == 1 && original.vibe_quiet == 0);
assert(original.lost_reply == 0);
assert(original.tx_media == mesh::RADIO_TX_AUTO && original.tx_ssh == mesh::RADIO_TX_AUTO);
strcpy(original.node_name, "durable image");
original.freq = 915.25f; original.ble_pin = 123456;
original.bluetooth_enabled = 1; original.usb_debug_enabled = 1;
original.gps_sync_interval_hours = 336;
original.buzzer_quiet = 0; original.vibe_quiet = 1;
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)
assert(original.leds_quiet == 0);
original.leds_quiet = 1;
#endif
original.lost_reply = 2;
original.tx_media = mesh::RADIO_TX_SECONDARY;
original.tx_ssh = mesh::RADIO_TX_OFF;
@@ -199,6 +215,11 @@ int main() {
double lat = 1, lon = 2;
assert(reboot.loadPrefsInt("/new_prefs", live, lat, lon));
assert(live.lost_reply == 0 && live.ble_pin == original.ble_pin);
assert(live.buzzer_quiet == (length >= 85 ? 0 : 1));
assert(live.vibe_quiet == (length >= 143 ? 1 : 0));
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)
assert(live.leds_quiet == 1);
#endif
assert(live.tx_media==mesh::RADIO_TX_AUTO&&live.tx_ssh==mesh::RADIO_TX_AUTO);
assert(live.freq == original.freq && !strcmp(live.node_name, original.node_name));
assert(lat == 47.125 && lon == -122.25);
@@ -232,6 +253,8 @@ int main() {
assert(reboot.loadPrefsInt("/new_prefs", live, lat, lon) == expected);
if (expected) {
assert(live.freq == original.freq);
assert(live.buzzer_quiet == (length >= 85 ? 0 : 1));
assert(live.vibe_quiet == (length >= 143 ? 1 : 0));
assert(live.lost_reply == (length >= historical_size ? 2 : 0));
assert(live.tx_media == (length == before.size() ? mesh::RADIO_TX_SECONDARY : mesh::RADIO_TX_AUTO));
assert(live.tx_ssh == (length == before.size() ? mesh::RADIO_TX_OFF : mesh::RADIO_TX_AUTO));
@@ -291,8 +314,75 @@ int main() {
changed.freq = 910.5f; changed.usb_debug_enabled = 0;
changed.gps_sync_interval_hours = 24;
changed.lost_reply = 1;
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)
changed.leds_quiet = 0;
#endif
const auto after = golden(changed, 42.25, -121.125);
// Saved choices remain independent of the new fresh-install defaults.
for (unsigned sound_quiet = 0; sound_quiet < 2; ++sound_quiet) {
for (unsigned vibration_quiet = 0; vibration_quiet < 2; ++vibration_quiet) {
CompanionNodePrefs saved;
assert(saved.copyPersistedValuesFrom(original));
saved.buzzer_quiet = sound_quiet;
saved.vibe_quiet = vibration_quiet;
DataStore writer;
assert(writer.savePrefs(saved, 47.125, -122.25));
const auto disk = writer.fs.files.at("/new_prefs");
assert(disk[84] == sound_quiet && disk[142] == vibration_quiet);
CompanionNodePrefs reboot;
reboot.buzzer_quiet = !sound_quiet;
reboot.vibe_quiet = !vibration_quiet;
double lat = 0, lon = 0;
assert(writer.loadPrefsInt("/new_prefs", reboot, lat, lon));
assert(reboot.buzzer_quiet == sound_quiet);
assert(reboot.vibe_quiet == vibration_quiet);
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)
assert(disk[79] == 1 && reboot.leds_quiet == 1);
#endif
assert(writer.fs.files.at("/new_prefs") == disk);
++comparisons;
}
}
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)
// The reserved byte is part of the atomic image, at its original offset.
// Old firmware emitted zero; raw invalid encodings are clamped by startup.
for (unsigned quiet : {0u, 1u, 2u, 255u}) {
CompanionNodePrefs saved;
assert(saved.copyPersistedValuesFrom(original));
saved.leds_quiet = quiet;
DataStore writer;
assert(writer.savePrefs(saved, 47.125, -122.25));
const auto disk = writer.fs.files.at("/new_prefs");
assert(disk.size() == before.size() && disk[79] == quiet);
auto expected = before; expected[79] = quiet;
assert(disk == expected);
CompanionNodePrefs reboot;
reboot.leds_quiet = !quiet;
double lat = 0, lon = 0;
assert(writer.loadPrefsInt("/new_prefs", reboot, lat, lon));
assert(reboot.leds_quiet == quiet);
assert(reboot.buzzer_quiet == 0 && reboot.vibe_quiet == 1);
assert(writer.fs.files.at("/new_prefs") == disk);
++comparisons;
}
for (size_t length : {size_t(84), historical_size}) {
auto historical = before;
historical[79] = 0;
historical.resize(length);
DataStore old;
old.fs.files["/new_prefs"] = historical;
CompanionNodePrefs live;
live.leds_quiet = 1;
double lat = 0, lon = 0;
assert(old.loadPrefsInt("/new_prefs", live, lat, lon));
assert(live.leds_quiet == 0); // Old reserved zero restores default-on LEDs.
assert(live.ble_pin == original.ble_pin && live.freq == original.freq);
assert(old.fs.files.at("/new_prefs") == historical);
++comparisons;
}
#endif
// Every possible short write and readback, including every tail byte,
// must preserve the old durable image. STM32 issues only one image write;
// the other supported platforms preserve their scalar-write path.
@@ -424,7 +514,14 @@ int main() {
def field_code(rows, seed=False):
result = []
for name, size in rows:
if name == 'tz_offset':
if name is None and size == 1:
result.append('#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)')
result.append('fill(&prefs.leds_quiet, sizeof(prefs.leds_quiet));' if seed
else 'append(&prefs.leds_quiet, 1);')
if not seed:
result.append('#else\nbytes.push_back(0);')
result.append('#endif')
elif name == 'tz_offset':
if seed:
result.append('prefs.tz_offset = float(int(random % 27) - 12);')
else:
@@ -530,7 +627,8 @@ inline char* utoa(unsigned int value,char* output,int base){
(work / 'helpers/IdentityStore.h').write_text(fs)
cpp = work / 'test.cpp'
cpp.write_text(code + tests)
for platform in ('STM32_PLATFORM', 'NRF52_PLATFORM', 'NRF52_PLATFORM,RAK_4631', 'ESP32_PLATFORM',
for platform in ('STM32_PLATFORM', 'STM32_PLATFORM,WIO_E5_MINI_HEADLESS_COMPANION_UI',
'NRF52_PLATFORM', 'NRF52_PLATFORM,RAK_4631', 'ESP32_PLATFORM',
'RP2040_PLATFORM', 'ESP32_PLATFORM,TBEAM_1W',
'RP2040_PLATFORM,ENABLE_WIFI_INTERFACE'):
with self.subTest(platform=platform):
+1
View File
@@ -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@
+3 -1
View File
@@ -152,6 +152,8 @@ int main() {
auto count=events.size();
vibration.trigger();assert(events.size()==count); // cooldown suppresses wake
vibration.quiet(true);
assert(standby() && !vibration.isVibrating());
count=events.size();
vibration.trigger(true);assert(events.size()==count);
vibration.stop();assert(standby() && !vibration.isVibrating());
assert(hardware.registers[DRV2605_REG_GO]==0 && hardware.registers[2]==0);
@@ -216,7 +218,7 @@ class Drv2605PowerTests(unittest.TestCase):
source = source.replace("(drv.readRegister8(DRV2605_REG_GO) & 1) == 0",
"(drv.readRegister8(DRV2605_REG_GO) & 1) != 0")
elif mutation == "no-pulse-off":
source = source.replace("if (!on) {", "if (false && !on) {")
source = source.replace("if (!on || _quiet) {", "if (false && !on) {")
elif mutation == "poll-idle":
source = source.replace("if (!_ready || !_effect_active) return;",
"if (!_ready) return;")
+2 -2
View File
@@ -313,7 +313,7 @@ BUZZER_SEQUENCE = r'''
#include <helpers/ui/buzzer.cpp>
uint32_t now_ms=0;
int main(){
genericBuzzer buzzer;buzzer.begin();
genericBuzzer buzzer;buzzer.begin();buzzer.quiet(false);
char status[180];
auto check=[&](const char* expected){buzzer.voiceStatus(status,sizeof(status));assert(strstr(status,expected));};
auto finish_tone=[&]{
@@ -363,7 +363,7 @@ BUZZER_TONES = r'''
#include <helpers/ui/buzzer.cpp>
uint32_t now_ms=0;
int main(){
genericBuzzer buzzer;buzzer.begin();
genericBuzzer buzzer;buzzer.begin();buzzer.quiet(false);
assert(buzzer.speakGps(true));assert(buzzer.isPlaying());
assert(rtttl::melody=="Startup:d=4,o=5,b=160:16c6,16e6,8g6");
rtttl::finish();buzzer.loop();assert(!buzzer.isPlaying());