mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-22 17:41:19 +00:00
Wake Heltec V4 buttons from power-saving sleep
This commit is contained in:
1 parent
c88885a315
commit
ebf2f0823e
7 files changed
+718
-6
No files matched your search
@@ -11,6 +11,9 @@ layout before installing an image.
|
||||
- ESP32 native USB recipes now use hardware CDC where the chip supports it.
|
||||
Classic ESP32 and explicit UART transports keep their UART path. nRF52,
|
||||
RP2040 and STM32 USB backends are unchanged by this migration.
|
||||
- Heltec V4 and V4 R8 user buttons wake CPU light sleep when power saving is
|
||||
enabled. Button debounce and click deadlines remain awake so the display can
|
||||
process the press. Explicit display-off mode still suppresses screen wake.
|
||||
- ESP32 USB output is bounded so a stopped reader cannot indefinitely hold
|
||||
the radio loop. The G2 preserves complete serial packet-log lines under
|
||||
congestion. This supports unmodified Cisien meshcoretomqtt; congested
|
||||
@@ -126,7 +129,18 @@ endpoint, reported the WebConfig handoff, and served its page and exact G2
|
||||
identity over HTTP. OTA stopped cleanly and the original WebConfig, settings,
|
||||
Admin ACLs, Pi network and host services were restored. The earlier radio CAD
|
||||
warning remained unchanged; the earlier LAN upload failure's cause is still
|
||||
unproven. The current GitHub unit workflow remains pending for this held release.
|
||||
unproven. The GitHub unit workflow passed for source `c88885a3`; this release
|
||||
remains held and unpublished.
|
||||
|
||||
The normal Full Heltec V4 Repeater from `c88885a3` passed two fresh-flash
|
||||
connections through the unchanged native Chromium setup GUI. With power saving
|
||||
on, its quiet console checks passed for 600 seconds open and closed with logging
|
||||
off, and 600 seconds open with USB logging. See the [V4 native console
|
||||
qualification](../test-results/hardware_validation_heltec_v4_native_console_2026-10-06.md)
|
||||
for deadlines, host preparation and retained failed attempts. USB remained
|
||||
attached, so these checks do not measure CPU sleep or button wake. The subsequent
|
||||
V4 button-wake correction and its software validation are separate; physical
|
||||
button wake was not tested here.
|
||||
|
||||
Build qualification does not establish physical compatibility on every board.
|
||||
Hardware CDC has finite buffers, and closing a Linux TTY does not necessarily
|
||||
|
||||
@@ -92,8 +92,11 @@ its recovery ACLs, and its running MQTT services were not changed by this run.
|
||||
|
||||
## Remaining limits
|
||||
|
||||
The user's permanent/intermittent web-console hang has not been reproduced or
|
||||
proven fixed. Current tests do not qualify every host OS, physical USB adapter,
|
||||
cold power cycle, ESP32 board, or firmware profile. The final V4 build has not
|
||||
been flashed through Chromium's remote physical Web Serial interface. Keep
|
||||
those limits separate from the successful browser OTA and console idle checks.
|
||||
The subsequent [current Full V4 native browser qualification](hardware_validation_heltec_v4_native_console_2026-10-06.md)
|
||||
adds actual Pi Chromium Web Serial, the unchanged setup GUI's five-second
|
||||
deadline, and measured fresh-flash and power-saving console idle results. The
|
||||
older source and boundaries above remain historical evidence. Neither report
|
||||
qualifies every host OS, physical USB adapter, cold power cycle, ESP32 board,
|
||||
firmware profile, or radio traffic load. USB-attached console checks do not
|
||||
establish CPU sleep, measured current or button wake; the subsequent V4
|
||||
button-wake correction needs separate qualification.
|
||||
@@ -0,0 +1,153 @@
|
||||
# Heltec V4 Full native browser console - 2026-10-06
|
||||
|
||||
The normal Full Heltec V4 Repeater from source
|
||||
`c88885a315208908e050c57fc0ec0a71ff10bd6f` passed fresh-flash connection,
|
||||
600 seconds with the console open, and 600 seconds with it closed through the
|
||||
unchanged `config.meshcore.io` setup GUI. Power saving was on and logging output
|
||||
was off during those idle intervals. A separate fresh-flash and 600-second
|
||||
open-console trial also passed with USB logging on. Both trials restored the
|
||||
original settings and closed their streams. Final operator readback and owned
|
||||
browser/host-service cleanup also passed. Release 1.17.1.9 remains held and
|
||||
unpublished.
|
||||
|
||||
These results extend the [earlier startup investigation](hardware_validation_esp32_web_console_startup_2026-10-05.md)
|
||||
to the current normal Full image and native hardware CDC. They are physical
|
||||
console stability checks, not a measurement of sleep current, radio traffic
|
||||
capacity, or every ESP32/host combination.
|
||||
|
||||
## Image and build gates
|
||||
|
||||
The requested target was `heltec_v4_repeater`, Full profile. Its resolved
|
||||
`heltec_v4_repeater_observer_mqtt` feature base retains the normal repeater
|
||||
update identity. Runtime readback had to match the complete version
|
||||
`v1.17.1.9-halo-keymind-cascade-dev-c88885a3 (Build: 06-Oct-2026)`.
|
||||
The source includes the OTA upload correction in `d2983762` and WiFi ownership
|
||||
correction in `95913c53`. The GitHub unit workflow for the current source passed;
|
||||
this does not replace the physical checks below or the final release matrix.
|
||||
|
||||
| Artifact | Bytes | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| Full application | 2,097,528 | `1f10f44149b3f56edbe0ecb2a2bab9b51da651a6d047e2c2c1555691468d2b4c` |
|
||||
| Merged image | 2,163,064 | `3264d7a1a5713755b5dd10ad1c81bb0588e4a072eeaa126b86307a42be77bb5b` |
|
||||
| Matching ELF | - | `0545eef46da30a86f86531199a91e887167d463c5981f193c0455cd75d633538` |
|
||||
|
||||
The package hash, linked capability, and RAM gates passed, with 20,880 bytes
|
||||
of margin above the required heap budget. The linked application includes
|
||||
HWCDC, its retained-PHY handoff and startup owner hook; no TinyUSB USBCDC class
|
||||
is linked. Full ESP-NOW, WiFi/LoRa OTA, and USB logging capability checks passed.
|
||||
These are build checks; this run did not exercise those radio/OTA features.
|
||||
|
||||
Both application slots are `0x640000` bytes, at `0x10000` and `0x650000`.
|
||||
The padded partition table SHA-256 is
|
||||
`9af3af2b74e944337ba85f2b0027ee80df160579a1ab746ba0f95853f618cd60`.
|
||||
The merged image contains that table and the exact audited application at
|
||||
`0x10000`. The application fits either slot with 4,456,072 bytes of margin.
|
||||
The merged artifact was audited, but the physical tests wrote the application
|
||||
only; they do not qualify a factory erase or merged-image installation.
|
||||
|
||||
## Native browser boundary
|
||||
|
||||
Chromium `154.0.8037.92` ran on the Pi with native OS Web Serial ports and
|
||||
streams. The actual Vue GUI and stock SerialCLI were loaded before flashing.
|
||||
The stock DOM Connect flow after the stock flasher returned sent the normal
|
||||
plain `time <epoch>` request. Its unchanged 5,000 ms timeout covers both native
|
||||
write completion and the framed response. No additional reset, cable action,
|
||||
post-flash settle, command retry, or keepalive was inserted to turn that
|
||||
connection into a pass.
|
||||
|
||||
| Website source | SHA-256 |
|
||||
| --- | --- |
|
||||
| GUI `index.html` | `efd64d1b56d57988fc4108f3f84efc943ed4c42ade4e4dbf8139c46fb3c0476d` |
|
||||
| GUI `src/gui.js` | `919034806e903fbc41e29a367a7d854837f6520f47aa34e5d0618f5dd3d29800` |
|
||||
| GUI `lib/serial-cli.js` | `fbfbdcfa8bf1a372a0396d342e018ba8ee83d33b7b98c3a546fd37cd7e8ce870` |
|
||||
| Flasher `js/flash.js` | `e9762a29f6a91f8e33736e02ff41a81cd49e7b96fe253a5d71cba7daefd46d5b` |
|
||||
| Flasher `lib/esp32.js` | `2a896d5e520ea9b6ea9900223c2460df797c3b2287f441daa4e50168a50b17e6` |
|
||||
|
||||
Automation selected only the single preauthorized native device. Before each
|
||||
write, actual ROM reads checked the partition table and valid OTA sequence/CRC,
|
||||
then directed the application write to the verified active slot (`app1` at
|
||||
`0x650000`). The stock loader transfer, reset and disconnect remained in place;
|
||||
full erase was disabled. Phase instrumentation recorded existing calls without
|
||||
adding hardware operations. This qualifies that stock flash backend and native
|
||||
GUI connection, not the complete firmware-picker selection/erase flow.
|
||||
|
||||
Original public identity, the ACL response, and 15 checked saved preferences
|
||||
were compared with the original private reference after flashing and cleanup.
|
||||
Only the numeric channel-busy diagnostic counter was normalized in preference
|
||||
comparison. No raw identities, credentials or console replies are published.
|
||||
|
||||
## Physical results
|
||||
|
||||
| Check | Result |
|
||||
| --- | --- |
|
||||
| Fresh flash, then first GUI connection with logging off | PASS. Plain time clock-set acknowledgement in 1,614.4 ms; complete GUI bootstrap and exact version/reference checks passed. |
|
||||
| Power saving on, logging off, console open | PASS. 600.093 seconds with zero native command writes or keepalives. Uptime advanced 18 -> 619 seconds; the first post-idle core reply took 35.9 ms. A full DOM Disconnect/Connect reread acknowledged time in 23.6 ms. |
|
||||
| Power saving on, logging off, console closed | PASS. 600.100 seconds with streams closed and zero writes. Uptime advanced 627 -> 1232 seconds. The first DOM reconnect time response took 33.2 ms; it was the recognized framed clock-cannot-go-backwards rejection, not a clock-set acknowledgement. Full GUI bootstrap and version checks still passed. |
|
||||
| Separate fresh flash, power saving on, USB logging | PASS. Fresh plain time acknowledgement in 696.1 ms, then 600.101 seconds open with zero writes or keepalives. Uptime advanced 18 -> 618 seconds; first post-idle core reply took 40.4 ms. Full DOM reconnect time acknowledgement took 24.8 ms. |
|
||||
| Both trial cleanups | PASS. Original power saving off, logging output USB, all 15 checked preferences, public identity and ACL response were restored. The GUI port, reader and writer closed. |
|
||||
|
||||
Uptime gates account for the surrounding query/GUI bootstrap time. None of the
|
||||
three quiet intervals contains a native write event. Both trials recorded zero
|
||||
native command errors and no GUI modem-signal calls.
|
||||
|
||||
Radio observations stayed in active RX with zero physical RX errors. The core
|
||||
error bitmask changed from `0` to `8`, with zero received packets and one sent
|
||||
packet. This is the configured receive-silence watchdog warning, consistent
|
||||
with the quiet-channel recovery policy; it is not eight errors or a CPU reset.
|
||||
Neither trial establishes error-free RF operation or a loaded radio soak.
|
||||
|
||||
## Retained setup failures and host qualification
|
||||
|
||||
Two preceding browser attempts remain failed and unqualified. Attempt 2 timed
|
||||
out before metadata completion or any GUI command was recorded; its exact
|
||||
flasher substage was not instrumented. Attempt 4 timed out in the stock loader
|
||||
connection/reset loop after 674 USB JTAG reset entries. It did not reach loader
|
||||
main return, application `writeFlash`, or a GUI command. Neither failure
|
||||
exercised the GUI's five-second command timer, so they are not counted as
|
||||
firmware console timeout failures or silently converted into passing trials.
|
||||
|
||||
Before the passing logging-off attempt, the host TTY was normalized from
|
||||
`VMIN=0, VTIME=0` to `VMIN=1, VTIME=0`, with zero explicit modem-signal calls
|
||||
in that preparation. No firmware or website code changed. That host setup is
|
||||
part of the qualified configuration; the evidence does not isolate the original
|
||||
ROM synchronization failure's cause. The second logging trial started without
|
||||
another external CLI preflight or TTY change.
|
||||
|
||||
The pinned stock flasher also calls `connect(resetMode, romBaudrate)` from
|
||||
`detectChip`, although the second `connect` argument is a retry count with a
|
||||
default of seven. The supplied value is 115,200. This explains the excessive
|
||||
retry budget after failed synchronization, not why synchronization first failed.
|
||||
It is an external flasher issue; no website patch was required for the passing
|
||||
GUI tests and no website deployment is claimed.
|
||||
|
||||
## Evidence and limits
|
||||
|
||||
Final read-only operator verification again matched the exact version, original
|
||||
15 checked preferences, public identity and ACL response; uptime was 686 seconds
|
||||
and flags remained `8`. The owned Chromium profile and tunnel were closed, the
|
||||
serial port had no owner, and original host service states were restored:
|
||||
ModemManager and mctomqtt active, mesh-logger inactive. No Pi network
|
||||
configuration changed. Restored service state does not establish MQTT delivery.
|
||||
|
||||
The compact [safe audit](hardware_validation_heltec_v4_native_console_2026-10-06.safe.json)
|
||||
contains artifact/helper/report hashes and selected outcome fields. Completed
|
||||
sanitized reports and phase observations are retained privately; raw reference
|
||||
settings remain private. Failed attempts are retained separately from the
|
||||
qualified results.
|
||||
|
||||
The USB cable remained attached throughout these trials. The native-host sleep
|
||||
guard keeps CPU command service available while the USB host is connected,
|
||||
including when its serial port is closed. Power saving on is therefore not
|
||||
proof of CPU light-sleep entry, measured current, or physical button wake.
|
||||
At this source, V4 lacks the momentary-button wake profile flag. The later
|
||||
profile correction is outside this image and needs separate software/build
|
||||
and console validation. No physical button access is available in this setup,
|
||||
so physical button wake remains unqualified.
|
||||
|
||||
These checks qualify this historical `c88885a3` Full application, native Pi Chromium setup,
|
||||
protected active-slot application flash, and measured intervals. They do not
|
||||
qualify every host OS, board/role, adapter, cold power cycle, factory erase,
|
||||
heavy logging volume, measured power consumption, or RF traffic load. GPIO/reset
|
||||
behavior outside the unchanged stock flasher's reset sequence was not separately
|
||||
qualified. The release remains unpublished; the final same-source all-board
|
||||
build and release packaging are separate gates.
|
||||
@@ -0,0 +1,240 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"scope": "Heltec V4 normal Full native Chromium stock config GUI; active-slot app-only flash and quiet USB stability",
|
||||
"source_commit": "c88885a315208908e050c57fc0ec0a71ff10bd6f",
|
||||
"firmware_version": "v1.17.1.9-halo-keymind-cascade-dev-c88885a3 (Build: 06-Oct-2026)",
|
||||
"status": "qualified_historical_c888_USB_stability_operator_restored",
|
||||
"build": {
|
||||
"target": "heltec_v4_repeater",
|
||||
"profile": "full",
|
||||
"platformio_feature_base": "heltec_v4_repeater_observer_mqtt",
|
||||
"normal_update_identity_retained": true,
|
||||
"app_filename": "heltec_v4_repeater-full-usb-wifi-ota-v1.17.1.9-halo-keymind-cascade-dev-c88885a3.bin",
|
||||
"app_sha256": "1f10f44149b3f56edbe0ecb2a2bab9b51da651a6d047e2c2c1555691468d2b4c",
|
||||
"app_bytes": 2097528,
|
||||
"merged_filename": "heltec_v4_repeater-full-usb-wifi-ota-v1.17.1.9-halo-keymind-cascade-dev-c88885a3-merged.bin",
|
||||
"merged_sha256": "3264d7a1a5713755b5dd10ad1c81bb0588e4a072eeaa126b86307a42be77bb5b",
|
||||
"merged_bytes": 2163064,
|
||||
"elf_sha256": "0545eef46da30a86f86531199a91e887167d463c5981f193c0455cd75d633538",
|
||||
"partition_table_4096_sha256": "9af3af2b74e944337ba85f2b0027ee80df160579a1ab746ba0f95853f618cd60",
|
||||
"slot_bytes": 6553600,
|
||||
"usb_mode": 1,
|
||||
"cdc_on_boot": 1,
|
||||
"hwcdc_phy_handoff_compiled": true,
|
||||
"ram_gate_passed": true,
|
||||
"heap_margin_bytes": 20880,
|
||||
"capability_gate_passed": true,
|
||||
"verified_capabilities": [
|
||||
"bridge.espnow",
|
||||
"logging.usb.output",
|
||||
"ota.update.lora",
|
||||
"ota.update.wifi",
|
||||
"profile.full"
|
||||
],
|
||||
"independent_build_audit_sha256": "34d1bed7549f9457063370c1a7703a2c6cd7d75b9f2a8cb3308d000bf33c56f3"
|
||||
},
|
||||
"helpers": {
|
||||
"original_driver_sha256": "460b217824c897daefc377247562330290fb61e67facdf9ed3d9283160c5ca78",
|
||||
"phase_driver_sha256": "20797d40df1b0e2f30abd5779b3c1006173974cf71404b0d6e1932c721492d84",
|
||||
"phase_derivative_adds_no_hardware_operations": true
|
||||
},
|
||||
"host_preparation": {
|
||||
"tty_before": {
|
||||
"VMIN": 0,
|
||||
"VTIME": 0
|
||||
},
|
||||
"tty_after": {
|
||||
"VMIN": 1,
|
||||
"VTIME": 0
|
||||
},
|
||||
"explicit_modem_signal_calls": 0,
|
||||
"scope": "Host TTY VMIN/VTIME only; no firmware or browser JS changes"
|
||||
},
|
||||
"failed_attempts": [
|
||||
{
|
||||
"attempt": 2,
|
||||
"report_sha256": "60640f0bdd6b2f91d278bc3ffdc3a372ec799f8f057674437432ab091b7d302c",
|
||||
"qualified": false,
|
||||
"error_name": "TimeoutError",
|
||||
"duration_seconds": 303.30445313453674,
|
||||
"website_command_sent": false,
|
||||
"website_five_second_deadline_exercised": false,
|
||||
"application_write_not_reached_proven": false,
|
||||
"stock_usb_jtag_reset_entries": null,
|
||||
"initial_sync_failure_cause": "not established"
|
||||
},
|
||||
{
|
||||
"attempt": 4,
|
||||
"report_sha256": "9ab397494c011d1828d46aefbc2b927b36ad81160d92ec11a14dedb3f280f1ae",
|
||||
"qualified": false,
|
||||
"error_name": "TimeoutError",
|
||||
"duration_seconds": 277.81140637397766,
|
||||
"website_command_sent": false,
|
||||
"website_five_second_deadline_exercised": false,
|
||||
"application_write_not_reached_proven": true,
|
||||
"stock_usb_jtag_reset_entries": 674,
|
||||
"initial_sync_failure_cause": "not established"
|
||||
}
|
||||
],
|
||||
"external_flasher_library": {
|
||||
"sha256": "2a896d5e520ea9b6ea9900223c2460df797c3b2287f441daa4e50168a50b17e6",
|
||||
"default_connection_retry_count": 7,
|
||||
"stock_detectChip_supplies_rom_baudrate_as_retry_count": 115200,
|
||||
"scope": "Excessive failed-reset budget in stock flasher; not a MeshCore firmware fix or root-cause proof for initial sync failure."
|
||||
},
|
||||
"pending_physical_trials": [],
|
||||
"radio_scope": "USB stability qualification records CAD and receive-silence watchdog flags as warnings; requires physical RX errors zero and rejects queue-full/start-RX/unknown flags. No zero-error radio claim.",
|
||||
"limitations": [
|
||||
"Current-source normal Full HWCDC application; not released-stock migration proof.",
|
||||
"Host TTY VMIN/VTIME normalization is part of qualified host setup.",
|
||||
"Failed flasher preparations remain failed and did not exercise the GUI five-second command timer.",
|
||||
"Merged artifact is audited; actual browser tests flashed only the protected active application slot.",
|
||||
"No measured sleep current, factory erase/cold-cycle, heavy logging or RF traffic-soak claim.",
|
||||
"USB-attached CPU sleep guard means no physical light-sleep/button-wake or current-draw qualification; V4 momentary-button wake profile correction needs new-source qualification.",
|
||||
"Original host service states restored; MQTT packet delivery was not measured."
|
||||
],
|
||||
"qualified_trials": [
|
||||
{
|
||||
"attempt": 5,
|
||||
"logging_output": "off",
|
||||
"power_saving": "on",
|
||||
"report_sha256": "e4d49f17971d980565ad351b9b49a2e19c5ca8a83ac91805b87bf85ada49223e",
|
||||
"independent_review_sha256": "0e60290e3d63b393bf6c8647bb6c966f7a015bdd09067ab0cf7840290201c9ea",
|
||||
"first_plain_time_ms": 1614.4000000003725,
|
||||
"first_plain_time_clock_set_acknowledged": true,
|
||||
"native_command_deadline_ms": 5000,
|
||||
"quiet_intervals": [
|
||||
{
|
||||
"kind": "open",
|
||||
"requested_seconds": 600,
|
||||
"actual_seconds": 600.0930547369935,
|
||||
"native_writes_during_idle": 0,
|
||||
"uptime_before": 18,
|
||||
"serial_open": true,
|
||||
"uptime_after": 619,
|
||||
"uptime_delta": 601,
|
||||
"uptime_continuity_verified": true,
|
||||
"ok": true
|
||||
},
|
||||
{
|
||||
"kind": "closed",
|
||||
"requested_seconds": 600,
|
||||
"actual_seconds": 600.100213859987,
|
||||
"native_writes_during_idle": 0,
|
||||
"uptime_before": 627,
|
||||
"serial_open": false,
|
||||
"uptime_after": 1232,
|
||||
"uptime_delta": 605,
|
||||
"uptime_continuity_verified": true,
|
||||
"ok": true
|
||||
}
|
||||
],
|
||||
"first_post_open_idle_core_ms": 35.90000000037253,
|
||||
"post_open_idle_DOM_clock_reply_ms": 23.59999999962747,
|
||||
"post_closed_idle_DOM_clock_reply_ms": 33.19999999925494,
|
||||
"post_closed_idle_clock_set_acknowledged": false,
|
||||
"post_closed_idle_clock_response": "recognized framed clock-cannot-go-backwards rejection",
|
||||
"full_GUI_rereads_passed": true,
|
||||
"native_command_errors": 0,
|
||||
"GUI_modem_signal_calls": 0,
|
||||
"original_restoration_verified": true,
|
||||
"checked_preferences_count": 15,
|
||||
"identity_ACL_response_and_checked_preference_hashes_preserved": true,
|
||||
"original_power_saving": "off",
|
||||
"original_logging_output": "usb",
|
||||
"streams_closed": true,
|
||||
"radio": {
|
||||
"initial_core_error_flags": 0,
|
||||
"final_core_error_flags": 8,
|
||||
"all_observations_active_RX": true,
|
||||
"physical_rx_errors_max": 0,
|
||||
"final_recv": 0,
|
||||
"final_sent": 1,
|
||||
"scope": "Receive-silence watchdog8 warning after quiet; not a CPU reset or zero-error/traffic radio qualification."
|
||||
}
|
||||
},
|
||||
{
|
||||
"attempt": 3,
|
||||
"logging_output": "usb",
|
||||
"power_saving": "on",
|
||||
"report_sha256": "13202bf1d378128c2ac0d99404aac2a877b3ef3e9e14cfc0fc7a727d71aeb1ee",
|
||||
"independent_review_sha256": "0ab2d0c2d24e025fb381fedeebe5ee267b805c57f7ad6ec6a173a98201cacc34",
|
||||
"first_plain_time_ms": 696.0999999996275,
|
||||
"first_plain_time_clock_set_acknowledged": true,
|
||||
"native_command_deadline_ms": 5000,
|
||||
"quiet_intervals": [
|
||||
{
|
||||
"kind": "open",
|
||||
"requested_seconds": 600,
|
||||
"actual_seconds": 600.1005607169936,
|
||||
"native_writes_during_idle": 0,
|
||||
"uptime_before": 18,
|
||||
"serial_open": true,
|
||||
"uptime_after": 618,
|
||||
"uptime_delta": 600,
|
||||
"uptime_continuity_verified": true,
|
||||
"ok": true
|
||||
}
|
||||
],
|
||||
"first_post_open_idle_core_ms": 40.40000000037253,
|
||||
"post_open_idle_DOM_clock_reply_ms": 24.800000000745058,
|
||||
"post_open_idle_DOM_clock_set_acknowledged": true,
|
||||
"full_GUI_rereads_passed": true,
|
||||
"native_command_errors": 0,
|
||||
"GUI_modem_signal_calls": 0,
|
||||
"original_restoration_verified": true,
|
||||
"checked_preferences_count": 15,
|
||||
"identity_ACL_response_and_checked_preference_hashes_preserved": true,
|
||||
"original_power_saving": "off",
|
||||
"original_logging_output": "usb",
|
||||
"streams_closed": true,
|
||||
"radio": {
|
||||
"initial_core_error_flags": 0,
|
||||
"final_core_error_flags": 8,
|
||||
"all_observations_active_RX": true,
|
||||
"physical_rx_errors_max": 0,
|
||||
"final_recv": 0,
|
||||
"final_sent": 1,
|
||||
"scope": "Known CAD/receive-silence flags are warnings; no zero-error RF or traffic-soak claim."
|
||||
}
|
||||
}
|
||||
],
|
||||
"browser": {
|
||||
"version": "154.0.8037.92",
|
||||
"native_OS_Web_Serial": true,
|
||||
"stock_GUI_command_timeout_ms": 5000,
|
||||
"GUI_sources": {
|
||||
"index.html": "efd64d1b56d57988fc4108f3f84efc943ed4c42ade4e4dbf8139c46fb3c0476d",
|
||||
"src/gui.js": "919034806e903fbc41e29a367a7d854837f6520f47aa34e5d0618f5dd3d29800",
|
||||
"lib/serial-cli.js": "fbfbdcfa8bf1a372a0396d342e018ba8ee83d33b7b98c3a546fd37cd7e8ce870"
|
||||
}
|
||||
},
|
||||
"operator_cleanup": {
|
||||
"ok": true,
|
||||
"exact_runtime_version_verified": true,
|
||||
"checked_preferences_identity_ACL_response_preserved": true,
|
||||
"uptime_seconds": 686,
|
||||
"core_error_flags": 8,
|
||||
"serial_closed_and_no_owner": true,
|
||||
"owned_browser_profile_closed": true,
|
||||
"owned_tunnel_closed": "parent independently verified",
|
||||
"original_services_restored": {
|
||||
"ModemManager": "active",
|
||||
"mctomqtt": "active",
|
||||
"mesh-logger": "inactive"
|
||||
},
|
||||
"network_configuration_changed": false,
|
||||
"operator_report_sha256": "d6d6e03ad5058e1257a6653af70203a3852dace47701220d9fd5c5020c56ae33",
|
||||
"services_report_sha256": "f02ab0773cc35c11c2c9f400d8dffe0e7dd1807a09ebd942ee652ccc82c0f420",
|
||||
"browser_cleanup_report_sha256": "d22a76bffa71dac46ab9514f066369047642e8c0bdeda36c380e133b724ca299"
|
||||
},
|
||||
"sleep_scope": {
|
||||
"USB_cable_remained_attached": true,
|
||||
"native_host_sleep_guard_present": true,
|
||||
"CPU_light_sleep_entry_measured": false,
|
||||
"current_draw_measured": false,
|
||||
"physical_button_wake_tested": false,
|
||||
"c888_V4_momentary_button_wake_profile_flag_present": false,
|
||||
"new_button_fix_qualification": "pending"
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
"""Run ESP32Board's real sleep/USB methods against host peripheral stubs."""
|
||||
|
||||
from pathlib import Path
|
||||
import configparser
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
@@ -248,7 +250,305 @@ def board_method(signature: str) -> str:
|
||||
return source[start:end]
|
||||
|
||||
|
||||
def v4_profile_config():
|
||||
config = configparser.ConfigParser(
|
||||
interpolation=None, inline_comment_prefixes=("#", ";"))
|
||||
config.read([ROOT / "platformio.ini",
|
||||
ROOT / "variants/heltec_v4/platformio.ini",
|
||||
ROOT / "variants/heltec_v4_r8/platformio.ini"])
|
||||
return config
|
||||
|
||||
|
||||
def profile_option(config, section, option, seen=()):
|
||||
key = (section, option)
|
||||
if key in seen:
|
||||
raise ValueError("cyclic profile interpolation")
|
||||
if config.has_option(section, option):
|
||||
value = config.get(section, option)
|
||||
else:
|
||||
parents = config.get(section, "extends", fallback="").replace(",", " ").split()
|
||||
value = next(profile_option(config, parent, option, seen + (key,))
|
||||
for parent in parents if config.has_section(parent))
|
||||
return re.sub(r"\x24[{]([^}]+)[}]",
|
||||
lambda match: profile_option(
|
||||
config, *match[1].rsplit(".", 1), seen + (key,)),
|
||||
value)
|
||||
|
||||
|
||||
def button_defines(config, section):
|
||||
flags = profile_option(config, section, "build_flags")
|
||||
values = {}
|
||||
for match in re.finditer(
|
||||
r"(?:^|\s)-(D|U)\s*([A-Za-z_]\w*)(?:=([^\s]+))?", flags):
|
||||
operation, name, value = match.groups()
|
||||
if operation == "U":
|
||||
values.pop(name, None)
|
||||
else:
|
||||
values[name] = value if value is not None else "1"
|
||||
return values
|
||||
|
||||
|
||||
def conditional_block(source, start):
|
||||
"""Keep the production preprocessor branches, including nested guards."""
|
||||
depth = 0
|
||||
end = start
|
||||
for line in source[start:].splitlines(keepends=True):
|
||||
if re.match(r"\s*#if(?:def|ndef)?\b", line):
|
||||
depth += 1
|
||||
elif re.match(r"\s*#endif\b", line):
|
||||
depth -= 1
|
||||
end += len(line)
|
||||
if depth == 0:
|
||||
return source[start:end]
|
||||
raise ValueError("unterminated conditional block")
|
||||
|
||||
|
||||
V4_BUTTON_HARNESS = r'''
|
||||
#include <Arduino.h>
|
||||
#include <helpers/UsbHostSleepPolicy.h>
|
||||
#include <helpers/ui/MomentaryButton.h>
|
||||
#include <helpers/ui/DisplayPowerPolicy.h>
|
||||
#include <array>
|
||||
#include <stdexcept>
|
||||
#include <iostream>
|
||||
static void require(bool ok, const char* why) {
|
||||
if (!ok) throw std::runtime_error(why);
|
||||
}
|
||||
struct SerialPort {
|
||||
explicit operator bool() const { return false; }
|
||||
bool isPlugged() const { return false; }
|
||||
} Serial;
|
||||
namespace mesh {
|
||||
bool isUsbLoggingEnabled() { return false; }
|
||||
struct Console { void printf(const char*) {} };
|
||||
static Console console;
|
||||
Console& usbConsolePort() { return console; }
|
||||
}
|
||||
using gpio_num_t = int;
|
||||
constexpr int USER_BTN_PRESSED = LOW;
|
||||
constexpr int GPIO_INTR_HIGH_LEVEL = 5;
|
||||
constexpr int GPIO_INTR_LOW_LEVEL = 4;
|
||||
constexpr int GPIO_INTR_POSEDGE = 1;
|
||||
constexpr int GPIO_INTR_DISABLE = 0;
|
||||
static std::array<int, 64> wake_levels{}, restored_interrupts{};
|
||||
static unsigned sleep_calls = 0;
|
||||
static int critical_depth = 0;
|
||||
static bool press_during_sleep = false, radio_irq_high = false;
|
||||
static void esp_sleep_enable_timer_wakeup(uint64_t) {}
|
||||
static void esp_sleep_enable_gpio_wakeup() {}
|
||||
static int gpio_get_level(gpio_num_t pin) {
|
||||
return pin == PIN_USER_BTN ? digitalRead(pin) : (radio_irq_high ? HIGH : LOW);
|
||||
}
|
||||
static void gpio_wakeup_enable(gpio_num_t pin, int level) { wake_levels.at(pin) = level; }
|
||||
static void gpio_wakeup_disable(gpio_num_t pin) { wake_levels.at(pin) = 0; }
|
||||
static void gpio_set_intr_type(gpio_num_t pin, int type) { restored_interrupts.at(pin) = type; }
|
||||
static void esp_light_sleep_start() {
|
||||
++sleep_calls;
|
||||
if (press_during_sleep) {
|
||||
require(wake_levels[PIN_USER_BTN] == GPIO_INTR_LOW_LEVEL,
|
||||
"resolved V4 profile did not arm active-low button wake");
|
||||
require(wake_levels[P_LORA_DIO_1] == GPIO_INTR_HIGH_LEVEL,
|
||||
"button wake replaced the radio wake source");
|
||||
g_mock_pin_levels[PIN_USER_BTN] = LOW;
|
||||
press_during_sleep = false;
|
||||
}
|
||||
}
|
||||
#define portENTER_CRITICAL(mux) (++critical_depth)
|
||||
#define portEXIT_CRITICAL(mux) (--critical_depth)
|
||||
struct MainBoard {
|
||||
virtual void sleep(uint32_t) = 0;
|
||||
virtual bool isUsbDataConnected() = 0;
|
||||
virtual bool isUsbHostConnected() = 0;
|
||||
bool isRadioTestActive() const { return false; }
|
||||
};
|
||||
struct ESP32Board : MainBoard {
|
||||
bool inhibit_sleep = false;
|
||||
uint32_t getIRQGpio() { return P_LORA_DIO_1; }
|
||||
bool isExternalPowered() const { return false; }
|
||||
@METHODS@
|
||||
} board;
|
||||
@BUTTON_CONSTRUCTOR@
|
||||
struct TestDisplay {
|
||||
mesh::ui::DisplayPowerPrefs prefs;
|
||||
mesh::ui::DisplayPowerPolicy policy;
|
||||
bool servicePower(bool usb) {
|
||||
const bool before = policy.on();
|
||||
policy.update(prefs, usb, false, false, millis());
|
||||
return before != policy.on();
|
||||
}
|
||||
bool isOn() const { return policy.on(); }
|
||||
void wake(mesh::ui::DisplayWake reason) { policy.wake(reason, millis()); }
|
||||
} display;
|
||||
struct UITask {
|
||||
TestDisplay* _display = &display;
|
||||
uint32_t _next_refresh = 0, _powering_off_at = 0, _next_read = 0;
|
||||
int _prevBtnState = HIGH;
|
||||
void resetRadioProfileDisplayPage() {}
|
||||
void advanceRadioProfileDisplayPage() {}
|
||||
void loop() {
|
||||
@UI_BUTTON@
|
||||
}
|
||||
} ui;
|
||||
static bool roleCanPowerSave() {
|
||||
bool can_power_save = true;
|
||||
@ROLE_SLEEP_GUARD@
|
||||
return can_power_save;
|
||||
}
|
||||
int main() {
|
||||
try {
|
||||
static_assert(PIN_USER_BTN == 0 && P_LORA_DIO_1 == 14,
|
||||
"fixture must use the actual V4 GPIOs");
|
||||
resetArduinoMock();
|
||||
g_mock_millis = 1000;
|
||||
g_mock_pin_levels[PIN_USER_BTN] = HIGH;
|
||||
user_btn.begin();
|
||||
ui.loop();
|
||||
require(!display.isOn(), "button-mode screen did not start blank");
|
||||
require(roleCanPowerSave(), "idle button blocked initial power saving");
|
||||
// A press already pending must be serviced instead of entering sleep.
|
||||
g_mock_pin_levels[PIN_USER_BTN] = LOW;
|
||||
board.sleep(30);
|
||||
require(sleep_calls == 0 && critical_depth == 0,
|
||||
"V4 slept over an already pressed button");
|
||||
g_mock_pin_levels[PIN_USER_BTN] = HIGH;
|
||||
// Simulate the physical edge arriving while the CPU is sleeping.
|
||||
press_during_sleep = true;
|
||||
board.sleep(30);
|
||||
require(sleep_calls == 1 && digitalRead(PIN_USER_BTN) == LOW,
|
||||
"fresh button press failed to wake the V4");
|
||||
require(wake_levels[PIN_USER_BTN] == 0 && wake_levels[P_LORA_DIO_1] == 0
|
||||
&& restored_interrupts[PIN_USER_BTN] == GPIO_INTR_DISABLE
|
||||
&& restored_interrupts[P_LORA_DIO_1] == GPIO_INTR_POSEDGE
|
||||
&& critical_depth == 0, "button/radio wake cleanup regressed");
|
||||
ui.loop();
|
||||
require(user_btn.needsPolling() && !roleCanPowerSave(),
|
||||
"role slept before button debounce completed");
|
||||
g_mock_millis += 25;
|
||||
ui.loop();
|
||||
require(user_btn.needsPolling() && !roleCanPowerSave(),
|
||||
"held button stopped gesture polling");
|
||||
g_mock_millis += 50;
|
||||
g_mock_pin_levels[PIN_USER_BTN] = HIGH;
|
||||
ui.loop();
|
||||
require(user_btn.needsPolling() && !roleCanPowerSave(),
|
||||
"role slept before release debounce completed");
|
||||
g_mock_millis += 25;
|
||||
ui.loop();
|
||||
require(user_btn.needsPolling() && !roleCanPowerSave(),
|
||||
"role slept over the pending multi-click deadline");
|
||||
g_mock_millis += MOMENTARY_BUTTON_MULTI_CLICK_MS;
|
||||
ui.loop();
|
||||
require(display.isOn(), "real UITask click did not wake the screen");
|
||||
require(!user_btn.needsPolling() && roleCanPowerSave(),
|
||||
"completed gesture permanently disabled power saving");
|
||||
board.sleep(30);
|
||||
require(sleep_calls == 2, "released button failed to allow sleep again");
|
||||
g_mock_millis += 15000;
|
||||
ui.loop();
|
||||
require(!display.isOn(), "button screen failed to expire normally");
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n'; return 1;
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class Esp32UsbSleepTest(unittest.TestCase):
|
||||
def test_v4_profiles_inherit_button_wake_with_the_actual_gpio_pins(self):
|
||||
config = v4_profile_config()
|
||||
# These standalone partition-migration utilities deliberately bypass
|
||||
# the V4 board/display/radio bases and have no user-button support.
|
||||
utilities = {"env:heltec_v4_partition_migrator" + suffix for suffix in
|
||||
("", "_lora_repeater", "_test_hold", "_test_usb_diagnostic")}
|
||||
variants = configparser.ConfigParser(interpolation=None)
|
||||
variants.read([ROOT / "variants/heltec_v4/platformio.ini",
|
||||
ROOT / "variants/heltec_v4_r8/platformio.ini"])
|
||||
profiles = [section for section in variants.sections()
|
||||
if section.startswith("env:") and section not in utilities]
|
||||
self.assertGreater(len(profiles), 30)
|
||||
for section in profiles:
|
||||
with self.subTest(profile=section):
|
||||
defines = button_defines(config, section)
|
||||
self.assertEqual(defines["MOMENTARY_BUTTON_WAKE_FROM_SLEEP"], "1")
|
||||
self.assertEqual(defines["PIN_USER_BTN"], "0")
|
||||
self.assertEqual(defines["P_LORA_DIO_1"], "14")
|
||||
|
||||
def test_v4_sleep_button_poller_and_real_ui_wake_the_screen(self):
|
||||
config = v4_profile_config()
|
||||
methods = "\n".join(board_method(signature) for signature in (
|
||||
"void sleep(uint32_t secs) override",
|
||||
"bool isUsbDataConnected() override",
|
||||
"bool isUsbHostConnected() override"))
|
||||
compiler = os.environ.get("CXX", "c++")
|
||||
self.assertIsNotNone(shutil.which(compiler), "a C++17 compiler is required")
|
||||
with tempfile.TemporaryDirectory(prefix="meshcore-v4-button-wake-") as directory:
|
||||
for family in ("heltec_v4", "heltec_v4_r8"):
|
||||
target = (ROOT / f"variants/{family}/target.cpp").read_text()
|
||||
constructor = re.search(
|
||||
r"MomentaryButton user_btn\([\s\S]*?\);", target)[0]
|
||||
for role, suffix in (("simple_repeater", "repeater"),
|
||||
("simple_room_server", "room_server"),
|
||||
("simple_sensor", "sensor")):
|
||||
section = f"env:{family}_{suffix}"
|
||||
defines = button_defines(config, section)
|
||||
ui_source = (ROOT / f"examples/{role}/UITask.cpp").read_text()
|
||||
loop = ui_source.index("void UITask::loop()")
|
||||
opening = ui_source.index("{", loop) + 1
|
||||
button_start = ui_source.index("#if defined(PIN_USER_BTN)", opening)
|
||||
button_block = conditional_block(ui_source, button_start)
|
||||
ui = ui_source[opening:button_start] + button_block
|
||||
main = (ROOT / f"examples/{role}/main.cpp").read_text()
|
||||
guard_start = main.index("#if defined(MOMENTARY_BUTTON_WAKE_FROM_SLEEP)")
|
||||
guard = conditional_block(main, guard_start)
|
||||
harness = V4_BUTTON_HARNESS.replace("@METHODS@", methods)
|
||||
harness = harness.replace("@BUTTON_CONSTRUCTOR@", constructor)
|
||||
harness = harness.replace("@UI_BUTTON@", ui)
|
||||
harness = harness.replace("@ROLE_SLEEP_GUARD@", guard)
|
||||
cases = [("profile", harness, defines, None)]
|
||||
if family == "heltec_v4" and role == "simple_repeater":
|
||||
# Removing the actual inherited flag reproduces the
|
||||
# missed GPIO wake using the same production methods.
|
||||
missing = dict(defines, MOMENTARY_BUTTON_WAKE_FROM_SLEEP="0")
|
||||
cases.append(("missing-flag", harness, missing,
|
||||
"V4 slept over an already pressed button"))
|
||||
cases.append(("missing-poller", harness.replace(
|
||||
"can_power_save = can_power_save && !user_btn.needsPolling();",
|
||||
""), defines, "role slept before button debounce completed"))
|
||||
cases.append(("missing-display-wake", harness.replace(
|
||||
"_display->wake(mesh::ui::DisplayWake::Button);",
|
||||
"(void)0;"), defines,
|
||||
"real UITask click did not wake the screen"))
|
||||
for name, text, selected, expected_error in cases:
|
||||
with self.subTest(profile=section, control=name):
|
||||
stem = Path(directory) / f"{family}-{suffix}-{name}"
|
||||
cpp = stem.with_suffix(".cpp")
|
||||
cpp.write_text(text, encoding="ascii")
|
||||
flags = [f"-D{key}={selected[key]}" for key in (
|
||||
"PIN_USER_BTN", "P_LORA_DIO_1",
|
||||
"MOMENTARY_BUTTON_WAKE_FROM_SLEEP", "DISPLAY_CLASS")]
|
||||
result = subprocess.run([
|
||||
compiler, "-std=c++17", "-Wall", "-Wextra",
|
||||
"-DARDUINO_USB_CDC_ON_BOOT=1", "-DARDUINO_USB_MODE=1",
|
||||
"-DMESH_ESP32_USB_CONSOLE_COOPERATIVE=0",
|
||||
"-DMESH_USB_LOGGING_AVAILABLE=0",
|
||||
"-DUSER_BTN_LONG_PRESS_MILLIS=" +
|
||||
selected.get("USER_BTN_LONG_PRESS_MILLIS", "1000"),
|
||||
"-DUSER_BTN_MULTICLICK=" +
|
||||
selected.get("USER_BTN_MULTICLICK", "1"),
|
||||
"-DPOWEROFF_DELAY=2000",
|
||||
*flags, "-I", str(ROOT / "test/mocks"),
|
||||
"-I", str(ROOT / "src"), str(cpp),
|
||||
str(ROOT / "src/helpers/ui/MomentaryButton.cpp"),
|
||||
"-o", str(stem)], capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
result = subprocess.run(
|
||||
[str(stem)], capture_output=True, text=True, timeout=10)
|
||||
if expected_error is None:
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
else:
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn(expected_error, result.stderr)
|
||||
|
||||
def test_raw_uart_driver_preserves_fresh_console_indefinitely_until_end(self):
|
||||
methods = "\n".join(board_method(signature) for signature in (
|
||||
"void sleep(uint32_t secs) override", "bool isUsbDataConnected() override",
|
||||
|
||||
@@ -25,6 +25,7 @@ build_flags =
|
||||
-D P_LORA_KCT8103L_PA_CSD=2
|
||||
-D P_LORA_KCT8103L_PA_CTX=5
|
||||
-D PIN_USER_BTN=0
|
||||
-D MOMENTARY_BUTTON_WAKE_FROM_SLEEP=1
|
||||
-D PIN_VEXT_EN=36
|
||||
-D PIN_VEXT_EN_ACTIVE=HIGH
|
||||
-D LORA_TX_POWER=22 ; Default to maximum SX1262 drive
|
||||
|
||||
@@ -22,6 +22,7 @@ build_flags =
|
||||
-D P_LORA_KCT8103L_PA_CTX=5 ; PA_CTX (LOW=RX LNA, HIGH=RX bypass)
|
||||
-D P_LORA_TX_LED=46
|
||||
-D PIN_USER_BTN=0
|
||||
-D MOMENTARY_BUTTON_WAKE_FROM_SLEEP=1
|
||||
-D PIN_VEXT_EN=40
|
||||
-D PIN_VEXT_EN_ACTIVE=LOW
|
||||
-D ADC_MULTIPLIER=5.0715f
|
||||
|
||||
Reference in new issue
Block a user