You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Adds the system component and a MonitorService to the monitor component, plus a hosted system console web app, so any espp device can report what it is / how it is doing and be rebooted (or dropped into the ROM bootloader) over the framed USB / WebUSB / Web Serial stream.
system (new)
espp::SystemInfo — static getters and a one-call collect() / to_string() snapshot: chip model / revision / cores / features, ESP-IDF version, application description (project name, version, build date + time, ELF SHA-256), running + boot partition with OTA image state, reset reason, uptime, base MAC, flash / PSRAM size, CPU MHz, free / min-free heap.
espp::SystemControl — reboot() and reboot_to_bootloader(): sets the chip's force download boot flag and restarts, so the device comes back in the ROM download mode ready for esptool / idf.py flash. Per family (mirroring ESP-IDF's ROM USB console): RTC_CNTL_OPTION1 on S2 / S3 / C3 / C2 (S2 / S3 also keep the ROM USB device persistent across the reset), LP_AON_SYS_CFG on C6 / H2 / C5 / C61 / H21, LP_SYSTEM_REG_SYS_CTRL on P4; the classic ESP32 has no software path and reports operation_not_supported (bootloader_reboot_supported() is constexpr false there). *_after(delay) variants restart from a detached thread.
espp::SystemService — dispatcher module espp.system v1, module 7: GET_INFO answers with tagged records ([tag u8][len u8][value], hosts skip unknown tags so fields can be added without a version bump); REBOOT / REBOOT_TO_BOOTLOADER ([delay_ms u16]) reply OK first and restart after max(delay, Config::min_restart_delay) from a detached thread (the OtaService pattern). Guards: Config::allow_reboot / allow_bootloader (ERROR "disabled"), an optional on_reboot_request(RebootKind)veto callback run outside the lock (ERROR "refused by the application"), and "not supported" for the bootloader restart on chips without a path. The INFO capabilities record tells a host up front which of the two it may offer.
Host-buildable codec detail/system_protocol.hpp + test/system_host_test.cpp (golden bytes, every tag, unknown-tag skip, truncated-record rejection); README, doc/en/system/*, Doxygen wiring, index toctree.
Example (components/system/example, esp32s3, vendor + CDC via DispatcherWorker, in the CI matrix manager-off) registering both services with a logging / permitting on_reboot_request.
monitor: MonitorService
Dispatcher module espp.monitor v1, module 8 over the existing HeapMonitor / TaskMonitor: GET_HEAP (one record per configured MALLOC_CAP_* region; regions with no memory are left out), GET_TASKS (name, CPU %, stack high-water mark, priority, core; capped at the frame payload with the overflow logged; empty + a rate-limited log without the FreeRTOS stats Kconfig), SET_STREAM ([enable u8][period_ms u16][what u8]) driving an espp::Task that pushes HEAP / TASKS events periodically (period clamped to Config::min_stream_period). Every outbound frame is serialized on a send mutex held across send, so streamed events and replies never interleave.
Web console components/system/web/system_console.html
WebUSB (vendor) + Web Serial (CDC, doubling as a serial monitor). Device-info panel (all tags, unknown ones ignored), Reboot / Reboot into bootloader with an in-page confirmation step (the bootloader button is disabled unless the device reports the capability), heap-region gauges with a peak marker, a live sortable task table with a stream toggle / period / selection, and a log pane. Both module ids are resolved from one discovery reply with the shared helper block (byte-identical, added to the console lint in resolve_module_id_test.js); the monitor section stays hidden when the device does not advertise espp.monitor. Listed in web_apps.rst; module tables in the dispatcher README / docs gained rows 7 and 8.
Test plan
components/system/test/system_host_test.cpp and components/monitor/test/monitor_host_test.cpp pass (host build).
node components/dispatcher/web/test/resolve_module_id_test.js passes with the new console in its list (identical helper block, resolved-id-only lint, moduleReady gate, transport-bound probe).
components/system/example builds for esp32s3 (manager off); components/monitor/example still builds for esp32.
cppcheck with the CI arguments is clean on the new sources; the console parse-checks and headless-loads with no JS errors.
On hardware (S3): connect the console over WebUSB and Web Serial, read the info, stream heap + tasks, reboot, reboot into the bootloader and flash with esptool; verify the classic-ESP32 path reports unsupported and a P4 enters download mode.
…nitor service with a browser console
New `system` component:
- SystemInfo: static getters (and a collect() snapshot + to_string()) for the
chip, ESP-IDF version, app description (project, version, build date/time,
ELF SHA-256), running/boot partitions + OTA state, reset reason, uptime,
MAC, flash/PSRAM size, CPU MHz and heap.
- SystemControl: reboot(), and reboot_to_bootloader() which sets the chip's
force-download-boot flag (RTC_CNTL on S2/S3/C3/C2, LP_AON on C6/H2/C5/
C61/H21, LP_SYSTEM on P4; classic ESP32 reports not supported) and
restarts, so the device comes back in the ROM download mode; the S2/S3
ROM USB device is kept persistent across the reset.
- SystemService: dispatcher module (`espp.system` v1, module 7): GET_INFO
as tagged records hosts decode while skipping unknown tags, REBOOT and
REBOOT_TO_BOOTLOADER (OK first, restart after a clamped delay) guarded by
allow_reboot / allow_bootloader and an on_reboot_request veto callback.
- Host-buildable codec (detail/system_protocol.hpp) with golden tests,
docs, README, Doxygen wiring, and an esp32s3 example (vendor + CDC) in
the CI matrix.
`monitor` component:
- MonitorService: dispatcher module (`espp.monitor` v1, module 8) serving
HeapMonitor regions (GET_HEAP), the TaskMonitor table (GET_TASKS, capped
at the frame payload) and a SET_STREAM periodic push of either; codec in
detail/monitor_protocol.hpp with host tests. monitor now REQUIRES
stream_frame + dispatcher.
Web: components/system/web/system_console.html (WebUSB / Web Serial):
device info, reboot / reboot-into-bootloader with an in-page confirmation
(bootloader disabled when the device reports no capability), heap gauges
and a live sortable task table with a stream toggle; resolves both module
ids from discovery (monitor optional) with the shared helper block, added
to the console lint. Module tables and web_apps docs updated.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
…ressing cppcheck
CI's cppcheck evaluates the configuration where the FreeRTOS stats are
enabled, so the two inline suppressions were unmatched (and fatal). The
conversion now lives under the stats Kconfig and the other configuration
returns an empty list directly: no suppressions in either.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
…r-transport send mutex, frame cap, console state)
- Console: ?module=N is applied to whichever ident module N advertises the
protocol of (the monitor's for espp.monitor, else the system's), so the
Device Hub's Monitor link works; the monitor panel counts as present only
on a protocol match (both services share the app); capabilities, info,
heap and task state are reset on every new connection; heap-region labels
use the real MALLOC_CAP_* bits; the task-table sort controls are buttons
with aria-sort on the active column.
- Example: one send mutex per transport that both services' `send` go
through, so a streamed monitor event never interleaves a system reply.
- MonitorService::Config::max_frame_bytes (4096): the TASKS payload is
capped so header + payload + CRC fits one write; test + docs.
- system_protocol.hpp: tag 13 documents both sentinels (0xFE undefined,
0xFF unavailable / not an OTA partition).
- CI / docs housekeeping: system in upload_components.yml, build matrix
entry and Doxyfile entries in alphabetical order after sx126x,
system_example.md includes the example README.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
The reason will be displayed to describe this comment to others. Learn more.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
…des, strict delay payload, dialog focus)
- MonitorService: HEAP records carry the MALLOC_CAP_* mask the region was
queried with, not the heap's own flags.
- Both services: ERROR codes are the POSIX errno of the chosen std::errc
(std::make_error_code(errc).value()); documented in both protocol headers
and the README.
- system_protocol: decode_delay() accepts an empty payload or exactly
[delay_ms u16]; any other size is malformed (test added).
- Console: the confirm dialog is aria-modal, Escape cancels, Tab cycles its
two buttons, and focus returns to the button that opened it on close.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
The stream wait uses the legacy two-argument Task callback and waits without a predicate. A condition variable may wake spuriously, causing HEAP/TASKS events to be emitted earlier (and potentially much more frequently) than the configured minimum period. Use the notified-aware callback and clear the flag while holding its mutex, as required by Task's current callback contract.
Reject payloads with incomplete trailing record headers
components/system/web/system_console.html:1104
A payload ending with a lone tag byte is silently accepted because the loop only runs while two bytes remain. This disagrees with the C++ decoder and its truncation test ({1} is rejected), so the browser can render a malformed partial INFO response as valid. Iterate while any data remains and explicitly reject an incomplete record header.
…ff by default)
The S2 / S3 ROM only expects a persisted USB peripheral from an application
whose USB device is ROM-CDC/DFU-compatible (ESP-IDF's ROM USB console);
persisting a TinyUSB vendor + CDC composite can leave the host with a stale
enumeration the bootloader cannot serve. reboot_to_bootloader() therefore
no longer sets USBDC_PERSIST_ENA unconditionally: SystemBootloaderOptions
(SystemControl::BootloaderOptions) / SystemService::Config::usb_persist opt
in, default false, and when enabled the ROM prep runs the way usb_console.c
does (usb_dc_prepare_persist() then chip_usb_set_persist_flags()), S2 / S3
only. Header, README and system.rst document when persistence applies.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
max_frame_bytes is only applied to TASKS; GET_HEAP and streamed HEAP frames ignore it. Even the default three regions produce an 86-byte frame, and a large heap_regions list can exceed the codec limit and make build_frame() return empty, silently dropping the reply. Either enforce this transport limit for HEAP too, or scope/rename the option so callers are not promised a maximum frame size it does not provide.
This selects Task's legacy two-argument callback and waits without a predicate, so a spurious condition-variable wake starts the next iteration immediately and can emit events faster than min_stream_period. Use the notified callback overload, predicate-wait on that flag, and clear it under the mutex as required by the Task contract.
Close port when writer acquisition fails
components/system/web/system_console.html:691
If acquiring the writer fails after port.open() succeeds, the exception reaches the click handler but there is no transport object to close, so the selected port remains open and subsequent connections can fail. Clean up the partially opened port before rethrowing.
Close writable stream before releasing its lock
components/system/web/system_console.html:703
Close the writable stream before releasing its lock. releaseLock() can throw while a write is pending; because that error is swallowed, port.close() then also fails on the still-locked stream and the UI reports a disconnect while leaving the serial port open.
Prevent stale replies from satisfying requests after timeout
components/system/web/system_console.html:920
A timed-out transaction clears its pending slot and lets the chain send another uncorrelated request. A delayed reply from the timed-out request can then satisfy the later request (ERROR/OK matching does not even check the echoed request type), leaving the UI and device state out of sync. After a timeout, require a reconnect before sending more requests, or add correlation IDs that both services echo and match.
Keep example on the supported C++20 standard
components/system/example/CMakeLists.txt:5
This example only uses C++20 features, but opts the entire build into C++23. The repository standard and the corresponding task/monitor examples use C++20 (components/task/example/CMakeLists.txt:22, components/monitor/example/CMakeLists.txt:22); keep this example on the supported baseline rather than requiring a newer language mode unnecessarily.
…HEAP, stream wait predicate, correlation ids, serial teardown)
- MonitorService: one max_payload() (max_frame_bytes less the largest header
+ CRC) caps HEAP as well as TASKS; encode_heap() takes the cap and reports
what fit, the overflow is logged rate-limited. Docs + host test.
- MonitorService: the stream task uses the (mutex, cv, notified) callback,
waits on the notified predicate and clears it under the mutex, so a
spurious wake-up no longer emits events early.
- Both services echo the request frame's correlation id on every reply
(INFO / HEAP / TASKS / OK / ERROR); streamed events carry none. The
console stamps each request with a fresh u16 id and pairs replies by it:
a mismatching correlated reply is dropped as stale, an uncorrelated reply
is accepted only until the device has shown it echoes ids (older
firmware). Protocol headers, READMEs and rst pages say so; host tests
cover the echo through the codec.
- Console Web Serial: a failing getWriter() closes the port it just opened
before rethrowing; close() finishes / aborts the writable stream before
releasing its lock so the port cannot stay locked and open.
- Console INFO decoder rejects a payload ending in a lone tag byte
(incomplete record header), like the C++ decoder.
- System example builds with C++20 like the other examples.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU
Addressed the "previously missed" findings from the last two review summaries in 47cd985 (they were only in the summaries, not posted as threads):
monitor: max_frame_bytes now caps HEAP replies too (one max_payload() for both builders, regions dropped from the end with a rate-limited log); the stream task uses the notified-aware Task callback with a predicate wait and clears the flag under the mutex.
both services echo the request's correlation id on every reply (INFO / HEAP / TASKS / OK / ERROR; streamed events carry none) and the console stamps a fresh id per request and only accepts a matching reply (uncorrelated replies accepted only until the device has echoed one, for older firmware), so a late reply after a timeout can no longer satisfy a later request.
console: the port is closed if acquiring the writer fails after open; the writable stream is closed before its lock is released on disconnect; the INFO decoder rejects an incomplete trailing record header / an overrunning length like the C++ decoder.
The reason will be displayed to describe this comment to others. Learn more.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the
systemcomponent and aMonitorServiceto themonitorcomponent, plus a hosted system console web app, so any espp device can report what it is / how it is doing and be rebooted (or dropped into the ROM bootloader) over the framed USB / WebUSB / Web Serial stream.system(new)espp::SystemInfo— static getters and a one-callcollect()/to_string()snapshot: chip model / revision / cores / features, ESP-IDF version, application description (project name, version, build date + time, ELF SHA-256), running + boot partition with OTA image state, reset reason, uptime, base MAC, flash / PSRAM size, CPU MHz, free / min-free heap.espp::SystemControl—reboot()andreboot_to_bootloader(): sets the chip's force download boot flag and restarts, so the device comes back in the ROM download mode ready foresptool/idf.py flash. Per family (mirroring ESP-IDF's ROM USB console):RTC_CNTL_OPTION1on S2 / S3 / C3 / C2 (S2 / S3 also keep the ROM USB device persistent across the reset),LP_AON_SYS_CFGon C6 / H2 / C5 / C61 / H21,LP_SYSTEM_REG_SYS_CTRLon P4; the classic ESP32 has no software path and reportsoperation_not_supported(bootloader_reboot_supported()isconstexpr falsethere).*_after(delay)variants restart from a detached thread.espp::SystemService— dispatcher moduleespp.systemv1, module 7:GET_INFOanswers with tagged records ([tag u8][len u8][value], hosts skip unknown tags so fields can be added without a version bump);REBOOT/REBOOT_TO_BOOTLOADER([delay_ms u16]) replyOKfirst and restart aftermax(delay, Config::min_restart_delay)from a detached thread (the OtaService pattern). Guards:Config::allow_reboot/allow_bootloader(ERROR "disabled"), an optionalon_reboot_request(RebootKind)veto callback run outside the lock (ERROR "refused by the application"), and "not supported" for the bootloader restart on chips without a path. The INFO capabilities record tells a host up front which of the two it may offer.detail/system_protocol.hpp+test/system_host_test.cpp(golden bytes, every tag, unknown-tag skip, truncated-record rejection); README,doc/en/system/*, Doxygen wiring, index toctree.components/system/example, esp32s3, vendor + CDC viaDispatcherWorker, in the CI matrix manager-off) registering both services with a logging / permittingon_reboot_request.monitor:MonitorServiceespp.monitorv1, module 8 over the existingHeapMonitor/TaskMonitor:GET_HEAP(one record per configuredMALLOC_CAP_*region; regions with no memory are left out),GET_TASKS(name, CPU %, stack high-water mark, priority, core; capped at the frame payload with the overflow logged; empty + a rate-limited log without the FreeRTOS stats Kconfig),SET_STREAM([enable u8][period_ms u16][what u8]) driving anespp::Taskthat pushes HEAP / TASKS events periodically (period clamped toConfig::min_stream_period). Every outbound frame is serialized on a send mutex held acrosssend, so streamed events and replies never interleave.detail/monitor_protocol.hpp(host-buildable) +test/monitor_host_test.cpp;monitornow REQUIRESstream_frame+dispatcher(manifest updated); README +doc/en/core/monitor.rstsection.Web console
components/system/web/system_console.htmlWebUSB (vendor) + Web Serial (CDC, doubling as a serial monitor). Device-info panel (all tags, unknown ones ignored), Reboot / Reboot into bootloader with an in-page confirmation step (the bootloader button is disabled unless the device reports the capability), heap-region gauges with a peak marker, a live sortable task table with a stream toggle / period / selection, and a log pane. Both module ids are resolved from one discovery reply with the shared helper block (byte-identical, added to the console lint in
resolve_module_id_test.js); the monitor section stays hidden when the device does not advertiseespp.monitor. Listed inweb_apps.rst; module tables in the dispatcher README / docs gained rows 7 and 8.Test plan
components/system/test/system_host_test.cppandcomponents/monitor/test/monitor_host_test.cpppass (host build).node components/dispatcher/web/test/resolve_module_id_test.jspasses with the new console in its list (identical helper block, resolved-id-only lint,moduleReadygate, transport-bound probe).components/system/examplebuilds for esp32s3 (manager off);components/monitor/examplestill builds for esp32.esptool; verify the classic-ESP32 path reports unsupported and a P4 enters download mode.Webapp example:

Reading out task info:

🤖 Generated with Claude Code
https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU