diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index d2162028a..b415adc25 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -10320,7 +10320,8 @@ void MyMesh::handleTerminalCommand(char* command, bool reader_prompt, terminalOutput().printf(" -> Companion %s (protocol %u, build %s)\r\n", FIRMWARE_VERSION, (unsigned)FIRMWARE_VER_CODE, FIRMWARE_BUILD_DATE); } else if (strcmp(command, "help") == 0) { - terminalOutput().print( + auto& output = terminalOutput(); + output.print( "Commands:\r\n" " stats-core / stats-radio / stats-radio-diag / stats-packets\r\n" " get prv.key (if export enabled)\r\n" @@ -10330,48 +10331,59 @@ void MyMesh::handleTerminalCommand(char* command, bool reader_prompt, #else " get password (no admin password)\r\n" #endif + ); + output.print( " erase (settings and identity)\r\n" " board\r\n" " version\r\n" " get storage.layout\r\n"); #if MESH_ENABLE_TX_BEACONS - terminalOutput().print(" get beacon [slot] / set beacon ...\r\n" + output.print( + " get beacon [slot] / set beacon ...\r\n" " delete beacon / set beacon.interval <12|24|36|48> (default 24h)\r\n"); #endif #if MESH_ENABLE_MESHTEXT_CHAT - terminalOutput().print(" get mt.status / get mt.region / get mt.slot / get mt.hops\r\n" + output.print( + " get mt.status / get mt.region / get mt.slot / get mt.hops\r\n" " get mt.node / get mt.shortname\r\n" " get mt.rebroadcast / set mt.rebroadcast (saved; default off)\r\n" - " Relay decoded channel text only; adds 5 packet airtimes before TX\r\n" + " Relay decoded channel text only; adds 5 packet airtimes before TX\r\n"); + output.print( " set mt.region \r\n" " set mt.slot <0|1-based frequency slot>\r\n" " set mt.hops <0..7> (saved; default 7)\r\n" - " set mt.node (saved; use a unique ID)\r\n" + " set mt.node (saved; use a unique ID)\r\n"); + output.print( " set mt.shortname <1..4 UTF-8 characters|default> (saved; NodeInfo uses 4 bytes)\r\n" " send mt.nodeinfo (GPS-free; also automatic after first chat)\r\n" - " set mt.preset (saved manual radio2)\r\n" + " set mt.preset (saved manual radio2)\r\n"); + output.print( " set mt.channel <32/64 hex digits|AQ==>\r\n" " get mt.channel / delete mt.channel \r\n" " Join #mt-longfast or #mt-shortturbo in the app for automatic radio2\r\n"); #endif #if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH) - terminalOutput().print(" get flash.health\r\n"); + output.print(" get flash.health\r\n"); #endif #if COMPANION_FEATURE_READER - terminalOutput().print(" get reader [:[-]] [page] (World English Bible, offline)\r\n"); + output.print(" get reader [:[-]] [page] (World English Bible, offline)\r\n"); #endif - terminalOutput().print(" get/set tx.media auto|radio|radio2|both|off (binary media)\r\n" + output.print( + " get/set tx.media auto|radio|radio2|both|off (binary media)\r\n" " get/set tx.ssh auto|radio|radio2|both|off (tagged tunnel data)\r\n"); #if MESH_ENABLE_ETHERNET_TUNNEL - terminalOutput().print(" get ethernet / get ethernet.radio\r\n" + output.print( + " get ethernet / get ethernet.radio\r\n" " set ethernet.radio ,,,[,]\r\n" " set ethernet on|off (saved; trusted-LAN KISS TCP 5003, radio2 only)\r\n"); #endif #if defined(OTA_RAK15002_SD_STORE) && defined(ETHERNET_USE_RAK13800) - terminalOutput().print(" get eth / set eth on|off (physical RAK13800; default off, this boot)\r\n" + output.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" + output.print( + " get pwrmgt.bootreason\r\n" #if COMPANION_FEATURE_MEMORY_DIAGNOSTICS " memory\r\n" #endif @@ -10382,32 +10394,40 @@ void MyMesh::handleTerminalCommand(char* command, bool reader_prompt, #if defined(PIN_VIBRATION) || defined(HAS_DRV2605) " get/set vibration on|off\r\n" #endif - #if COMPANION_FEATURE_NOTIFICATIONS + ); +#if COMPANION_FEATURE_NOTIFICATIONS + output.print( " get notify / get notify.rules / get notify.gpio.pins\r\n" " set notify.{sound|vibration|led|screen|gpio} on|off\r\n" - " set notify. \r\n" + " set notify. \r\n"); + output.print( " set notify.remote on|off (!notify, max 15s, no GPIO)\r\n" - " notify.test / notify.stop / notify.delete \r\n" - #endif + " notify.test / notify.stop / notify.delete \r\n"); +#endif + output.print( " get/set display.inbox \r\n" " get/set display.touch (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 wifi|espnow on|off [force] (force only with off)\r\n"); + output.print( " set 2.4ghz on [all|force]|off [force] (this boot)\r\n" - " get/set bluetooth.name \r\n" + " get/set bluetooth.name \r\n"); #if defined(BLE_PIN_CODE) + output.print( " get bluetooth (alias: get ble)\r\n" " set bluetooth [force] (saved; force only with off)\r\n" " get bluetooth.mac\r\n" " set bluetooth.mac \r\n" - " get/set bluetooth.stealth on|off\r\n" + " get/set bluetooth.stealth on|off\r\n"); #endif + output.print( #if MESH_ENABLE_LOST_REPLY " get/set lost.reply \r\n" #endif - " 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 " get/set usb.logging [reboot]\r\n" " get/set usb.debug \r\n" @@ -10419,54 +10439,56 @@ void MyMesh::handleTerminalCommand(char* command, bool reader_prompt, ); #if defined(ESP32) && defined(WIFI_SSID) #if defined(COMPANION_RADIO_FULL) - terminalOutput().print(" start ota [ap] (WiFi firmware update on port 8080)\r\n"); - terminalOutput().print(" stop ota\r\n"); + output.print(" start ota [ap] (WiFi firmware update on port 8080)\r\n"); + output.print(" stop ota\r\n"); #endif #if defined(COMPANION_EXCLUSIVE_WIFI_BLE) - terminalOutput().print(" get companion.transport\r\n"); - terminalOutput().print(" set companion.transport \r\n"); + output.print(" get companion.transport\r\n"); + output.print(" set companion.transport \r\n"); #endif - terminalOutput().print(" get wifi.powersave\r\n"); - terminalOutput().print(" set wifi.powersave \r\n"); - terminalOutput().print(" get wifi.protocol\r\n"); - terminalOutput().print(" get wifi.status (connection, saved/boot/live settings)\r\n"); - terminalOutput().print(" set wifi.protocol (saved; reboot required)\r\n"); + output.print(" get wifi.powersave\r\n"); + output.print(" set wifi.powersave \r\n"); + output.print(" get wifi.protocol\r\n"); + output.print(" get wifi.status (connection, saved/boot/live settings)\r\n"); + output.print(" set wifi.protocol (saved; reboot required)\r\n"); #if !defined(COMPANION_EXCLUSIVE_WIFI_BLE) - terminalOutput().print(" get wifi.mode\r\n"); - terminalOutput().print(" set wifi.mode (saved; reboot required)\r\n"); + output.print(" get wifi.mode\r\n"); + output.print(" set wifi.mode (saved; reboot required)\r\n"); #endif #ifdef WITH_WEBCONFIG - terminalOutput().print(" get wifi.{ssid|pwd}\r\n"); - terminalOutput().print(" set wifi.ssid \r\n"); - terminalOutput().print(" set wifi.pwd \r\n"); - terminalOutput().print(" get webui\r\n"); - terminalOutput().print(" set webui \r\n"); - terminalOutput().print(" start webconfig [ap]\r\n"); - terminalOutput().print(" stop webconfig\r\n"); + output.print(" get wifi.{ssid|pwd}\r\n"); + output.print(" set wifi.ssid \r\n"); + output.print(" set wifi.pwd \r\n"); + output.print(" get webui\r\n"); + output.print(" set webui \r\n"); + output.print(" start webconfig [ap]\r\n"); + output.print(" stop webconfig\r\n"); #endif #endif #if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW - terminalOutput().print(" get espnow.channel\r\n"); - terminalOutput().print(" set espnow.channel <1-13>\r\n"); + output.print(" get espnow.channel\r\n"); + output.print(" set espnow.channel <1-13>\r\n"); #endif - // Concise help and grouped Print calls preserve small-board flash space. - terminalOutput().print( + // Keep native USB producer writes within 256 bytes without changing help text. + output.print( " get radio.rxps.config\r\n" " get/set radio.rxps \r\n" " get/set radio.cad \r\n" " set radio.cad timings \r\n" - " get/set radio.rxgain \r\n" + " get/set radio.rxgain \r\n"); + output.print( " get/set radio.fem.rxgain \r\n" " get/set radio.fem.txgain \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" " get/set flood.retry.advert \r\n" - " card\r\n" + " card\r\n"); + output.print( " import \r\n" " clock\r\n" " time \r\n"); - terminalOutput().print( + output.print( " list [n]\r\n" " show [adverts|channels|emergency] [on|off]\r\n" " to [name|prefix]\r\n" @@ -10474,7 +10496,7 @@ void MyMesh::handleTerminalCommand(char* command, bool reader_prompt, " send \r\n" " login \r\n" " cmd \r\n"); - terminalOutput().print( + output.print( " trace [name|prefix]\r\n" " trace path <1|2|4> \r\n" " advert\r\n" @@ -10483,26 +10505,28 @@ void MyMesh::handleTerminalCommand(char* command, bool reader_prompt, " channels\r\n" " channel \r\n"); #if defined(NRF52_PLATFORM) && defined(EXTRAFS) && !defined(QSPIFLASH) - terminalOutput().print(" repair extrafs (erases/rebuilds internal ExtraFS)\r\n"); - terminalOutput().print(" scan extrafs erase (USB only; reboot for destructive page test)\r\n"); + output.print(" repair extrafs (erases/rebuilds internal ExtraFS)\r\n"); + output.print(" scan extrafs erase (USB only; reboot for destructive page test)\r\n"); #endif #if COMPANION_FEATURE_TEMP_RADIO || COMPANION_FEATURE_OTA_CLI #if COMPANION_FEATURE_TEMP_RADIO - terminalOutput().print(" tempradio [freq,bw,sf,cr,minutes]\r\n"); - terminalOutput().print(" normalradio\r\n"); + output.print(" tempradio [freq,bw,sf,cr,minutes]\r\n"); + output.print(" normalradio\r\n"); #endif #if COMPANION_FEATURE_OTA_CLI - terminalOutput().print(" ota {status|ls|announce|folder|config|...}\r\n"); + output.print(" ota {status|ls|announce|folder|config|...}\r\n"); #endif #endif - terminalOutput().print(" reboot\r\n" " ver\r\n"); + output.print( + " reboot\r\n" + " ver\r\n"); #if defined(NRF52_PLATFORM) - terminalOutput().println(" uf2reset"); + output.println(" uf2reset"); #endif if (_terminal_mode) { - terminalOutput().print(" +++MESHCORE-TERM-STOP\r\n"); + output.print(" +++MESHCORE-TERM-STOP\r\n"); } else { - terminalOutput().print(" disconnect (network terminal)\r\n"); + output.print(" disconnect (network terminal)\r\n"); } } else if (handleCommand(command, 0, local_reply)) { // Fill the same safe shared-command surface for getters (`get name`, diff --git a/test/fixtures/companion_help_output.sha256.json b/test/fixtures/companion_help_output.sha256.json new file mode 100644 index 000000000..eab3260df --- /dev/null +++ b/test/fixtures/companion_help_output.sha256.json @@ -0,0 +1,492 @@ +{ + "profiles": { + "all-options-native0-lost0": { + "flags": [ + "MESH_USB_LOGGING_AVAILABLE=1", + "COMPANION_FEATURE_READER=1", + "COMPANION_FEATURE_MEMORY_DIAGNOSTICS=1", + "COMPANION_FEATURE_NOTIFICATIONS=1", + "COMPANION_RADIO_FULL=1", + "COMPANION_EXCLUSIVE_WIFI_BLE=1", + "WITH_WEBCONFIG=1", + "MESH_PRIMARY_ESPNOW=1", + "COMPANION_FEATURE_TEMP_RADIO=1", + "COMPANION_FEATURE_OTA_CLI=1", + "BLE_PIN_CODE=1", + "ESP32=1", + "NRF52_PLATFORM=1", + "EXTRAFS=1", + "WIFI_SSID=\"test\"", + "MESH_ENABLE_TX_BEACONS=1", + "MESH_ENABLE_MESHTEXT_CHAT=1", + "MESH_ENABLE_ETHERNET_TUNNEL=1", + "OTA_RAK15002_SD_STORE=1", + "ETHERNET_USE_RAK13800=1", + "PIN_BUZZER=1", + "PIN_VIBRATION=1", + "HAS_DRV2605=1", + "ESP32_PLATFORM=1", + "MESH_USB_CONSOLE_COOPERATIVE=0", + "MESH_ENABLE_LOST_REPLY=0" + ], + "output_bytes": [ + 4644, + 4636 + ], + "output_sha256": [ + "c830a4086f88e06ec7fe21bb6f1287e78fe4e72cefd7c3999740913f9ce3831d", + "b71c0ba723d2ff693c1ed8ce23998d977607a213dba4af688fe4db16a59e38f6" + ] + }, + "all-options-native0-lost1": { + "flags": [ + "MESH_USB_LOGGING_AVAILABLE=1", + "COMPANION_FEATURE_READER=1", + "COMPANION_FEATURE_MEMORY_DIAGNOSTICS=1", + "COMPANION_FEATURE_NOTIFICATIONS=1", + "COMPANION_RADIO_FULL=1", + "COMPANION_EXCLUSIVE_WIFI_BLE=1", + "WITH_WEBCONFIG=1", + "MESH_PRIMARY_ESPNOW=1", + "COMPANION_FEATURE_TEMP_RADIO=1", + "COMPANION_FEATURE_OTA_CLI=1", + "BLE_PIN_CODE=1", + "ESP32=1", + "NRF52_PLATFORM=1", + "EXTRAFS=1", + "WIFI_SSID=\"test\"", + "MESH_ENABLE_TX_BEACONS=1", + "MESH_ENABLE_MESHTEXT_CHAT=1", + "MESH_ENABLE_ETHERNET_TUNNEL=1", + "OTA_RAK15002_SD_STORE=1", + "ETHERNET_USE_RAK13800=1", + "PIN_BUZZER=1", + "PIN_VIBRATION=1", + "HAS_DRV2605=1", + "ESP32_PLATFORM=1", + "MESH_USB_CONSOLE_COOPERATIVE=0", + "MESH_ENABLE_LOST_REPLY=1" + ], + "output_bytes": [ + 4679, + 4671 + ], + "output_sha256": [ + "7375ec52046c4479f32f33a1845689344eb6d9c3b29d1d0a1404d57c148eb703", + "03401a37d0b371186885ddda541f8c011c76125f98c46b27be4f3d559fe2cd3e" + ] + }, + "all-options-native1-lost0": { + "flags": [ + "MESH_USB_LOGGING_AVAILABLE=1", + "COMPANION_FEATURE_READER=1", + "COMPANION_FEATURE_MEMORY_DIAGNOSTICS=1", + "COMPANION_FEATURE_NOTIFICATIONS=1", + "COMPANION_RADIO_FULL=1", + "COMPANION_EXCLUSIVE_WIFI_BLE=1", + "WITH_WEBCONFIG=1", + "MESH_PRIMARY_ESPNOW=1", + "COMPANION_FEATURE_TEMP_RADIO=1", + "COMPANION_FEATURE_OTA_CLI=1", + "BLE_PIN_CODE=1", + "ESP32=1", + "NRF52_PLATFORM=1", + "EXTRAFS=1", + "WIFI_SSID=\"test\"", + "MESH_ENABLE_TX_BEACONS=1", + "MESH_ENABLE_MESHTEXT_CHAT=1", + "MESH_ENABLE_ETHERNET_TUNNEL=1", + "OTA_RAK15002_SD_STORE=1", + "ETHERNET_USE_RAK13800=1", + "PIN_BUZZER=1", + "PIN_VIBRATION=1", + "HAS_DRV2605=1", + "ESP32_PLATFORM=1", + "MESH_USB_CONSOLE_COOPERATIVE=1", + "MESH_ENABLE_LOST_REPLY=0" + ], + "output_bytes": [ + 4729, + 4721 + ], + "output_sha256": [ + "7c2f2ca10b445bd5a4ea9f747198dc41bf4275997b38a5af1a108edb1fada34c", + "8841b225cf40dcad34d7de15b945384e933c668dd0d63f2abbf9449cf542af95" + ] + }, + "all-options-native1-lost1": { + "flags": [ + "MESH_USB_LOGGING_AVAILABLE=1", + "COMPANION_FEATURE_READER=1", + "COMPANION_FEATURE_MEMORY_DIAGNOSTICS=1", + "COMPANION_FEATURE_NOTIFICATIONS=1", + "COMPANION_RADIO_FULL=1", + "COMPANION_EXCLUSIVE_WIFI_BLE=1", + "WITH_WEBCONFIG=1", + "MESH_PRIMARY_ESPNOW=1", + "COMPANION_FEATURE_TEMP_RADIO=1", + "COMPANION_FEATURE_OTA_CLI=1", + "BLE_PIN_CODE=1", + "ESP32=1", + "NRF52_PLATFORM=1", + "EXTRAFS=1", + "WIFI_SSID=\"test\"", + "MESH_ENABLE_TX_BEACONS=1", + "MESH_ENABLE_MESHTEXT_CHAT=1", + "MESH_ENABLE_ETHERNET_TUNNEL=1", + "OTA_RAK15002_SD_STORE=1", + "ETHERNET_USE_RAK13800=1", + "PIN_BUZZER=1", + "PIN_VIBRATION=1", + "HAS_DRV2605=1", + "ESP32_PLATFORM=1", + "MESH_USB_CONSOLE_COOPERATIVE=1", + "MESH_ENABLE_LOST_REPLY=1" + ], + "output_bytes": [ + 4764, + 4756 + ], + "output_sha256": [ + "c141a0237099f1f63ae8768001614673d6d54914ae78d41fc4a483f031326f25", + "d562a7dcac544c502908205b14eaa20e921c76830ba5921018c4aa19798efea8" + ] + }, + "full-native0-lost0": { + "flags": [ + "MESH_USB_LOGGING_AVAILABLE=1", + "COMPANION_FEATURE_READER=1", + "COMPANION_FEATURE_MEMORY_DIAGNOSTICS=1", + "COMPANION_FEATURE_NOTIFICATIONS=1", + "COMPANION_RADIO_FULL=1", + "COMPANION_EXCLUSIVE_WIFI_BLE=1", + "WITH_WEBCONFIG=1", + "MESH_PRIMARY_ESPNOW=1", + "COMPANION_FEATURE_TEMP_RADIO=1", + "COMPANION_FEATURE_OTA_CLI=1", + "BLE_PIN_CODE=1", + "ESP32=1", + "NRF52_PLATFORM=1", + "EXTRAFS=1", + "WIFI_SSID=\"test\"", + "MESH_USB_CONSOLE_COOPERATIVE=0", + "MESH_ENABLE_LOST_REPLY=0" + ], + "output_bytes": [ + 3214, + 3206 + ], + "output_sha256": [ + "ef1d9548ee100678eac89f0d60a42dd356bf99127eb819beccc4a15895145b94", + "8a3fe836f56831c9c8de86254fd8186f0ece25d0d63acf3b1c916990341a0c0a" + ] + }, + "full-native0-lost1": { + "flags": [ + "MESH_USB_LOGGING_AVAILABLE=1", + "COMPANION_FEATURE_READER=1", + "COMPANION_FEATURE_MEMORY_DIAGNOSTICS=1", + "COMPANION_FEATURE_NOTIFICATIONS=1", + "COMPANION_RADIO_FULL=1", + "COMPANION_EXCLUSIVE_WIFI_BLE=1", + "WITH_WEBCONFIG=1", + "MESH_PRIMARY_ESPNOW=1", + "COMPANION_FEATURE_TEMP_RADIO=1", + "COMPANION_FEATURE_OTA_CLI=1", + "BLE_PIN_CODE=1", + "ESP32=1", + "NRF52_PLATFORM=1", + "EXTRAFS=1", + "WIFI_SSID=\"test\"", + "MESH_USB_CONSOLE_COOPERATIVE=0", + "MESH_ENABLE_LOST_REPLY=1" + ], + "output_bytes": [ + 3249, + 3241 + ], + "output_sha256": [ + "ef5d1cd1f549568acf0b1e769bdcd68bd96855301ec8e7e053df9e9c87a03b6d", + "17a4afab06e7e61cccde52da08ea6d939ead8c34f4ca321cabdaed3efdf9da62" + ] + }, + "full-native1-lost0": { + "flags": [ + "MESH_USB_LOGGING_AVAILABLE=1", + "COMPANION_FEATURE_READER=1", + "COMPANION_FEATURE_MEMORY_DIAGNOSTICS=1", + "COMPANION_FEATURE_NOTIFICATIONS=1", + "COMPANION_RADIO_FULL=1", + "COMPANION_EXCLUSIVE_WIFI_BLE=1", + "WITH_WEBCONFIG=1", + "MESH_PRIMARY_ESPNOW=1", + "COMPANION_FEATURE_TEMP_RADIO=1", + "COMPANION_FEATURE_OTA_CLI=1", + "BLE_PIN_CODE=1", + "ESP32=1", + "NRF52_PLATFORM=1", + "EXTRAFS=1", + "WIFI_SSID=\"test\"", + "MESH_USB_CONSOLE_COOPERATIVE=1", + "MESH_ENABLE_LOST_REPLY=0" + ], + "output_bytes": [ + 3299, + 3291 + ], + "output_sha256": [ + "3402723097417e800d48275bb7e4b7a8911ac57a8370a544900137938fcbde66", + "2313fef0b2d9f3f74ab816dd6d8d27b44acb126469b3e08446544c3f0c291e77" + ] + }, + "full-native1-lost1": { + "flags": [ + "MESH_USB_LOGGING_AVAILABLE=1", + "COMPANION_FEATURE_READER=1", + "COMPANION_FEATURE_MEMORY_DIAGNOSTICS=1", + "COMPANION_FEATURE_NOTIFICATIONS=1", + "COMPANION_RADIO_FULL=1", + "COMPANION_EXCLUSIVE_WIFI_BLE=1", + "WITH_WEBCONFIG=1", + "MESH_PRIMARY_ESPNOW=1", + "COMPANION_FEATURE_TEMP_RADIO=1", + "COMPANION_FEATURE_OTA_CLI=1", + "BLE_PIN_CODE=1", + "ESP32=1", + "NRF52_PLATFORM=1", + "EXTRAFS=1", + "WIFI_SSID=\"test\"", + "MESH_USB_CONSOLE_COOPERATIVE=1", + "MESH_ENABLE_LOST_REPLY=1" + ], + "output_bytes": [ + 3334, + 3326 + ], + "output_sha256": [ + "71c96f2d042f38c896fe14dc6f867898bb413498dfe669d874130e1595a8e560", + "feb8bb64cca5032c07d967f7428dafb4787fc7eb342fc398ea78bb3746b4b7b8" + ] + }, + "minimal-native0-lost0": { + "flags": [ + "MESH_USB_LOGGING_AVAILABLE=0", + "MESH_USB_CONSOLE_COOPERATIVE=0", + "MESH_ENABLE_LOST_REPLY=0" + ], + "output_bytes": [ + 1696, + 1688 + ], + "output_sha256": [ + "82ed92c5c52cbd699c5b57f11006367b338cab5347f06aeccb0edf80f8b16ec2", + "c0047e0c3eae1e9d828dabe6157ed4296af1d0c93c249aa1dda0ea5920f5ef25" + ] + }, + "minimal-native0-lost1": { + "flags": [ + "MESH_USB_LOGGING_AVAILABLE=0", + "MESH_USB_CONSOLE_COOPERATIVE=0", + "MESH_ENABLE_LOST_REPLY=1" + ], + "output_bytes": [ + 1731, + 1723 + ], + "output_sha256": [ + "de5cbd88c77ed4f30817bcd89904725c97fb49142cf048e7b90e897d646e7b6b", + "20daa08b614642afffe00bc1c89621a020e688d79a32113da16037e3a3f4c460" + ] + }, + "minimal-native1-lost0": { + "flags": [ + "MESH_USB_LOGGING_AVAILABLE=0", + "MESH_USB_CONSOLE_COOPERATIVE=1", + "MESH_ENABLE_LOST_REPLY=0" + ], + "output_bytes": [ + 1696, + 1688 + ], + "output_sha256": [ + "82ed92c5c52cbd699c5b57f11006367b338cab5347f06aeccb0edf80f8b16ec2", + "c0047e0c3eae1e9d828dabe6157ed4296af1d0c93c249aa1dda0ea5920f5ef25" + ] + }, + "minimal-native1-lost1": { + "flags": [ + "MESH_USB_LOGGING_AVAILABLE=0", + "MESH_USB_CONSOLE_COOPERATIVE=1", + "MESH_ENABLE_LOST_REPLY=1" + ], + "output_bytes": [ + 1731, + 1723 + ], + "output_sha256": [ + "de5cbd88c77ed4f30817bcd89904725c97fb49142cf048e7b90e897d646e7b6b", + "20daa08b614642afffe00bc1c89621a020e688d79a32113da16037e3a3f4c460" + ] + }, + "qspi-native0-lost0": { + "flags": [ + "MESH_USB_LOGGING_AVAILABLE=1", + "COMPANION_FEATURE_READER=1", + "COMPANION_FEATURE_MEMORY_DIAGNOSTICS=1", + "COMPANION_FEATURE_NOTIFICATIONS=1", + "COMPANION_RADIO_FULL=1", + "COMPANION_EXCLUSIVE_WIFI_BLE=1", + "WITH_WEBCONFIG=1", + "MESH_PRIMARY_ESPNOW=1", + "COMPANION_FEATURE_TEMP_RADIO=1", + "COMPANION_FEATURE_OTA_CLI=1", + "BLE_PIN_CODE=1", + "ESP32=1", + "NRF52_PLATFORM=1", + "EXTRAFS=1", + "WIFI_SSID=\"test\"", + "MESH_ENABLE_TX_BEACONS=1", + "MESH_ENABLE_MESHTEXT_CHAT=1", + "MESH_ENABLE_ETHERNET_TUNNEL=1", + "OTA_RAK15002_SD_STORE=1", + "ETHERNET_USE_RAK13800=1", + "PIN_BUZZER=1", + "PIN_VIBRATION=1", + "HAS_DRV2605=1", + "ESP32_PLATFORM=1", + "QSPIFLASH=1", + "MESH_USB_CONSOLE_COOPERATIVE=0", + "MESH_ENABLE_LOST_REPLY=0" + ], + "output_bytes": [ + 4504, + 4496 + ], + "output_sha256": [ + "32c607f77a4c48829a6bbd91538d0dc717f813fc32251455702913a8ffea0cdb", + "cfb475576309abb89fcad75d7d019049ed2a5489dd325fea5bcaf0ed186c9d82" + ] + }, + "qspi-native0-lost1": { + "flags": [ + "MESH_USB_LOGGING_AVAILABLE=1", + "COMPANION_FEATURE_READER=1", + "COMPANION_FEATURE_MEMORY_DIAGNOSTICS=1", + "COMPANION_FEATURE_NOTIFICATIONS=1", + "COMPANION_RADIO_FULL=1", + "COMPANION_EXCLUSIVE_WIFI_BLE=1", + "WITH_WEBCONFIG=1", + "MESH_PRIMARY_ESPNOW=1", + "COMPANION_FEATURE_TEMP_RADIO=1", + "COMPANION_FEATURE_OTA_CLI=1", + "BLE_PIN_CODE=1", + "ESP32=1", + "NRF52_PLATFORM=1", + "EXTRAFS=1", + "WIFI_SSID=\"test\"", + "MESH_ENABLE_TX_BEACONS=1", + "MESH_ENABLE_MESHTEXT_CHAT=1", + "MESH_ENABLE_ETHERNET_TUNNEL=1", + "OTA_RAK15002_SD_STORE=1", + "ETHERNET_USE_RAK13800=1", + "PIN_BUZZER=1", + "PIN_VIBRATION=1", + "HAS_DRV2605=1", + "ESP32_PLATFORM=1", + "QSPIFLASH=1", + "MESH_USB_CONSOLE_COOPERATIVE=0", + "MESH_ENABLE_LOST_REPLY=1" + ], + "output_bytes": [ + 4539, + 4531 + ], + "output_sha256": [ + "de162715b7175070aa2fce814b9b20dcef2a1a33ca1a14a5d0063f7dbccb489a", + "3ea7895b287cbe415d3febfc4d1f97084828f37ae4fa78e98ab8da0b0c2bf4fe" + ] + }, + "qspi-native1-lost0": { + "flags": [ + "MESH_USB_LOGGING_AVAILABLE=1", + "COMPANION_FEATURE_READER=1", + "COMPANION_FEATURE_MEMORY_DIAGNOSTICS=1", + "COMPANION_FEATURE_NOTIFICATIONS=1", + "COMPANION_RADIO_FULL=1", + "COMPANION_EXCLUSIVE_WIFI_BLE=1", + "WITH_WEBCONFIG=1", + "MESH_PRIMARY_ESPNOW=1", + "COMPANION_FEATURE_TEMP_RADIO=1", + "COMPANION_FEATURE_OTA_CLI=1", + "BLE_PIN_CODE=1", + "ESP32=1", + "NRF52_PLATFORM=1", + "EXTRAFS=1", + "WIFI_SSID=\"test\"", + "MESH_ENABLE_TX_BEACONS=1", + "MESH_ENABLE_MESHTEXT_CHAT=1", + "MESH_ENABLE_ETHERNET_TUNNEL=1", + "OTA_RAK15002_SD_STORE=1", + "ETHERNET_USE_RAK13800=1", + "PIN_BUZZER=1", + "PIN_VIBRATION=1", + "HAS_DRV2605=1", + "ESP32_PLATFORM=1", + "QSPIFLASH=1", + "MESH_USB_CONSOLE_COOPERATIVE=1", + "MESH_ENABLE_LOST_REPLY=0" + ], + "output_bytes": [ + 4589, + 4581 + ], + "output_sha256": [ + "a173923cd67bf96c4c1f6f5d4fa933214ecaa68c8c43d7d881f53f3e49a807c5", + "c367f8675987d86270c0b763e1249201872d0e725a20cee4b93e155d360cf12d" + ] + }, + "qspi-native1-lost1": { + "flags": [ + "MESH_USB_LOGGING_AVAILABLE=1", + "COMPANION_FEATURE_READER=1", + "COMPANION_FEATURE_MEMORY_DIAGNOSTICS=1", + "COMPANION_FEATURE_NOTIFICATIONS=1", + "COMPANION_RADIO_FULL=1", + "COMPANION_EXCLUSIVE_WIFI_BLE=1", + "WITH_WEBCONFIG=1", + "MESH_PRIMARY_ESPNOW=1", + "COMPANION_FEATURE_TEMP_RADIO=1", + "COMPANION_FEATURE_OTA_CLI=1", + "BLE_PIN_CODE=1", + "ESP32=1", + "NRF52_PLATFORM=1", + "EXTRAFS=1", + "WIFI_SSID=\"test\"", + "MESH_ENABLE_TX_BEACONS=1", + "MESH_ENABLE_MESHTEXT_CHAT=1", + "MESH_ENABLE_ETHERNET_TUNNEL=1", + "OTA_RAK15002_SD_STORE=1", + "ETHERNET_USE_RAK13800=1", + "PIN_BUZZER=1", + "PIN_VIBRATION=1", + "HAS_DRV2605=1", + "ESP32_PLATFORM=1", + "QSPIFLASH=1", + "MESH_USB_CONSOLE_COOPERATIVE=1", + "MESH_ENABLE_LOST_REPLY=1" + ], + "output_bytes": [ + 4624, + 4616 + ], + "output_sha256": [ + "9ba51362d80560bafd41f170e4791542dea7f128cca2c07c8e13413a42e74b09", + "d3a7f18a0f799b95b3a44cee602fc3d6e60a314bdd15e5bcf4f29ad510b45cda" + ] + } + }, + "schema": 1, + "scope": "Exact existing help bytes before bounded print-call splitting; network then USB terminal mode", + "source_commit": "c46defaefd642973cbbdb473815d56b5b261cb2e", + "source_file_sha256": "4665bf3d9c60abe3961aa3548d51c0d3a242d49a2ff061a4732fa07d06ae5a6a" +} diff --git a/test/fixtures/mono_led_rollover.cpp b/test/fixtures/mono_led_rollover.cpp index 8335c5110..5a6c19eda 100644 --- a/test/fixtures/mono_led_rollover.cpp +++ b/test/fixtures/mono_led_rollover.cpp @@ -1,6 +1,7 @@ // Full actual handlers/field initializers are injected. RGB is not configured. // Arduino clock and output writes are the only hardware boundaries mocked. #include +#include #include #include #include @@ -86,9 +87,16 @@ int main(int argc, char** argv) { CHECK(argc == 2); active_case = argv[1]; UITask ui; + CHECK(mesh::leds::enabled()); const char* name = argv[1]; #ifdef PIN_STATUS_LED - if (!std::strcmp(name, "zero")) { + if (!std::strcmp(name, "quiet-fresh")) { + mesh::leds::setEnabled(false); + at(ui, 0, 0, false); + at(ui, 0x80000000u, 0, false); + mesh::leds::setEnabled(true); + cadence(ui, 0x80000000u, LED_ON_MILLIS); + } else if (!std::strcmp(name, "zero")) { at(ui, 0, 0, false); cadence(ui, 1, LED_ON_MILLIS); } else if (!std::strcmp(name, "upper")) { diff --git a/test/fixtures/morse_driver.cpp b/test/fixtures/morse_driver.cpp index eb2001894..627405abe 100644 --- a/test/fixtures/morse_driver.cpp +++ b/test/fixtures/morse_driver.cpp @@ -161,17 +161,34 @@ int main() { assert(w.stopMorseTx() == MResult::Stopped && w.chip.carrier && w.chip.starts == 1); assert(w.setCarrierWave(0, 0) == mesh::RadioParamApplyResult::APPLIED); } + { + RadioLibWrapper w; + w.chip.standby_result = -705; + // RX admission failed before Morse owns or changes the modem. The shared + // reconfiguration gate requires repair and defers the attempted operation. + assert(w.startMorseTx(902.1f, 25, "E") == MResult::Busy); + assert(w.chip.standby_calls == 1 && !w.chip.starts && !w.board.before); + assert(!w._cw_active && !w._morse_owned && !w.board.awake); + assert(!w.board.frequency_prepares && !w.isInRecvMode()); + assert(w._profile_refresh_required && w._profile_repair_required); + w.chip.standby_result = 0; + assert(w.startMorseTx(902.1f, 25, "E") == MResult::Started); + assert(w.stopMorseTx() == MResult::Stopped); + fresh_receive(w); + } for (int reason = 0; reason < 5; ++reason) { RadioLibWrapper w; if (reason == 0) w.board.frequency_ok = false; if (reason == 1) w.chip.frequency_result = -705; if (reason == 2) w.chip.power_result = -705; - if (reason == 3) w.chip.standby_result = -705; + // Fail the post-admission standby, not the earlier RX admission gate. + if (reason == 3) w.chip.standby_fail_call = 2; if (reason == 4) w.chip.irq_result = -705; assert(w.startMorseTx(902.1f, 25, "E") == MResult::Failed); assert(!w.chip.starts && !w.board.before); if (w._cw_active) assert(w._cw_stopping && w.board.awake); w.board.frequency_ok = true; + w.chip.standby_fail_call = 0; w.chip.frequency_result = w.chip.power_result = w.chip.standby_result = w.chip.irq_result = 0; if (w._cw_active) { advance(100); w.serviceCarrierWave(); } assert(!w._cw_active && !w.board.awake && w.isInRecvMode()); diff --git a/test/fixtures/morse_main_loop.cpp b/test/fixtures/morse_main_loop.cpp index c1d30c594..d26ff575e 100644 --- a/test/fixtures/morse_main_loop.cpp +++ b/test/fixtures/morse_main_loop.cpp @@ -3,6 +3,7 @@ #include #include static unsigned usb_services=0,command_services=0,transport_services=0,delays=0; +static unsigned usb_bluetooth_priority_services=0; static bool usb_watchdog_armed=false,wireless_pending=false; bool profileIdlePreparationAllowed(); bool profileIdleQueryAllowed(); @@ -120,6 +121,12 @@ void serviceDeferredCompanionBluetooth() { ++transport_services; } void serviceCompanionBluetoothIdentity() { ++transport_services; } void serviceCompanionBluetoothControl() { ++transport_services; } void serviceCompanionBluetoothName() { ++transport_services; } +// Transport handoff has separate policy/state tests. Keep its loop boundary +// live here, including during Morse, before the authoritative sleep snapshot. +void serviceCompanionUsbBluetoothPriority() { + assert(usb_bluetooth_priority_services == board.preparations); + ++usb_bluetooth_priority_services; +} @LOOP@ int main() { assert(!mesh::morseTimingActive(nullptr)); @@ -268,4 +275,9 @@ int main() { #if !ROLE_REPEATER assert(mesh::power::regulator_services==board.calls); #endif +#if ROLE_COMPANION + assert(usb_bluetooth_priority_services==board.preparations); +#else + assert(usb_bluetooth_priority_services==0); +#endif } diff --git a/test/fixtures/rgb_led_rollover.cpp b/test/fixtures/rgb_led_rollover.cpp index 8d066ae90..893b3bf12 100644 --- a/test/fixtures/rgb_led_rollover.cpp +++ b/test/fixtures/rgb_led_rollover.cpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace mesh { namespace notify { @OUTPUT_ENUM@ @@ -151,8 +152,16 @@ int main(int argc, char** argv) { active_case = argv[1]; const char* name = argv[1]; UITask ui; + CHECK(mesh::leds::enabled()); #ifdef STATUS_LED_RGB - if (!std::strcmp(name, "boot-zero") || !std::strcmp(name, "boot-upper") || + if (!std::strcmp(name, "quiet-fresh")) { + mesh::leds::setEnabled(false); + tick(ui, 0); tick(ui, 0x80000000u); + CHECK(writes == 0); CHECK(batt_reads == 0); CHECK(power_reads == 0); + CHECK(charge_reads == 0); + mesh::leds::setEnabled(true); + boot(ui, 0x80000000u); + } else if (!std::strcmp(name, "boot-zero") || !std::strcmp(name, "boot-upper") || !std::strcmp(name, "boot-wrap") || !std::strcmp(name, "boot-zero-end")) { const uint32_t start = !std::strcmp(name, "boot-zero") ? 0u : !std::strcmp(name, "boot-upper") ? 0x80000000u : diff --git a/test/fixtures/rtttl_rollover/queue.cpp b/test/fixtures/rtttl_rollover/queue.cpp index 7292efbb7..84ccb96cf 100644 --- a/test/fixtures/rtttl_rollover/queue.cpp +++ b/test/fixtures/rtttl_rollover/queue.cpp @@ -21,6 +21,12 @@ static void check(bool okay, unsigned line) { extern "C" void _start() { genericBuzzer buzzer; buzzer.begin(); + CHECK(buzzer.isQuiet()); + CHECK(buzzer.speakGps(true)); + buzzer.loop(); + CHECK(tone_calls == 0 && !buzzer.isPlaying() && !registers.ENABLE); + buzzer.quiet(false); // The UI applies its saved sound-on choice before use. + CHECK(!buzzer.isQuiet()); now_ms = 0xffffffffU - 100; CHECK(buzzer.speakGps(true)); buzzer.loop(); diff --git a/test/test_carrier_wave.py b/test/test_carrier_wave.py index 484bdabc9..103a5e105 100644 --- a/test/test_carrier_wave.py +++ b/test/test_carrier_wave.py @@ -45,7 +45,7 @@ struct Chip { int power=17, initial_fsk_power=0, current_limit=60; int ramp_time=4; unsigned ramp_calls=0, ramp_power_calls=0; - unsigned starts=0, stops=0, standby_calls=0, fsk_starts=0; + unsigned starts=0, stops=0, standby_calls=0, fsk_starts=0, standby_fail_call=0; unsigned raw_rx_starts=0; unsigned admission_order=0,last_receive_order=0; unsigned morse_ramp_calls=0; @@ -59,8 +59,9 @@ struct Chip { void (*callback)()=setFlag; int standby() { ++standby_calls; g_mock_millis+=standby_latency; - if (!standby_result) carrier=false; - return standby_result; + const int result=standby_fail_call==standby_calls ? -705 : standby_result; + if (!result) carrier=false; + return result; } int standby(uint8_t mode) { ++warm_standby_calls; last_standby_mode=mode; diff --git a/test/test_companion_ota_includes.py b/test/test_companion_ota_includes.py index 8edd6f2a1..c83f0f939 100644 --- a/test/test_companion_ota_includes.py +++ b/test/test_companion_ota_includes.py @@ -26,6 +26,7 @@ class CompanionOtaIncludeTest(unittest.TestCase): directory = Path(temp) for header in ( "helpers/RegulatorSettings.h", + "helpers/LedSettings.h", "helpers/ui/StartupScreen.h", "helpers/ui/DisplayPowerSettings.h", "helpers/UsbLoggingWatchdog.h", "helpers/UsbLoggingClientActivity.h", "Mesh.h", "helpers/BluetoothMac.h", "CompanionBluetooth.h", diff --git a/test/test_companion_terminal_profile.py b/test/test_companion_terminal_profile.py index 3ae9f5c36..8de1da386 100644 --- a/test/test_companion_terminal_profile.py +++ b/test/test_companion_terminal_profile.py @@ -99,8 +99,8 @@ for help_line in (' board\\r\\n', ' version\\r\\n', assert "no automatic adverts" not in help_branch assert re.search( r"#if COMPANION_FEATURE_MEMORY_DIAGNOSTICS\s+" - r'terminalOutput\(\)\.print\(" memory\\r\\n"\);\s+#endif', - terminal, + r'" memory\\r\\n"\s+#endif', + help_branch, ) # Terminal-specific handlers keep precedence, then both the generic `set` diff --git a/test/test_companion_usb_logging.py b/test/test_companion_usb_logging.py index 079d48c98..74456994d 100644 --- a/test/test_companion_usb_logging.py +++ b/test/test_companion_usb_logging.py @@ -1,5 +1,7 @@ """Execute Companion debug persistence/CLI and bounded packet logging paths.""" from pathlib import Path +import hashlib +import json import os import subprocess import tempfile @@ -322,6 +324,7 @@ class CompanionUsbLoggingTests(unittest.TestCase): self.assertEqual(built.returncode, 0, built.stdout + built.stderr) ran = subprocess.run([str(binary)], capture_output=True, text=True) self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr) + return ran.stdout def prepare(self, work): (work / 'Utils.h').write_text('''#pragma once @@ -407,6 +410,7 @@ inline char* utoa(unsigned int value,char* output,int base){ end = source.index(' } else if (handleCommand(command, 0, local_reply))', start) harness = r''' #include +#include #include #include struct Port { @@ -427,38 +431,44 @@ int main(){ assert(text.find("Commands:\r\n stats-core / stats-radio / stats-radio-diag / stats-packets\r\n")==0); assert(text.find(" get public.key\r\n")!=std::string::npos); #if MESH_ENABLE_LOST_REPLY - assert(text.find(" get lost.reply\r\n set lost.reply \r\n")!=std::string::npos); + assert(text.find(" get/set lost.reply \r\n")!=std::string::npos); #else assert(text.find("lost.reply")==std::string::npos); #endif - assert(text.find(" get usb.debug\r\n set usb.debug \r\n")!=std::string::npos); -#if MESH_USB_CONSOLE_COOPERATIVE - assert(text.find(" get usb.watchdog\r\n get usb.watchdog.last\r\n set usb.watchdog (saved; default auto)\r\n")!=std::string::npos); +#if MESH_USB_LOGGING_AVAILABLE + assert(text.find(" get/set usb.debug \r\n")!=std::string::npos); +#else + assert(text.find("usb.debug")==std::string::npos); +#endif +#if MESH_USB_LOGGING_AVAILABLE && MESH_USB_CONSOLE_COOPERATIVE + assert(text.find(" get usb.watchdog.last\r\n get/set usb.watchdog (saved; default auto)\r\n")!=std::string::npos); #else assert(text.find("usb.watchdog")==std::string::npos); #endif assert(text.find(" reboot\r\n ver\r\n")!=std::string::npos); assert(text.find(" +++MESHCORE-TERM-STOP\r\n")!=std::string::npos ? terminal : !terminal); - assert(text.find(" disconnect (closes the network terminal)\r\n")!=std::string::npos ? !terminal : terminal); + assert(text.find(" disconnect (network terminal)\r\n")!=std::string::npos ? !terminal : terminal); + for(unsigned char value : text) printf("%02x",value); + printf("\n"); } } '''.replace('@HELP@', source[start:end]) - features = ['MESH_USB_LOGGING_AVAILABLE', 'COMPANION_FEATURE_READER', - 'COMPANION_FEATURE_MEMORY_DIAGNOSTICS', 'COMPANION_FEATURE_NOTIFICATIONS', - 'COMPANION_RADIO_FULL', 'COMPANION_EXCLUSIVE_WIFI_BLE', - 'WITH_WEBCONFIG', 'MESH_PRIMARY_ESPNOW', 'COMPANION_FEATURE_TEMP_RADIO', - 'COMPANION_FEATURE_OTA_CLI', 'BLE_PIN_CODE', 'ESP32', 'NRF52_PLATFORM', - 'EXTRAFS'] + # These hashes were captured from the unmodified c46 help branch. + # Compare the complete output bytes, including every conditional line, + # rather than duplicating a second help implementation in this test. + fixture = json.loads((ROOT / 'test/fixtures/companion_help_output.sha256.json').read_text()) + self.assertEqual(fixture['schema'], 1) with tempfile.TemporaryDirectory() as directory: work = Path(directory) self.prepare(work) - for native in (0, 1): - for lost in (0, 1): - with self.subTest(native=native, lost=lost): - self.compile_and_run(work, harness, - [flag + '=1' for flag in features] + ['WIFI_SSID="test"', - 'MESH_USB_CONSOLE_COOPERATIVE=' + str(native), - 'MESH_ENABLE_LOST_REPLY=' + str(lost)]) + for name, expected in fixture['profiles'].items(): + with self.subTest(profile=name): + raw = self.compile_and_run(work, harness, expected['flags']) + outputs = [bytes.fromhex(line) for line in raw.splitlines()] + self.assertEqual(len(outputs), 2) + self.assertEqual([len(value) for value in outputs], expected['output_bytes']) + self.assertEqual([hashlib.sha256(value).hexdigest() for value in outputs], expected['output_sha256']) + if __name__ == '__main__': diff --git a/test/test_companion_wifi_compatibility.py b/test/test_companion_wifi_compatibility.py index 6265c3d30..deea7bbcf 100644 --- a/test/test_companion_wifi_compatibility.py +++ b/test/test_companion_wifi_compatibility.py @@ -33,7 +33,7 @@ HARNESS = r''' uint32_t now_ms = 100; uint32_t millis() { return now_ms; } unsigned identity_calls=0, address_calls=0, begin_calls=0, enable_calls=0; -unsigned register_calls=0, sdk_ps_calls=0; +unsigned register_calls=0, manager_enable_calls=0, sdk_ps_calls=0; bool identity_ok=true, register_ok=true, begin_ok=true; enum class InterfaceType { Bluetooth }; struct CompanionNodePrefs { @@ -49,16 +49,24 @@ struct Mesh { uint32_t getBLEPin() { return 123456; } bool isBluetoothEnabledPreference() { return preference; } } the_mesh; -struct { +struct TestInterfaceManager { + bool enabled=true; bool addInterface(InterfaceType, void*) { ++register_calls; return register_ok; } bool removeInterface(void*) { return true; } - bool isEnabled() const { return true; } + bool isEnabled() const { return enabled; } + void enableBluetooth(); } interface_manager; struct { template bool begin(T...) { ++begin_calls; return begin_ok; } void enable() { ++enable_calls; } const char* beginFailure() const { return "test failure"; } } bluetooth_interface; +// Startup now requests Bluetooth through the manager; its transport policy +// has a separate suite. These non-T1000 variants forward an allowed request. +void TestInterfaceManager::enableBluetooth() { + ++manager_enable_calls; + bluetooth_interface.enable(); +} namespace mesh { struct Logger { template void printf(T...) {} @@ -144,7 +152,9 @@ void rebootCompatibility() { assert(mesh::wifi::loadCompanionModeForBoot()); } void resetBootCounters() { - identity_calls=address_calls=begin_calls=enable_calls=register_calls=0; + identity_calls=address_calls=begin_calls=enable_calls=register_calls=manager_enable_calls=0; + interface_manager.enabled=true; + the_mesh.preference=true; companion_bluetooth_initialized=false; companion_bluetooth_start_at=0; } @@ -227,7 +237,7 @@ int main() { // The guard must precede identity writes, registration, SDK init and retry. startCompanionBluetooth(); assert(!companion_bluetooth_initialized && !companion_bluetooth_start_at); - assert(!identity_calls && !address_calls && !register_calls && !begin_calls && !enable_calls); + assert(!identity_calls && !address_calls && !register_calls && !begin_calls && !enable_calls && !manager_enable_calls); assert(companion_bluetooth_start_failure[0]); // Even an old retry deadline cannot resurrect Bluetooth in this boot. companion_bluetooth_start_at=now_ms; @@ -254,9 +264,9 @@ int main() { resetBootCounters(); startCompanionBluetooth(); assert(companion_bluetooth_initialized); - assert(identity_calls==1 && register_calls==1 && address_calls==1 && begin_calls==1 && enable_calls==1); + assert(identity_calls==1 && register_calls==1 && address_calls==1 && begin_calls==1 && enable_calls==1 && manager_enable_calls==1); startCompanionBluetooth(); serviceDeferredCompanionBluetooth(); - assert(begin_calls==1); // Already initialized remains idempotent. + assert(begin_calls==1 && manager_enable_calls==1); // Already initialized remains idempotent. #if defined(ESP32) && defined(WIFI_SSID) assert(companionWiFiBluetoothActive()); @@ -286,6 +296,16 @@ int main() { now_ms+=4999; serviceDeferredCompanionBluetooth(); assert(identity_calls==1); ++now_ms; identity_ok=true; serviceDeferredCompanionBluetooth(); assert(companion_bluetooth_initialized && identity_calls==2 && begin_calls==1); + + // Initializing the stack must not bypass the manager or saved enable gate. + resetBootCounters(); interface_manager.enabled=false; + startCompanionBluetooth(); + assert(companion_bluetooth_initialized && begin_calls==1); + assert(!manager_enable_calls && !enable_calls && !interface_manager.enabled); + resetBootCounters(); the_mesh.preference=false; + startCompanionBluetooth(); + assert(companion_bluetooth_initialized && begin_calls==1); + assert(!manager_enable_calls && !enable_calls && !the_mesh.preference); } ''' @@ -295,6 +315,7 @@ class CompanionWiFiCompatibilityTest(unittest.TestCase): main = (ROOT / "examples/companion_radio/main.cpp").read_text() signatures = [ "static void scheduleCompanionBluetoothRetry(", + "static void serviceCompanionUsbBluetoothPriority(", "static void startCompanionBluetooth(", "static void serviceDeferredCompanionBluetooth(", ] diff --git a/test/test_gps_upstream_adaptations.py b/test/test_gps_upstream_adaptations.py index 21d9aa336..0c7705d02 100644 --- a/test/test_gps_upstream_adaptations.py +++ b/test/test_gps_upstream_adaptations.py @@ -566,33 +566,54 @@ int main() { self.run_rak_uart_probe(negative=True) def test_ble_led_optout_preserves_default_and_begin_order(self): + from test_nrf52_led_policy import compile_run as run_led_policy + source = (ROOT / "src/helpers/nrf52/SerialBLEInterface.cpp").read_text() begin = extract_braced(source, "bool SerialBLEInterface::begin(") - block = re.search(r"#ifdef DISABLE_BLE_LED[\s\S]*?#endif", begin).group() - self.assertLess(begin.index(block), begin.index("Bluefruit.configPrphConn(")) - self.assertLess(begin.index(block), begin.index("if (!Bluefruit.begin())")) - self.assertLess(begin.index("if (_begin_attempted)"), begin.index(block)) + policy = "mesh_nrf52::applyBluetoothLedPolicy();" + self.assertEqual(begin.count(policy), 1) + self.assertLess(begin.index(policy), begin.index("Bluefruit.configPrphConn(")) + self.assertLess(begin.index(policy), begin.index("if (!Bluefruit.begin())")) + self.assertLess(begin.index("if (_begin_attempted)"), begin.index(policy)) harness = r''' -#include -struct BluefruitMock { - bool controlled=true;unsigned calls=0; - void autoConnLed(bool enabled){controlled=enabled;++calls;} -} Bluefruit; -void configure(){ -@BLOCK@ -} +#include +MockBluefruit Bluefruit; +int led_level = LED_STATE_ON, writes = 0; int main(){ - configure(); #ifdef DISABLE_BLE_LED - assert(!Bluefruit.controlled&&Bluefruit.calls==1); + const bool default_enabled = false; #else - assert(Bluefruit.controlled&&Bluefruit.calls==0); + const bool default_enabled = true; #endif - Bluefruit.controlled=false;configure();assert(!Bluefruit.controlled); + assert(mesh::leds::enabled()); + mesh_nrf52::applyBluetoothLedPolicy(); + assert(Bluefruit.automatic == default_enabled); + // Before begin(), configuring the owner must not query a connection or use + // the advertising timer that Bluefruit has not created yet. + assert(!Bluefruit.ready && Bluefruit.connection_queries == 0); + assert(Bluefruit.timer_starts == 0 && Bluefruit.timer_stops == 0); + mesh::leds::setEnabled(false); + mesh_nrf52::applyBluetoothLedPolicy(); + assert(!Bluefruit.automatic && led_level == !LED_STATE_ON); + assert(Bluefruit.connection_queries == 0 && Bluefruit.timer_starts == 0); + // The shared saved policy now explicitly restores enabled LEDs; the compile + // time opt-out continues to override that choice. + mesh::leds::setEnabled(true); + mesh_nrf52::applyBluetoothLedPolicy(); + assert(Bluefruit.automatic == default_enabled); + assert(Bluefruit.connection_queries == 0 && Bluefruit.timer_starts == 0); + mesh::leds::setEnabled(false); + Bluefruit.ready = true; + mesh_nrf52::bluetoothLedStarted(); + assert(!Bluefruit.automatic && Bluefruit.timer_starts == 0); } -'''.replace("@BLOCK@", block) - self.compile_run(harness) - self.compile_run(harness, ("DISABLE_BLE_LED=1",)) +''' + for disabled in (False, True): + with self.subTest(disabled=disabled): + defines = ["LED_BLUE=12", "LED_STATE_ON=1"] + if disabled: + defines.append("DISABLE_BLE_LED=1") + run_led_policy(self, harness, defines) def test_shared_success_mark_and_rak_discovery_reset_are_wired(self): provider = (ROOT / "src/helpers/sensors/MicroNMEALocationProvider.h").read_text() diff --git a/test/test_mono_led_rollover.py b/test/test_mono_led_rollover.py index bb204140b..2d12b9a5a 100644 --- a/test/test_mono_led_rollover.py +++ b/test/test_mono_led_rollover.py @@ -20,7 +20,7 @@ UIS = ('ui-orig', 'ui-new', 'ui-tiny') CASES = ('zero', 'upper', 'max', 'normal', 'unread', 'signed', 'wrap', 'off-wrap', 'zero-stamp', 'message-on', 'message-off', 'owner-fresh', 'mask-fresh', 'owner-on-long', 'owner-off-long', 'mask-on-long', 'mask-off-long', - 'suppressed-short', 'delayed-one-phase', 'both-vetoes') + 'suppressed-short', 'delayed-one-phase', 'both-vetoes', 'quiet-fresh') def harness(ui, mutation=None): @@ -41,7 +41,12 @@ def harness(ui, mutation=None): abstract, re.MULTILINE)) setters = '\n'.join(extract_braced(abstract, signature) for signature in ( 'void setNotificationLedOwnership(', 'void setNotificationOutputs(')) - if mutation == 'no-owner-guard': + if mutation == 'no-policy-guard': + guard = 'if (!mesh::leds::enabled()) return;' + if handler.count(guard) != 1: + raise AssertionError('the actual board LED policy guard must be present') + handler = handler.replace(guard, '', 1) + elif mutation == 'no-owner-guard': guard = 'if (_notification_led_owned || !(_notification_outputs & mesh::notify::Led)) return;' if handler.count(guard) != 1: raise AssertionError('the actual ownership/output guard must be present') @@ -68,6 +73,9 @@ class MonoLedRolloverTest(unittest.TestCase): def test_missing_ownership_guard_fails_semantic_check(self): self.run_variants('no-owner-guard', ('owner-fresh',), 'FAIL owner-fresh: writes == count') + def test_missing_board_policy_guard_fails_semantic_check(self): + self.run_variants('no-policy-guard', ('quiet-fresh',), 'FAIL quiet-fresh: writes == count') + def test_early_edge_regression_is_rejected(self): self.run_variants('non-strict-edge', ('zero',), 'FAIL zero: writes == count') diff --git a/test/test_rgb_led_rollover.py b/test/test_rgb_led_rollover.py index e90d69123..24a3dc482 100644 --- a/test/test_rgb_led_rollover.py +++ b/test/test_rgb_led_rollover.py @@ -24,7 +24,7 @@ CASES = ( 'poll-wrap-before', 'poll-wrap-after', 'poll-zero-stamp', 'poll-long', 'poll-near-full', 'poll-read-delays', 'owner-fresh', 'mask-fresh', 'owner-flourish-short', 'mask-flourish-long', 'owner-on-long', - 'owner-off-long', 'mask-on-long', 'mask-off-long', 'both-vetoes', + 'owner-off-long', 'mask-on-long', 'mask-off-long', 'both-vetoes', 'quiet-fresh', ) POWER_CASES = ( 'power-cadence', 'charging', 'charged', 'charge-unread', @@ -45,7 +45,12 @@ def harness(mutation=None): setters = '\n'.join(extract_braced(abstract, signature) for signature in ( 'void setNotificationLedOwnership(', 'void setNotificationOutputs(')) handler = extract_braced(source, 'void UITask::userLedHandler()') - if mutation == 'no-owner-guard': + if mutation == 'no-policy-guard': + guard = 'if (!mesh::leds::enabled()) return;' + if handler.count(guard) != 1: + raise AssertionError('the actual board LED policy guard must be present') + handler = handler.replace(guard, '', 1) + elif mutation == 'no-owner-guard': guard = 'if (_notification_led_owned || !(_notification_outputs & mesh::notify::Led)) return;' if handler.count(guard) != 1: raise AssertionError('the actual ownership/output guard must be present') @@ -81,6 +86,9 @@ class RgbLedRolloverTest(unittest.TestCase): def test_ownership_regression_is_rejected(self): self.run_variants('no-owner-guard', ('owner-fresh',), 'batt_reads == 0') + def test_board_policy_regression_is_rejected(self): + self.run_variants('no-policy-guard', ('quiet-fresh',), 'writes == 0') + def test_captured_poll_anchor_regression_is_rejected(self): self.run_variants('completion-time-anchor', ('poll-read-delays',), 'batt_reads == 2') diff --git a/test/test_rtttl_rollover.py b/test/test_rtttl_rollover.py index e7b5cdd4e..91d6ea414 100644 --- a/test/test_rtttl_rollover.py +++ b/test/test_rtttl_rollover.py @@ -187,10 +187,14 @@ class RuntimeTests(unittest.TestCase): return subprocess.run([str(binary)], capture_output=True, timeout=10).returncode def test_actual_unpatched_versions_reproduce_both_semantic_rollover_failures(self): + queue_lines = (FIXTURES / 'queue.cpp').read_text(encoding='ascii').splitlines() + rollover_check = ' CHECK(tone_calls == 2 && buzzer.isPlaying() && !registers.ENABLE);' + self.assertEqual(queue_lines.count(rollover_check), 1) + rollover_failure = queue_lines.index(rollover_check) + 1 for version in VERSIONS: with self.subTest(version=version): self.assertEqual(self.build_run(source_for(version), version, "regression.cpp"), 30) - self.assertEqual(self.build_run(source_for(version), version, "queue.cpp"), 33) + self.assertEqual(self.build_run(source_for(version), version, "queue.cpp"), rollover_failure) def test_patched_versions_keep_tempo_empty_stop_replacement_and_real_voice_queue(self): for version in VERSIONS: