From 8505361ebe88484e5c82e81726add67a60074e42 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Thu, 1 Oct 2026 23:19:48 -0700 Subject: [PATCH] Restore Companion boot display and default WiFi to opt-in --- docs/WiFi.md | 17 ++ docs/cli_commands.md | 16 ++ docs/companion_radio_full.md | 7 + examples/companion_radio/MyMesh.cpp | 12 +- examples/companion_radio/main.cpp | 6 +- src/helpers/ui/DisplayPowerPolicy.h | 14 +- src/helpers/ui/DisplayPowerSettings.cpp | 3 +- src/helpers/ui/DisplayPowerSettings.h | 4 +- .../test_companion_preferences_transaction.py | 20 ++- test/test_companion_usb_default.py | 37 +++++ test/test_display_power.py | 151 +++++++++++++++++- 11 files changed, 276 insertions(+), 11 deletions(-) diff --git a/docs/WiFi.md b/docs/WiFi.md index 1249a24bb..5faee6f50 100644 --- a/docs/WiFi.md +++ b/docs/WiFi.md @@ -206,6 +206,23 @@ image over USB once when moving a device from the legacy partition layout. ## WiFi companion setup +Full and Bluetooth/wired-capable Companions default to infrastructure WiFi off. +Bluetooth starts where the build supports it, and saved WiFi enablement is +preserved across upgrades. Before using the setup AP or station, enable WiFi +through the USB or Bluetooth CLI: + +```text +get wifi.enabled +set wifi.enabled 1 +``` + +ESP32 simultaneous-transport builds apply the change without a reboot. SenseCAP +Indicator exclusive-transport builds and Pico W require a reboot. WiFi-only +compatibility builds without any other CLI transport retain WiFi on so they are +not left unreachable. Saving an SSID/password alone does not enable WiFi on an +opt-in build. Use `set wifi.enabled 0` to save it off again. This controls +infrastructure WiFi, not a primary ESP-NOW mesh radio. + A `*_companion_radio_wifi` build replaces the BLE or USB companion link with the MeshCore companion protocol over TCP port 5000. The phone or computer must be able to reach the device on the same LAN. diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 8cd98807b..33eedecb0 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -363,6 +363,15 @@ new address; USB password input is masked. Binary Companion clients can use USB, BLE, or TCP port 5000 without the terminal-start token: send command `0x42` (`CMD_RUN_CLI_COMMAND`) followed by the same CLI text, such as `get wifi.powersave`, `get wifi.pwd`, `stats-core`, or `set wifi.powersave min`. +Full and Bluetooth/wired-capable Companions default to infrastructure WiFi off +and Bluetooth on where supported. Legacy WiFi-only builds without another CLI +transport retain WiFi on so they are not left unreachable. +Use `get wifi.enabled` and `set wifi.enabled 1` to inspect or persistently enable +WiFi; `set wifi.enabled 0` disables it. Saved enablement survives upgrades, and +credentials alone do not turn it on. ESP32 simultaneous-transport builds apply +the change from the main loop; exclusive-transport builds and Pico W apply it +after reboot. This does not disable a primary ESP-NOW mesh radio or add Bluetooth +to a WiFi-only build. See [local maintenance commands](terminal_chat_cli.md#local-maintenance-commands) for the complete access rules. WiFi-only Companions accept all three modes. A Full Companion that runs BLE and infrastructure WiFi @@ -900,6 +909,13 @@ saved settings. The WebConfig **Display** card exposes the same controls: Modes are `off`, `on`, `button`, `pairing`, `button-pairing`, and `automatic`. Pairing modes are available only on builds supporting BLE pairing. +All display-equipped Companion builds show the screen at boot in the default +`button` or `button-pairing` mode, on battery or USB power. It stays on for the +selected profile's timeout (15 seconds by default); button input restarts that +timer. This also applies to previously saved button modes. Saved `off` and +pairing-only modes still suppress the boot screen. Repeaters, room servers, +and sensors do not gain this Companion-only boot wake. + - `off`: stays dark, including boot and pairing. - `on`: stays on while the device is awake. - `button`: a physical button, keyboard/navigation input, or supported touch diff --git a/docs/companion_radio_full.md b/docs/companion_radio_full.md index 958f240d4..e7709ca21 100644 --- a/docs/companion_radio_full.md +++ b/docs/companion_radio_full.md @@ -18,6 +18,13 @@ updating other nodes. Most targets can run those transports simultaneously; the SenseCAP Indicator exception selects one secondary wireless transport per boot as described below. +Fresh Full Companion installs default to Bluetooth on and infrastructure WiFi off. +WiFi remains compiled in: use `set wifi.enabled 1` through the USB or Bluetooth +CLI to enable it, or `set wifi.enabled 0` to disable it persistently. Existing +saved WiFi enablement is honored after an upgrade. The SenseCAP Indicator +requires a reboot to apply its exclusive transport selection; primary ESP-NOW +mesh operation is unaffected by the infrastructure-WiFi default. + The full Companion is deliberately not a LoRa OTA destination: it has no firmware staging store, refuses `ota install`, and never advertises its own firmware as an mOTA image. diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index d3e9342d1..c586592bc 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2300,7 +2300,14 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.rx_ps_sleep_us = RX_POWERSAVING_DEFAULT_SLEEP_US; _prefs.powersaving_enabled = 1; _prefs.powersaving_policy_version = 0; + // Keep WiFi-only compatibility builds reachable when they have no other + // CLI transport. Full and Bluetooth/wired-capable Companions are opt-in. +#if defined(WIFI_SSID) && !defined(BLE_PIN_CODE) && !defined(ENABLE_USB_INTERFACE) \ + && !defined(SERIAL_RX) && !defined(ETHERNET_ENABLED) _prefs.wifi_enabled = 1; +#else + _prefs.wifi_enabled = 0; +#endif memset(_prefs.bluetooth_name, 0, sizeof(_prefs.bluetooth_name)); _prefs.display_rotation_degrees = 0; _prefs.cad_enabled = DEFAULT_CAD_ENABLED ? 1 : 0; @@ -2398,10 +2405,11 @@ void MyMesh::begin(bool has_display, bool radio_available) { mesh::ui::loadDisplayPowerSettings(_store->getPrimaryFS(), #ifdef BLE_PIN_CODE - true + true, #else - false + false, #endif + true // All Companion transports show startup unless the saved mode forbids it. ); #if COMPANION_FEATURE_NOTIFICATIONS diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 686285e89..35f1db982 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -1273,7 +1273,7 @@ void halt() { static char configured_wifi_ssid[32]; static char configured_wifi_password[65]; static bool companion_wifi_has_credentials = false; - static bool companion_wifi_requested = true; + static bool companion_wifi_requested = false; static bool companion_wifi_active = false; static bool companion_wifi_disable_in_progress = false; static bool companion_wifi_services_stopped = false; @@ -1291,7 +1291,7 @@ void halt() { #if defined(COMPANION_EXCLUSIVE_WIFI_BLE) static CompanionTransportMode companion_transport_boot_mode = - CompanionTransportMode::WiFi; + CompanionTransportMode::Bluetooth; static bool companion_transport_boot_mode_loaded = false; CompanionTransportMode getCompanionTransportMode() { @@ -3074,7 +3074,7 @@ void setup() { } else { stopCompanionInfrastructureWiFi(); board.setInhibitSleep(false); - WIFI_DEBUG_PRINTLN("WiFi remains off from the saved BOOT/GPIO 0 setting"); + WIFI_DEBUG_PRINTLN("WiFi remains off; use set wifi.enabled 1 to enable it"); } #if defined(COMPANION_EXCLUSIVE_WIFI_BLE) } diff --git a/src/helpers/ui/DisplayPowerPolicy.h b/src/helpers/ui/DisplayPowerPolicy.h index 8bc26e297..121ccc549 100644 --- a/src/helpers/ui/DisplayPowerPolicy.h +++ b/src/helpers/ui/DisplayPowerPolicy.h @@ -25,6 +25,8 @@ struct DisplayPowerPrefs { DisplayPowerProfile battery; DisplayPowerProfile usb; DisplayInboxMode inbox = DisplayInboxMode::History; + // Runtime role policy, not serialized with the user's display settings. + bool wake_on_boot = false; }; inline DisplayPowerPrefs& displayPowerPrefs() { @@ -70,7 +72,8 @@ public: void update(const DisplayPowerPrefs& prefs, bool usb, bool connected, bool pairing, uint32_t now) { const auto& selected = usb ? prefs.usb : prefs.battery; - const bool reset = !_initialized || _usb != usb || _profile.mode != selected.mode; + const bool startup = !_initialized; + const bool reset = startup || _usb != usb || _profile.mode != selected.mode; const bool timeout_changed = _profile.seconds != selected.seconds; const bool just_connected = connected && !_connected; _profile = selected; @@ -80,7 +83,14 @@ public: if (reset) { _timed = false; _initialized = true; - wake(DisplayWake::Boot, now); + // Companions show startup for the normal button timeout. Only the first + // initialization counts; changing profiles must not synthesize input. + if (startup && prefs.wake_on_boot && (_profile.mode == DisplayMode::Button + || _profile.mode == DisplayMode::ButtonPairing)) { + wake(DisplayWake::Button, now); + } else { + wake(DisplayWake::Boot, now); + } } else if (timeout_changed && _timed) { _deadline = now + uint32_t(_profile.seconds) * 1000; } diff --git a/src/helpers/ui/DisplayPowerSettings.cpp b/src/helpers/ui/DisplayPowerSettings.cpp index efb90949f..4259eb8aa 100644 --- a/src/helpers/ui/DisplayPowerSettings.cpp +++ b/src/helpers/ui/DisplayPowerSettings.cpp @@ -80,10 +80,11 @@ bool savePrefs(const DisplayPowerPrefs& prefs) { } } -bool loadDisplayPowerSettings(FILESYSTEM* fs, bool pairing_supported) { +bool loadDisplayPowerSettings(FILESYSTEM* fs, bool pairing_supported, bool wake_on_boot) { settings_fs = fs; pairing_available = pairing_supported; DisplayPowerPrefs prefs; + prefs.wake_on_boot = wake_on_boot; if (pairing_supported) prefs.battery.mode = prefs.usb.mode = DisplayMode::ButtonPairing; displayPowerPrefs() = prefs; if (!fs) return false; diff --git a/src/helpers/ui/DisplayPowerSettings.h b/src/helpers/ui/DisplayPowerSettings.h index 13dc73630..81fbdce37 100644 --- a/src/helpers/ui/DisplayPowerSettings.h +++ b/src/helpers/ui/DisplayPowerSettings.h @@ -7,7 +7,9 @@ namespace mesh { namespace ui { // Separate, versioned file: existing node and MQTT preference layouts stay // readable by older firmware. Returns true when a saved profile was loaded. -bool loadDisplayPowerSettings(FILESYSTEM* fs, bool pairing_supported); +// Companion startup enables a timed boot screen without changing saved modes. +bool loadDisplayPowerSettings(FILESYSTEM* fs, bool pairing_supported, + bool wake_on_boot = false); bool handleDisplayPowerCommand(const char* command, char* reply, size_t reply_size, bool inbox_supported = false); bool displayPairingSupported(); diff --git a/test/test_companion_preferences_transaction.py b/test/test_companion_preferences_transaction.py index c05cf8b4d..fed49413c 100644 --- a/test/test_companion_preferences_transaction.py +++ b/test/test_companion_preferences_transaction.py @@ -128,6 +128,7 @@ int main(int argc,char** argv){ } DataStore fresh;CompanionNodePrefs defaults;double lat=0,lon=0; assert(fresh.loadPrefs(defaults,lat,lon)); + assert(defaults.wifi_enabled==0); // Fresh installs never enable WiFi implicitly. assert(fresh.savePrefs(defaults,lat,lon)); } else if(scenario==2){ #if defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM) @@ -198,6 +199,23 @@ int main(int argc,char** argv){ assert(legacy.loadPrefs(loaded,lat,lon)); assert(loaded.flood_retry_advert_enabled==0); assert(loaded.one_key_dm_enabled==0); + } else if(scenario==8){ + for(uint8_t enabled : {0,1}){ + CompanionNodePrefs saved=original;saved.wifi_enabled=enabled; + assert(store.savePrefs(saved,47.1,-122.2)); + CompanionNodePrefs loaded;double lat=0,lon=0; + loaded.wifi_enabled=1-enabled; + assert(store.loadPrefs(loaded,lat,lon)); + assert(loaded.wifi_enabled==enabled); // Saved choices override the default. + assert(store.fs.files["/new_prefs"][156]==enabled); // Established byte offset. + } + for(unsigned size : {84,144,155,156}){ + DataStore legacy;legacy.fs.files["/new_prefs"]=store.fs.files["/new_prefs"]; + legacy.fs.files["/new_prefs"].resize(size); + CompanionNodePrefs loaded;double lat=0,lon=0; + assert(legacy.loadPrefs(loaded,lat,lon)); + assert(loaded.wifi_enabled==0); // Old files with no WiFi field use opt-in. + } } else if(scenario==4){ DataStore legacy;legacy.fs.files["/node_prefs"]=disk; legacy.fs.fail_write_after=17; @@ -285,7 +303,7 @@ inline char* utoa(unsigned int value,char* output,int base){ str(ROOT / 'src/helpers/TxtDataHelpers.cpp'), '-o', str(binary)], capture_output=True, text=True) self.assertEqual(build.returncode, 0, build.stdout + build.stderr) - for scenario in range(8): + for scenario in range(9): with self.subTest(scenario=scenario): run = subprocess.run([str(binary), str(scenario)], capture_output=True, text=True) self.assertEqual(run.returncode, 0, run.stdout + run.stderr) diff --git a/test/test_companion_usb_default.py b/test/test_companion_usb_default.py index a21f66869..caa2c05ea 100644 --- a/test/test_companion_usb_default.py +++ b/test/test_companion_usb_default.py @@ -170,6 +170,43 @@ int main() { class CompanionUsbDefaultTest(unittest.TestCase): + def test_wifi_defaults_preserve_a_reachable_cli(self): + mesh = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text() + start = mesh.index(" // Keep WiFi-only compatibility builds reachable") + end = mesh.index(" memset(_prefs.bluetooth_name", start) + defaults = mesh[start:end] + profiles = { + "esp32-full": (["ESP32=1", "WIFI_SSID=1", "BLE_PIN_CODE=123456", + "ENABLE_USB_INTERFACE=1", "COMPANION_RADIO_FULL=1"], 0), + "esp32-ble-wifi": (["ESP32=1", "WIFI_SSID=1", "BLE_PIN_CODE=123456"], 0), + "esp32-usb-wifi": (["ESP32=1", "WIFI_SSID=1", "ENABLE_USB_INTERFACE=1"], 0), + "esp32-wifi-only": (["ESP32=1", "WIFI_SSID=1"], 1), + "pico-usb-ble-wifi": (["RP2040_PLATFORM=1", "WIFI_SSID=1", + "ENABLE_USB_INTERFACE=1", "BLE_PIN_CODE=123456"], 0), + "pico-wifi-only": (["RP2040_PLATFORM=1", "WIFI_SSID=1"], 1), + "ethernet-wifi": (["ESP32=1", "WIFI_SSID=1", "ETHERNET_ENABLED=1"], 0), + "hardware-serial-wifi": (["ESP32=1", "WIFI_SSID=1", "SERIAL_RX=1"], 0), + "indicator-exclusive": (["ESP32=1", "WIFI_SSID=1", "BLE_PIN_CODE=123456", + "ENABLE_USB_INTERFACE=1", "COMPANION_EXCLUSIVE_WIFI_BLE=1"], 0), + "nrf52-ble": (["NRF52_PLATFORM=1", "BLE_PIN_CODE=123456"], 0), + "usb-only": (["ENABLE_USB_INTERFACE=1"], 0), + } + with tempfile.TemporaryDirectory() as directory: + binary = Path(directory) / "wireless-defaults" + for name, (flags, expected) in profiles.items(): + with self.subTest(profile=name): + source = ("#include \nstruct Prefs { unsigned wifi_enabled = 99; } _prefs;\n" + "int main() {\n" + defaults + + f"assert(_prefs.wifi_enabled == {expected});\n}}\n") + build = subprocess.run(["g++", "-std=c++17", "-x", "c++", + *["-D" + flag for flag in flags], + "-", "-o", str(binary)], input=source, + text=True, capture_output=True) + self.assertEqual(build.returncode, 0, build.stdout + build.stderr) + subprocess.run([str(binary)], check=True) + self.assertIn("static bool companion_wifi_requested = false;", MAIN) + self.assertIn("companion_transport_boot_mode =\n CompanionTransportMode::Bluetooth;", MAIN) + def test_startup_and_reconnection_for_each_usb_transport(self): functions = "\n".join(function("static void " + name + "(") for name in ( "resetUsbTerminalHostSession", "serviceUsbTerminalHostSessionReset", diff --git a/test/test_display_power.py b/test/test_display_power.py index 6a04992eb..533aa63d3 100644 --- a/test/test_display_power.py +++ b/test/test_display_power.py @@ -1,5 +1,6 @@ """Exercise the shared display policy and real settings/CLI code on the host.""" from pathlib import Path +import re import shutil import subprocess import tempfile @@ -109,6 +110,135 @@ void policyTests() { p.update(prefs, false, false, true, 1001); assert(!p.on()); // Off wins over a pending button wake and pairing. } +void companionBootTests() { + // The startup exception is role-wide, independent of transport, power + // source, or whether an app has already connected by the first UI frame. + for (bool usb : {false, true}) for (bool connected : {false, true}) { + for (unsigned mode = 0; mode <= 5; ++mode) { + DisplayPowerPrefs prefs; + prefs.wake_on_boot = true; + prefs.battery.mode = prefs.usb.mode = DisplayMode::Off; + auto& profile = usb ? prefs.usb : prefs.battery; + profile.mode = DisplayMode(mode); + DisplayPowerPolicy p; + p.update(prefs, usb, connected, false, 100); + const bool button_mode = mode == 2 || mode == 4; + assert(p.on() == (mode == 1 || button_mode || (mode == 5 && !connected))); + p.update(prefs, usb, connected, false, 4100); + assert(p.on() == (mode == 1 || button_mode)); // Normal 15s, not 4s. + p.update(prefs, usb, connected, false, 15099); + assert(p.on() == (mode == 1 || button_mode)); + p.update(prefs, usb, connected, false, 15100); + assert(p.on() == (mode == 1)); + p.update(prefs, usb, connected, false, 30000); + assert(p.on() == (mode == 1)); // Refresh cannot retrigger startup. + } + } + for (DisplayMode mode : {DisplayMode::Button, DisplayMode::ButtonPairing}) { + DisplayPowerPrefs prefs; + prefs.wake_on_boot = true; + prefs.battery = prefs.usb = {mode, 27}; + DisplayPowerPolicy p; + p.update(prefs, false, false, false, 100); + p.update(prefs, false, true, false, 101); + assert(p.on()); // Connecting does not dismiss a button-mode boot screen. + assert(!p.wake(DisplayWake::Message, 200)); + p.update(prefs, false, true, false, 27099); + assert(p.on()); + assert(p.wake(DisplayWake::Button, 27099)); + p.update(prefs, false, true, false, 27100); + assert(p.on()); // Real input extends the boot timeout. + p.update(prefs, false, true, false, 54099); + assert(!p.on()); + p.update(prefs, false, true, true, 54100); + assert(p.on() == (mode == DisplayMode::ButtonPairing)); + p.update(prefs, false, true, true, 100000); + assert(p.on() == (mode == DisplayMode::ButtonPairing)); + p.update(prefs, false, true, false, 100001); + assert(!p.on()); + + DisplayPowerPolicy switched; + switched.update(prefs, false, false, false, 0); + assert(switched.on()); + switched.update(prefs, true, false, false, 1); + assert(!switched.on()); // No synthetic boot when the power source changes. + switched.update(prefs, false, false, false, 2); + assert(!switched.on()); + prefs.battery.mode = DisplayMode::Off; + switched.update(prefs, false, false, false, 3); + prefs.battery.mode = mode; + switched.update(prefs, false, false, false, 4); + assert(!switched.on()); // No synthetic boot when the mode changes. + + DisplayPowerPolicy dismissed; + dismissed.update(prefs, false, false, false, 0); + dismissed.dismiss(); + dismissed.update(prefs, false, false, false, 1); + assert(!dismissed.on()); + + DisplayPowerPolicy rollover; + rollover.update(prefs, false, false, false, UINT32_MAX - 500); + rollover.update(prefs, false, false, false, 26498); + assert(rollover.on()); + rollover.update(prefs, false, false, false, 26499); + assert(!rollover.on()); + } +} +void companionSettingsTests() { + for (bool ble : {false, true}) { + FakeFS fs; + assert(!loadDisplayPowerSettings(&fs, ble, true)); + assert(displayPowerPrefs().wake_on_boot); + const DisplayMode mode = ble ? DisplayMode::ButtonPairing : DisplayMode::Button; + assert(displayPowerPrefs().battery.mode == mode); + assert(displayPowerPrefs().usb.mode == mode); + char reply[160]; + for (const char* command : {"set display.timeout 27", "set display.usb.timeout 123"}) { + assert(handleDisplayPowerCommand(command, reply, sizeof(reply))); + assert(std::string(reply) == "OK"); + } + assert(fs.files["/display_prefs"].size() == 12); // No on-device format change. + assert(fs.files["/display_prefs"][10] == 0); + assert(loadDisplayPowerSettings(&fs, ble, true)); + assert(displayPowerPrefs().wake_on_boot); + for (bool usb : {false, true}) { + DisplayPowerPolicy p; + p.update(displayPowerPrefs(), usb, false, false, 0); + assert(p.on()); // Previously saved button modes also show startup. + const uint32_t timeout = usb ? 123000 : 27000; + p.update(displayPowerPrefs(), usb, false, false, timeout - 1); + assert(p.on()); + p.update(displayPowerPrefs(), usb, false, false, timeout); + assert(!p.on()); + } + for (const char* command : {"set display.mode off", "set display.usb.mode off"}) { + assert(handleDisplayPowerCommand(command, reply, sizeof(reply))); + assert(std::string(reply) == "OK"); + } + for (unsigned version : {1, 2}) { + auto& saved = fs.files["/display_prefs"]; + saved[2] = version; saved[11] = 0; + for (unsigned i = 0; i < 11; ++i) saved[11] ^= saved[i]; + assert(loadDisplayPowerSettings(&fs, ble, true)); + assert(displayPowerPrefs().wake_on_boot); + for (bool usb : {false, true}) { + DisplayPowerPolicy p; + p.update(displayPowerPrefs(), usb, false, true, 0); + assert(!p.on()); // Saved off wins over startup and pairing. + assert(!p.wake(DisplayWake::Button, 1)); + } + } + assert(loadDisplayPowerSettings(&fs, ble)); + assert(!displayPowerPrefs().wake_on_boot); // Infrastructure does not opt in. + assert(!loadDisplayPowerSettings(nullptr, ble, true)); + assert(displayPowerPrefs().wake_on_boot); // Defaults still work without flash. + DisplayPowerPolicy p; + p.update(displayPowerPrefs(), false, false, false, 0); + assert(p.on()); + assert(!loadDisplayPowerSettings(nullptr, ble)); + assert(!displayPowerPrefs().wake_on_boot); + } +} void commandTests() { FakeFS fs; assert(!loadDisplayPowerSettings(&fs, true)); @@ -190,11 +320,30 @@ void commandTests() { run("get display.timeout", "> 42"); run("get display.usb.timeout", "> 42"); } -int main() { policyTests(); commandTests(); } +int main() { + policyTests(); companionBootTests(); companionSettingsTests(); commandTests(); +} ''' class DisplayPowerTest(unittest.TestCase): + def test_companion_boot_wake_is_transport_and_layout_independent(self): + mesh = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text() + self.assertRegex(mesh, re.compile( + r"loadDisplayPowerSettings\(_store->getPrimaryFS\(\),\s*" + r"#ifdef BLE_PIN_CODE\s*true,\s*#else\s*false,\s*#endif\s*true\b")) + main = (ROOT / "examples/companion_radio/main.cpp").read_text() + setup = main[main.index("void setup()"):main.index("\nvoid loop()")] + self.assertLess(setup.index("the_mesh.begin("), setup.index("ui_task.begin(")) + for layout in ("ui-new", "ui-orig", "ui-tiny"): + with self.subTest(layout=layout): + ui = (ROOT / "examples/companion_radio" / layout / "UITask.cpp").read_text() + start = ui.index("void UITask::begin(") + end = ui.index("\n}", start) + self.assertIn("_display->servicePower(", ui[start:end]) + infrastructure = (ROOT / "src/helpers/CommonCLI.cpp").read_text() + self.assertIn("loadDisplayPowerSettings(fs, false);", infrastructure) + def test_policy_and_persistent_commands(self): compiler = shutil.which("g++") or shutil.which("clang++") self.assertIsNotNone(compiler, "host C++ compiler required")