From b25005f52f521cd97230be5b3caf6672c5bc3779 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 7 Sep 2026 16:55:28 -0700 Subject: [PATCH] Document and test 1.17.1.5 USB logging sleep workaround --- .github/workflows/run-unit-tests.yml | 5 + docs/_javascript/firmware_picker.js | 25 ++++- docs/cli_build_matrix.md | 2 +- docs/cli_commands.md | 22 ++++- docs/companion_radio_full.md | 12 ++- docs/firmware_picker.md | 20 +++- docs/full_companion_features.md | 15 ++- docs/releases/1.17.1.5.md | 29 +++++- docs/role_feature_switches.md | 30 +++++- test/README.md | 1 + test/test_firmware_picker.js | 22 ++++- test/test_logging_sleep_contract.py | 140 +++++++++++++++++++++++++++ 12 files changed, 302 insertions(+), 21 deletions(-) create mode 100644 test/test_logging_sleep_contract.py diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index f01699e7..ea3627b1 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -33,6 +33,11 @@ jobs: - name: Verify ESP32 USB sleep and G3 button wake run: python3 -B test/test_esp32_usb_sleep.py + - name: Verify logging sleep guards and instructions + run: | + python3 -B test/test_logging_sleep_contract.py + node test/test_firmware_picker.js + - name: Verify nRF52 UF2-reset CLI coverage run: python3 -B test/test_nrf52_uf2reset_cli.py diff --git a/docs/_javascript/firmware_picker.js b/docs/_javascript/firmware_picker.js index 358ba8d9..83955af4 100644 --- a/docs/_javascript/firmware_picker.js +++ b/docs/_javascript/firmware_picker.js @@ -657,6 +657,7 @@ profile = Object.assign(parseTargetProfile(file.target), { files: [], otaPackaged: false, + releaseFamily: releaseSet.familyTag, }); grouped.set(file.target, profile); } @@ -939,6 +940,11 @@ }); } + function needsUsbLoggingSleepWorkaround(profile) { + return profile.chipFamily === "esp32" && + profile.releaseFamily === "v1.17.1.5-halo-keymind-cascade-dev-26303793"; + } + function installSteps(profile, kind) { const common = [ "Verify that the hardware name and every displayed variant match the physical board.", @@ -1014,6 +1020,10 @@ ); } } + if (needsUsbLoggingSleepWorkaround(profile) && + (profile.logging === "runtime" || profile.logging === "usb-runtime")) { + extra.push("1.17.1.5 ESP32 USB logging: run powersaving off before enabling USB logs. This saved workaround also applies to USB + WiFi logging; WiFi-only logging does not need the USB step."); + } return common.concat(byKind[kind] || [], extra); } @@ -1045,6 +1055,9 @@ ? ["set logging.output " + (mode === "none" ? "off" : mode), "get logging.output"] : ["set usb.logging " + (enabled ? "on" : "off") + (profile.dedicatedUsbLogging ? " reboot" : "")]; + if (enabled && needsUsbLoggingSleepWorkaround(profile)) { + commands.unshift("powersaving off"); + } return { label: LOGGING_LABELS[mode], commands: commands, text: full && info && info.mqtt @@ -1054,11 +1067,17 @@ : "", }; }); - section("Restore the selected logging mode", actions, - full ? (profile.dedicatedUsbLogging + let loggingNote = full ? (profile.dedicatedUsbLogging ? "Reboot adds/removes the second logging port. Keep Companion/MOTA on primary interface 00." : "USB logs and binary Companion share one port. Turn logging off, send +++MESHCORE-TERM-STOP, and close the console before connecting the app or MOTA host.") - : "Saved settings survive updates. These commands restore your selected output mode; downloading alone does not change it."); + : "Saved settings survive updates. These commands restore your selected output mode; downloading alone does not change it."; + if (needsUsbLoggingSleepWorkaround(profile)) { + loggingNote += " For 1.17.1.5 USB logging, powersaving off prevents the released USB sleep problem. Turning logging off does not restore power saving."; + if (infrastructure && profile.logging === "runtime") { + loggingNote += " WiFi-only MQTT already blocks sleep while the bridge is running; check get bridge.running."; + } + } + section("Restore the selected logging mode", actions, loggingNote); } if (!info) return sections; if (full || infrastructure) { diff --git a/docs/cli_build_matrix.md b/docs/cli_build_matrix.md index 1572cc95..0d77b7af 100644 --- a/docs/cli_build_matrix.md +++ b/docs/cli_build_matrix.md @@ -75,7 +75,7 @@ and TLora MQTT profiles to 50. Check the artifact capability manifest and | FULL ESP32 USB + WiFi | Uses the matching MQTT target with packet logging on, verbose debug off, and the complete command surface supported by that role and hardware. `get/set logging.output off\|usb\|wifi\|both` selects and persists the active output paths. | | FULL ESP32 logging fallback | Uses the matching non-MQTT target only when no WiFi MQTT sibling exists, with debug and packet logging enabled and the complete command surface supported by that role and hardware. Its persistent USB gate also covers output-off operation, avoiding a second FULL ESP-NOW image. | | nRF52 dual-CDC Full Companion | Fresh installs expose only interface `00`; it starts as an ASCII terminal and automatically hands a complete `<` frame to framed Companion. The same interface also carries exclusive serial mOTA traffic. Enabling logging and rebooting adds interface `02` for plaintext logs. BLE and source-only LoRa OTA remain available. `get/set usb.logging` persistently controls whether the logging interface is present. | -| ESP32 single-TTY Full Companion | Every ESP32 Full image starts with the ASCII terminal on its one USB TTY and automatically hands a complete `<` frame to framed Companion. `set usb.logging on` switches that TTY to an input-capable plaintext logging terminal and makes framed Companion unavailable on USB; `set usb.logging off` stops logging but leaves the TTY in normal ASCII mode. The terminal stop token or a valid incoming framed probe then performs the ordinary switch to Binary Companion. A saved logging-on setting starts directly in that logging terminal and disables automatic frame detection. BLE, WiFi, and source-only LoRa OTA remain available. ESP32 Full uses Arduino-ESP32 2.x where supported; RC32 and ESP32-C6 keep their board-required Arduino 3.x platform but still expose only one TTY. | +| ESP32 single-TTY Full Companion | Every ESP32 Full image starts with the ASCII terminal on its one USB TTY and automatically hands a complete `<` frame to framed Companion. On 1.17.1.5, run `powersaving off` first. `set usb.logging on` switches that TTY to an input-capable plaintext logging terminal and makes framed Companion unavailable on USB; `set usb.logging off` stops logging but leaves the TTY in normal ASCII mode. The terminal stop token or a valid incoming framed probe then performs the ordinary switch to Binary Companion. A saved logging-on setting starts directly in that logging terminal and disables automatic frame detection. BLE, WiFi, and source-only LoRa OTA remain available. ESP32 Full uses Arduino-ESP32 2.x where supported; RC32 and ESP32-C6 keep their board-required Arduino 3.x platform but still expose only one TTY. | | `no_external_sensors` | Trims selected optional environmental/ranging drivers and their settings; it does not remove generic I2C, core repeater discovery, routing, or runtime RS-232 commands. RAK3401 and RAK4631 profiles retain the four common INA I2C voltage/current monitors. GPS-preserving RAK nRF52 OTA profiles retain their GPS commands and provider; RAK4631 defaults the bridge to UART 2 because RAK12501/L76K GPS uses UART 1. Legacy target suffixes remain stable for OTA identity compatibility. | The four retained INA drivers are entries in the optional environmental-sensor diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 0a1f715d..172595f3 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -750,6 +750,19 @@ set usb.logging on reboot set usb.logging off reboot ``` +**ESP32 1.17.1.5 USB logging procedure:** disable device sleep before enabling +the log stream, using separate text commands: + +```text +powersaving off +set usb.logging on +``` + +Verify with `powersaving` (expect `off`) and `get usb.logging` (expect `on`). +Both settings are saved; disabling logging later does not restore power +saving. This works around the [released USB sleep bug](releases/1.17.1.5.md#g3-usb-disappearance-with-power-saving-enabled). +nRF52 does not need this ESP32 workaround. + These commands are compiled into ordinary USB-loggable artifacts and every Full Companion. They control live USB debug and packet output. CommonCLI roles save the setting in `/com_prefs`, so it survives reboot; their first boot @@ -772,7 +785,7 @@ required when the USB interface count must change. The exact choice, send their reply, and reboot one second later only when needed. On every ESP32 Full Companion, enter the USB text terminal and use -`set usb.logging on` to turn that TTY into a logging-repeater-style plaintext +`set usb.logging on` (preceded by `powersaving off` on 1.17.1.5) to turn that TTY into a logging-repeater-style plaintext stream. Framed Binary Companion is unavailable on USB while logging is on. The TTY remains an input-capable CLI, so `set usb.logging off` works on the same TTY. After its reply, logging stops and the TTY remains in the normal ASCII @@ -806,6 +819,13 @@ stream to both consumers; do not point both consumers at the same broker unless the downstream setup deduplicates messages. Fresh unified FULL installs start in `both` mode. +On **1.17.1.5 ESP32**, run `powersaving off` before selecting +`set logging.output usb` or `set logging.output both`, since those modes +enable USB logging. **WiFi/MQTT-only logging does not need this workaround +while the Repeater/Room Server MQTT bridge is running**; the released code +already blocks device sleep in that state. Check `get bridge.running`, not +only `get bridge.enabled`. WiFi modem power saving is a separate setting. + ### Begin capture of rx log to node storage **Search terms:** save logs, record received packets, RX logging, stored packet log. diff --git a/docs/companion_radio_full.md b/docs/companion_radio_full.md index f3cbb396..b38ff819 100644 --- a/docs/companion_radio_full.md +++ b/docs/companion_radio_full.md @@ -436,8 +436,16 @@ logging off on a fresh installation. Every ESP32 Full Companion exposes one USB TTY with two exclusive modes. It starts as the ASCII terminal unless a saved logging-on preference boots directly into the logging terminal. If it is already binary, enter its text -terminal with `+++MESHCORE-TERM-START`, then run `set usb.logging on`; the same -TTY emits plaintext packet/debug logs and continues accepting CLI commands, +terminal with `+++MESHCORE-TERM-START`. For an ESP32 1.17.1.5 USB logging session, +run these separate commands: + +```text +powersaving off +set usb.logging on +``` + +The first command is the release's USB power-saving workaround. The same +TTY then emits plaintext packet/debug logs and continues accepting CLI commands, including `set usb.logging off`. Framed Binary Companion is unavailable on USB while logging owns the TTY. Turning logging off sends the command reply, stops the logs, and leaves that TTY in the normal ASCII terminal, matching a fresh diff --git a/docs/firmware_picker.md b/docs/firmware_picker.md index 323e0aaa..0465a26e 100644 --- a/docs/firmware_picker.md +++ b/docs/firmware_picker.md @@ -255,7 +255,8 @@ restores the board default, so a separate rotated release image is not recommended. Ordinary non-OTA roles also use one artifact for normal operation and USB -logging. Select the saved mode with `set usb.logging off|on`; no `-logging-` +logging. On ESP32 1.17.1.5, run `powersaving off` before `set usb.logging on`. +Select the saved mode with `set usb.logging off|on`; no `-logging-` artifact is emitted. KISS, BLE-only Companion, and constrained LoRa OTA repeater images retain their protocol/partition contracts and do not inherit plaintext USB logging. @@ -265,8 +266,15 @@ as an ASCII terminal and automatically hands a complete `<` frame to Binary Companion. `set usb.logging on reboot` adds its plaintext interface `02`. Every ESP32 Full Companion instead exposes one USB TTY. Logging is off by -default, so the TTY serves the ASCII/Binary Companion switcher. Use -`set usb.logging on` to turn that same TTY into an input-capable plaintext +default, so the TTY serves the ASCII/Binary Companion switcher. On 1.17.1.5, +run these two text commands to enable USB logging: + +```text +powersaving off +set usb.logging on +``` + +The second command turns that same TTY into an input-capable plaintext CLI/logging stream; framed Binary Companion is unavailable on USB while logging owns it. `set usb.logging off` stops the logs and leaves the TTY in the normal ASCII terminal, matching a fresh Full installation. Send @@ -277,6 +285,12 @@ Arduino-ESP32 2.x base where the board supports it; RC32 and ESP32-C6 retain their board-required Arduino 3.x platform but still expose only one USB TTY. A second ESP32 CDC interface is not part of the release profile. +The picker includes the power-saving workaround when selecting **USB** or +**USB + WiFi** logging on ESP32 1.17.1.5. WiFi/MQTT-only logging does not need +it while the Repeater/Room Server bridge is running; check `get bridge.running`. +The workaround is not added to nRF52 directions. See [logging by role](role_feature_switches.md) +for the saved settings and the original firmware's USB sleep issue. + ## Installation methods | File | Use | diff --git a/docs/full_companion_features.md b/docs/full_companion_features.md index e0bf8e1f..84e4d83f 100644 --- a/docs/full_companion_features.md +++ b/docs/full_companion_features.md @@ -36,7 +36,7 @@ editor does not necessarily forward them. | Radio chip RX boost | `set radio.rxgain on` | `set radio.rxgain off` | Immediately; saved; supported radios only | | External FEM RX boost | `set radio.fem.rxgain on` | `set radio.fem.rxgain off` | Immediately; saved; controllable FEM only | | External FEM TX gain | `set radio.fem.txgain on` | `set radio.fem.txgain off` | Immediately; saved; controllable PA only | -| ESP32 USB packet/debug logging | `set usb.logging on` | `set usb.logging off` | Immediately; saved; logging owns the USB terminal | +| ESP32 USB packet/debug logging | For 1.17.1.5: `powersaving off`, then `set usb.logging on` | `set usb.logging off` | Immediately; saved; logging owns the USB terminal | | nRF52 separate USB logging port | `set usb.logging on reboot` | `set usb.logging off reboot` | Saves and reboots to add/remove the second USB port | | ESP32 browser settings | `set webui on` | `set webui off` | Saved; starts/stops WebConfig | | ESP32 temporary setup portal | `start webconfig ap` | `stop webconfig` | This session; opens a setup network/QR where available | @@ -48,6 +48,19 @@ Check a saved switch with the corresponding `get` command, for example inspect device power saving. Fresh installations enable device power saving and leave USB logging off. Existing saved preferences take precedence. +For an **ESP32 1.17.1.5 USB logging session**, use this sequence in the text +terminal: + +```text +powersaving off +set usb.logging on +``` + +The power-saving step is the documented USB-session workaround for this +release. It is saved separately; `set usb.logging off` does not restore power +saving. nRF52 does not need the ESP32 workaround. WiFi-only MQTT logging does +not need this USB step; see [logging by role](role_feature_switches.md). + ESP32 logging and Binary Companion share one USB port. After turning logging off, return to Binary Companion with `+++MESHCORE-TERM-STOP`. nRF52 keeps its optional logging port separate from its primary USB port and BLE connection. diff --git a/docs/releases/1.17.1.5.md b/docs/releases/1.17.1.5.md index b010a531..f343f448 100644 --- a/docs/releases/1.17.1.5.md +++ b/docs/releases/1.17.1.5.md @@ -10,7 +10,8 @@ The firmware identifier and main release tag follow `v1.17.1.5-halo-keymind-cascade-dev-`, matching the 1.17.1.4 naming convention. Supplemental pages retain the `repeater-room-`, `utility-`, `logging-`, `lora-ota-`, and `full-profiles-` prefixes where those outputs -exist. All pages are development prereleases. +exist. The Companion page is marked Latest; the supplemental pages remain +development prereleases. This release uses the USA/Canada radio preset and Cascade runtime defaults. The release build records the resolved frequency, bandwidth, spreading factor, @@ -32,15 +33,20 @@ button is only polled. Closing a terminal must not be treated as unplugging the USB host. Espressif documents the underlying [native USB sleep limitation](https://docs.espressif.com/projects/esp-idf/en/v4.4.7/esp32s3/api-guides/usb-otg-console.html#limitations). -On 1.17.1.5, use the repeater's remote admin CLI, or its USB terminal before -it disappears: +To enable **USB logging on an ESP32 1.17.1.5 image with logging support**, +disable power saving first. Use the role's text terminal, or the repeater's +remote admin CLI if USB has already disappeared: ```text powersaving off +set usb.logging on powersaving +get usb.logging ``` -The second command should return `off`. This saves the setting and takes +The read-back commands should report power saving `off` and USB logging `on`. +For an image without USB logging, use just `powersaving off` and `powersaving` +to apply/check the sleep workaround. Disabling power saving saves the setting and takes effect immediately; it does not erase the node or change LoRa RX power saving (`radio.rxps`). If USB does not reappear after disabling sleep remotely, power-cycle once and check that the port and display button remain @@ -48,6 +54,18 @@ usable beyond the two-minute mark. Ordinary reflashing can retain the saved power-saving setting, so check it during comparisons with another firmware build too. +On unified Full infrastructure, also run `powersaving off` before enabling +USB through `set logging.output usb` or `set logging.output both`. These are +separate commands. Turning logging off does not automatically restore the +saved power-saving setting. nRF52 logging does not need this ESP32 workaround. + +**WiFi/MQTT-only logging does not need `powersaving off` while the +Repeater/Room Server MQTT bridge is running.** The original `26303793` code +already prevents device sleep in that state. Select `set logging.output wifi` +where supported and check `get bridge.running`; `get bridge.enabled` alone +only checks the saved preference. Full Companion MQTT uses WebConfig instead +of infrastructure `logging.output`/`bridge.*` commands. + The source correction keeps native USB serviced while a host is enumerated, including when its terminal is closed. Enabled live USB logging also blocks ESP32 light sleep, even without a host. FreeRTOS can still idle/yield; this @@ -59,7 +77,8 @@ LoRa. Button activity keeps the CPU and display awake for at least two minutes; holding or pressing the button again extends the interval. The usual display timeout can keep the screen on longer. An intentional long-press power-off retains its existing action. After the wake interval, sleep is allowed when -power saving is enabled, logging is off, and no native USB host is attached. +power saving is enabled, logging is off, no native USB host is attached, and +no running bridge or pending work needs service. Host simulations cover both native USB modes, terminal close/reconnect, logging toggles, OTA inhibition, simultaneous button/LoRa wake configuration, diff --git a/docs/role_feature_switches.md b/docs/role_feature_switches.md index b2f0982c..76ff292a 100644 --- a/docs/role_feature_switches.md +++ b/docs/role_feature_switches.md @@ -75,7 +75,7 @@ Full Companion MQTT switches. | Radio RX boost | `set radio.rxgain on` | `set radio.rxgain off` | `get radio.rxgain` | | External FEM RX gain | `set radio.fem.rxgain on` | `set radio.fem.rxgain off` | `get radio.fem.rxgain` | | External FEM TX gain | `set radio.fem.txgain on` | `set radio.fem.txgain off` | `get radio.fem.txgain` | -| ESP32 USB logging | `set usb.logging on` | `set usb.logging off` | `get usb.logging` | +| ESP32 USB logging | `powersaving off`, then `set usb.logging on` | `set usb.logging off` | `powersaving`, `get usb.logging` | | nRF52 second USB logging port | `set usb.logging on reboot` | `set usb.logging off reboot` | `get usb.logging` | | ESP32 persistent WebConfig | `set webui on` | `set webui off` | `get webui` | | ESP32 temporary setup AP | `start webconfig ap` | `stop webconfig` | `get webui` | @@ -145,7 +145,7 @@ from some portable builds. The USB browser console still works without it. | Setting | Enable | Disable | Read back | | --- | --- | --- | --- | -| Live USB logging | `set usb.logging on` | `set usb.logging off` | `get usb.logging` | +| Live USB logging | ESP32 1.17.1.5: `powersaving off`, then `set usb.logging on`; other platforms: `set usb.logging on` | `set usb.logging off` | `powersaving`, `get usb.logging` | | Capture RX log to node storage | `log start` | `log stop` | `log` prints the capture locally | | MQTT / RS232 / ESP-NOW bridge master | `set bridge.enabled on` | `set bridge.enabled off` | `get bridge.enabled`, `get bridge.running`, `get bridge.type` | | MQTT periodic status publication | `set mqtt.status on` | `set mqtt.status off` | `get mqtt.status` shows connection status | @@ -165,6 +165,21 @@ Infrastructure `set usb.logging` has **no `reboot` suffix**, including nRF52. `log start/stop` records to storage independently of live USB logging. Use `log erase` to delete that capture. +For **ESP32 1.17.1.5 USB logging**, run these as separate commands in the +role's text terminal (or remote admin CLI on infrastructure): + +```text +powersaving off +set usb.logging on +powersaving +get usb.logging +``` + +Check for power saving `off` and USB logging `on`. This avoids the released +ESP32 USB sleep bug, including the G3 report. Both settings are saved; turning +logging off later does not automatically restore power saving. LoRa RXPS is +independent. nRF52 logging does not need this ESP32 workaround. + With the [G3 sleep correction](releases/1.17.1.5.md#g3-usb-disappearance-with-power-saving-enabled), enabled live USB logging keeps ESP32 USB serviced and blocks light sleep, including when a host closes the port or disconnects. CPU idle/yield remains @@ -183,9 +198,9 @@ On unified Full infrastructure with both MQTT and USB logging compiled: | Command | USB logs | MQTT bridge | | --- | --- | --- | | `set logging.output off` | Off | Off | -| `set logging.output usb` | On | Off | +| `powersaving off`, then `set logging.output usb` | On | Off | | `set logging.output wifi` | Off | On | -| `set logging.output both` | On | On | +| `powersaving off`, then `set logging.output both` | On | On | `get logging.output` reports the selection. Fresh unified Full preferences select `both`; saved settings override this. To toggle only MQTT while keeping @@ -193,6 +208,13 @@ USB logging unchanged, use `set bridge.enabled off` / `on`. Neither setting turns LoRa repeating off. Repeater forwarding uses `set repeat off` / `on` and `get repeat` separately. +The `powersaving off` step above is the **1.17.1.5 ESP32 USB workaround**. +**WiFi/MQTT-only logging does not need it while the Repeater/Room Server MQTT +bridge is running:** that sleep guard already exists in the released firmware. +Use `get bridge.running` to check that the bridge is running; an enabled +preference alone is not the running state. `set logging.output wifi` keeps +USB logging off and uses that MQTT guard. + For a custom broker on an MQTT-capable Repeater or Room Server: ```text diff --git a/test/README.md b/test/README.md index e9ae6250..44ca4b21 100644 --- a/test/README.md +++ b/test/README.md @@ -30,6 +30,7 @@ python3 test/test_esp32_full_partition.py # Full partition-preservation po python3 test/test_esp32_dram.py # Classic ESP32 static limits and cached-image build gates python3 test/test_esp32_usb_serial_hygiene.py # Single-TTY diagnostics/NVS contract python3 test/test_esp32_usb_sleep.py # USB/logging sleep guards, G3 button wake and two-minute hold/rollover +python3 test/test_logging_sleep_contract.py # USB enable examples and real Repeater/Room MQTT sleep gates python3 test/test_esp32_tinyusb_role_hygiene.py # G2/room USB write coverage and bounded-list contracts python3 test/test_esp32_tinyusb_cooperative_output.py # Real role dump/list pumps with host C++ stubs python3 test/test_esp32_tinyusb_nonblocking.py # Native CDC stalled-host/64-byte-FIFO simulation (C++17 compiler) diff --git a/test/test_firmware_picker.js b/test/test_firmware_picker.js index 0a7ee174..daaef89c 100644 --- a/test/test_firmware_picker.js +++ b/test/test_firmware_picker.js @@ -877,6 +877,7 @@ const controlledReleases = [release(liveFamily, '2026-09-01T00:00:00Z', [ asset('heltec_v4_repeater_observer_mqtt-full-usb-wifi-ota-' + liveFamily + '.bin'), ]), release('repeater-room-' + liveFamily, '2026-09-01T00:00:00Z', [ asset('RAK_4631_repeater-' + liveFamily + '.uf2'), + asset('Station_G3_ESP32_repeater-' + liveFamily + '.bin'), ])]; const controlled = picker.buildCatalog(controlledReleases, controls); const findControlled = name => controlled.profiles.find(p => p.target === name); @@ -885,10 +886,24 @@ const observer = findControlled('heltec_v4_repeater_observer_mqtt-full-usb-wifi' for (const mode of ['none', 'usb', 'wifi', 'both']) { const directions = picker.runtimeDirections(observer, {logging: mode}); assert.deepStrictEqual(directions[0].actions[0].commands, - ['set logging.output ' + (mode === 'none' ? 'off' : mode), 'get logging.output']); + (mode === 'usb' || mode === 'both' ? ['powersaving off'] : []).concat( + ['set logging.output ' + (mode === 'none' ? 'off' : mode), 'get logging.output'])); } const mqttCompanion = findControlled('heltec_v4_2_v4_3_companion_radio_full_femon'); assert.deepStrictEqual(mqttCompanion.loggingModes, ['none', 'usb', 'wifi', 'both']); +for (const mode of ['usb', 'both']) { + assert.deepStrictEqual(picker.runtimeDirections(mqttCompanion, {logging: mode})[0].actions[0].commands, + ['powersaving off', 'set usb.logging on']); +} +assert(picker.installSteps(mqttCompanion, 'bin').some(step => step.includes('powersaving off'))); +// The workaround belongs to the affected release, not every future ESP32 build. +for (const releaseFamily of ['v1.17.1.4-old', 'v1.17.1.5-halo-keymind-cascade-dev-newfix', + 'v1.17.1.6-next', 'v1.17.1.50-next', '']) { + const otherRelease = {...mqttCompanion, releaseFamily}; + assert.deepStrictEqual(picker.runtimeDirections(otherRelease, {logging: 'usb'})[0].actions[0].commands, + ['set usb.logging on']); + assert(!picker.installSteps(otherRelease, 'bin').some(step => step.includes('powersaving off'))); +} const companionWifi = picker.runtimeDirections(mqttCompanion, {logging: 'wifi'}); assert.deepStrictEqual(companionWifi[0].actions[0].commands, ['set usb.logging off']); assert(companionWifi[0].actions[0].text.includes('enable the desired MQTT')); @@ -900,6 +915,11 @@ const nrf = findControlled('RAK_4631_companion_radio_full'); assert.deepStrictEqual(picker.runtimeDirections(nrf, {logging: 'usb'})[0].actions[0].commands, ['set usb.logging on reboot']); const rakRepeater = findControlled('RAK_4631_repeater'); +const g3Repeater = findControlled('Station_G3_ESP32_repeater'); +assert.deepStrictEqual(picker.runtimeDirections(g3Repeater, {logging: 'usb'})[0].actions[0].commands, + ['powersaving off', 'set usb.logging on']); +assert.deepStrictEqual(picker.runtimeDirections(g3Repeater, {logging: 'none'})[0].actions[0].commands, + ['set usb.logging off']); const rakCommands = picker.runtimeDirections(rakRepeater, {logging: 'usb', mode: 'standard'}); assert.deepStrictEqual(rakCommands[0].actions[0].commands, ['set usb.logging on']); assert.strictEqual(rakCommands.find(s => s.title.startsWith('RS232')).actions[0].label, 'Off'); diff --git a/test/test_logging_sleep_contract.py b/test/test_logging_sleep_contract.py new file mode 100644 index 00000000..e4845731 --- /dev/null +++ b/test/test_logging_sleep_contract.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +"""Check USB workaround instructions and execute the real MQTT sleep gates.""" + +from pathlib import Path +import os +import re +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] + + +def method(path, signature): + text = (ROOT / path).read_text() + start = text.index(signature) + end = text.index("{", start) + 1 + depth = 1 + while depth: + depth += (text[end] == "{") - (text[end] == "}") + end += 1 + return text[start:end] + + +HARNESS = r''' +#include +#include +#include +#define WITH_BRIDGE 1 +struct AbstractBridge { virtual bool isRunning() const = 0; }; +struct MQTTBridge : AbstractBridge { + bool _initialized = false; + @RUNNING@ +}; +struct Radio { + bool isWatchdogObserving() const { return false; } + bool isCalibratingNoiseFloor() const { return false; } +}; +struct MyMesh { + MQTTBridge* bridge = nullptr; + const AbstractBridge* activeBridge() const { return bridge; } + struct { bool pending = false; } deferred_cli_command; + struct { bool hasActiveUserGpioTimer() const { return false; } } _cli; + struct { int getNumClients() const { return 0; } } acl; + struct { bool battery_alert_enabled = false; } _prefs; + Radio radio_driver; + bool pending_self_advert = false; + bool saved_radio_apply_pending = false, temp_radio_applied = false; + unsigned long next_flood_advert = 0, next_local_advert = 0; + unsigned long dirty_contacts_expiry = 0, next_recent_repeater_sweep = 0; + unsigned long next_battery_alert_check = 0, next_push = 0; + unsigned long radio_apply_retry_at = 0, set_radio_at = 0, revert_radio_at = 0; + bool hasPendingOtaApply() const { return false; } + bool hasQueuedWorkDue() const { return false; } + bool hasRetryWorkDue() const { return false; } + bool hasScheduledRadioWorkDue() const { return false; } + bool isMillisTimerDue(unsigned long) const { return false; } + bool millisHasNowPassed(unsigned long) const { return false; } + bool getNextQueueWakeDelay(uint32_t&) const { return false; } + bool getNextRetryWakeDelay(uint32_t&) const { return false; } + uint32_t limitSleepToMillisTimer(unsigned long, uint32_t secs) const { return secs; } + uint32_t limitSleepToScheduledRadioWork(uint32_t secs) const { return secs; } + bool hasPendingWork() const; + uint32_t getPowerSaveSleepSeconds(uint32_t) const; +}; +@METHODS@ +static void require(bool ok, const char* why) { + if (!ok) throw std::runtime_error(why); +} +int main() { + try { + MyMesh node; + require(node.getPowerSaveSleepSeconds(30) == 30, "quiet node cannot sleep"); + MQTTBridge logger; + node.bridge = &logger; + require(node.getPowerSaveSleepSeconds(30) == 30, "stopped MQTT blocked sleep"); + logger._initialized = true; + require(node.hasPendingWork(), "running MQTT lost its sleep blocker"); + require(node.getPowerSaveSleepSeconds(30) == 0, + "WiFi-only logging can enter device sleep without a USB guard"); + logger._initialized = false; + require(node.getPowerSaveSleepSeconds(30) == 30, + "stopping MQTT did not release its sleep blocker"); + require(node.getPowerSaveSleepSeconds(0) == 0, "zero sleep limit changed"); + } catch (const std::exception& e) { + std::cerr << e.what() << '\n'; + return 1; + } +} +''' + + +class LoggingSleepContractTest(unittest.TestCase): + def test_usb_enable_examples_include_workaround_first(self): + for path in ( + "docs/cli_commands.md", "docs/role_feature_switches.md", + "docs/full_companion_features.md", "docs/companion_radio_full.md", + "docs/firmware_picker.md", "docs/releases/1.17.1.5.md", + ): + with self.subTest(path=path): + text = (ROOT / path).read_text() + blocks = re.findall(r"```text\n(.*?)```", text, re.S) + self.assertTrue(any("powersaving off\nset usb.logging on\n" in block + for block in blocks), path) + self.assertNotRegex(text, r"powersaving off\nset usb.logging on reboot") + + def test_wifi_exception_is_documented_with_running_state_check(self): + for path in ("docs/cli_commands.md", "docs/role_feature_switches.md", + "docs/releases/1.17.1.5.md"): + with self.subTest(path=path): + text = " ".join((ROOT / path).read_text().split()) + self.assertRegex(text, r"WiFi/MQTT-only logging.*?does not need") + self.assertIn("get bridge.running", text) + + def test_running_mqtt_prevents_sleep_in_repeater_and_room(self): + running = method("src/helpers/bridges/MQTTBridge.h", + "bool isRunning() const override") + with tempfile.TemporaryDirectory(prefix="meshcore-mqtt-sleep-") as temp: + for role in ("simple_repeater", "simple_room_server"): + with self.subTest(role=role): + source = f"examples/{role}/MyMesh.cpp" + methods = "\n".join(method(source, signature) for signature in ( + "bool MyMesh::hasPendingWork() const", + "uint32_t MyMesh::getPowerSaveSleepSeconds(uint32_t max_secs) const", + )) + cpp = Path(temp) / f"{role}.cpp" + binary = Path(temp) / role + cpp.write_text(HARNESS.replace("@RUNNING@", running) + .replace("@METHODS@", methods)) + built = subprocess.run([ + os.environ.get("CXX", "c++"), "-std=c++17", "-Wall", "-Wextra", + str(cpp), "-o", str(binary), + ], capture_output=True, text=True) + self.assertEqual(built.returncode, 0, built.stderr) + result = subprocess.run([str(binary)], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + + +if __name__ == "__main__": + unittest.main()