Desktop, Desktop Service & Console Capture
The Desktop class is the retained model of a windowed desktop: apps (name, icon, description and a launch callback), windows (title, flags, geometry) each holding a tree of widgets (containers — Column, Row, Group — and leaves — Label, Button, Checkbox, TextBox, TextArea, List, Table, Select, Progress, Slider, Separator, Spacer), plus modal dialogs (message and input boxes) and notifications. The firmware never draws anything: a browser connected through the DesktopService renders the desktop, lets the user operate it, and reports every interaction back as an event.
An app is a few lines of C++: register it, and build its window in the launch
callback with the Window / Widget value handles (win.label(...),
win.button("+1", [=]{ ... }), label.set_text("Count: {}", n)). Every
mutator may be called from any task; it records the change and wakes the
desktop task, which coalesces everything changed since the last flush (last
value wins, text appends concatenate, a removed widget cancels its pending
changes) into the fewest frames every Config::flush_period (50 ms by
default). Every application callback — launch, widget / window / dialog
handlers, timers, post() — runs on the desktop task with no lock held, so a
handler may call anything, including closing its own window. Handles are
plain values that become invalid (and no-ops) once their target is gone.
Layout is a box model: nested Column / Row / Group containers become CSS
flexbox in the browser; a widget’s weight is its flex-grow along the
parent’s axis and its layout bits set the cross-axis behaviour (stretch,
scroll, align end / center); width / height give a preferred size.
Windows carry flags (movable, resizable, closable, modal, minimizable,
maximizable, centered, pinned, wants-geometry) and a geometry the browser may
override with what the user last chose (remembered per app and title).
The DesktopService class serves one Desktop over any byte stream as
a dispatcher module (espp.desktop v2,
module id 9 by default; one instance per transport). It only decodes and
validates: a malformed request is answered with ERROR, everything else is
handed to the desktop and handled — replied to, and its events broadcast — on
the desktop task. GET_DESKTOP returns the app list and settings followed
by the full tree of every open window and dialog (so a reconnecting browser
resyncs in one request) and marks the transport active; LAUNCH_APP /
CLOSE_WINDOW are acknowledged; WINDOW_EVENT / WIDGET_EVENT /
DIALOG_RESULT are not. Widget payloads are split across frames (a long
text becomes Text + TextAppend pieces, a long list several ranges, a big
window tree WINDOW_OPEN + WIDGET_ADD continuations), never truncated; dialogs,
notifications and the app registry are single frames whose limits the API
enforces (an oversized dialog / toast / title / placeholder / tooltip /
column set is refused and logged, the registry is bounded in app count and
string lengths). The transport’s send reports
whether a frame was queued; when one is refused (the host went away or
stopped reading) the desktop pauses streaming to that transport
(needs_resync(), it stays attached) and re-sends the full snapshot by
itself once frames go through again, retrying every second with back-off
to eight seconds, or at the host’s next GET_DESKTOP; the wire format
is documented in include/detail/desktop_protocol.hpp and checked by a
host test against the fixture test/desktop_vectors.txt, which the web
app’s test reads too.
The ConsoleCapture class keeps the last N bytes of everything the firmware
prints (ESP_LOG, espp::Logger, printf, stderr) in a byte ring a
reader can page through with a cursor, while still writing them to the
original console: a tiny write-only VFS device is registered and stdout /
stderr are re-opened on it. It is what the example’s Log Viewer app streams.
It is mutually exclusive with UsbDevice::route_console_to_cdc() (both
re-point stdout).
The hosted espp Desktop
web app (web/desktop.html) speaks the protocol over WebUSB or Web
Serial: a desktop with app icons and a start menu, draggable / resizable
windows with a taskbar, modal dialogs and toasts, and a frame log for
debugging.
API Reference
Header File
Classes
-
class Desktop : public espp::BaseComponent
The retained desktop: apps, windows, widgets, dialogs and notifications, flushed to the browser by its own task. One per device; attach one espp::DesktopService per transport.
Counter app (the API in 30 lines)
inline void register_counter_app(espp::Desktop &desktop) { desktop.register_app({ .name = "Counter", .icon = "\xF0\x9F\xA7\xAE", // abacus .description = "Counts clicks (kept in NVS)", // runs on the desktop task when the user launches the app .launch = [](espp::Desktop &d, espp::Desktop::AppId app) { std::error_code ec; auto nvs = std::make_shared<espp::NvsHandle>("desktop", ec); auto count = std::make_shared<int32_t>(0); nvs->get("count", *count, int32_t{0}, ec); auto win = d.create_window({.title = "Counter", .app = app, .w = 240, .h = 150}); auto label = win.label(fmt::format("Count: {}", *count), 0, espp::Desktop::kLabelBold); auto row = win.row(); // a horizontal container for the buttons auto bump = [=](int32_t delta) mutable { *count += delta; // set() only stages: commit() writes it to flash nvs->set("count", *count, ec); if (!ec) nvs->commit(ec); label.set_text("Count: {}{}", *count, ec ? " (not saved)" : ""); }; win.button( "-1", [=]() mutable { bump(-1); }, row.id()); win.button( "+1", [=]() mutable { bump(+1); }, row.id(), espp::Desktop::kButtonPrimary); win.button( "Reset", [=, &d]() mutable { // a message box owned by the window; the answer arrives later d.message_box({.owner = win.id(), .title = "Reset counter?", .text = "The count is kept in NVS.", .buttons = {"Reset", "Cancel"}, .icon = espp::Desktop::DialogIcon::Question, .on_result = [=](int button) mutable { if (button == 0) bump(-*count); }}); }, row.id(), espp::Desktop::kButtonDanger); }, }); }
Wiring
// The desktop: one per device. Apps register with it; every app callback // runs on its task. const auto sysinfo = espp::SystemInfo::collect(); espp::Desktop desktop( {.device_name = "espp Desktop", .firmware = fmt::format("{} {}", sysinfo.project_name, sysinfo.app_version), .task_config = {.name = "desktop", .stack_size_bytes = 10 * 1024}, .log_level = espp::Logger::Verbosity::INFO}); register_counter_app(desktop); register_about_app(desktop); register_system_monitor_app(desktop); register_task_manager_app(desktop); register_log_viewer_app(desktop); register_files_app(desktop); #if DESKTOP_EXAMPLE_CANOPEN_APP register_canopen_app(desktop); #elif CONFIG_DESKTOP_EXAMPLE_ENABLE_CANOPEN logger.warn("CONFIG_DESKTOP_EXAMPLE_ENABLE_CANOPEN is set but the CANopen app was not built: " "it needs IDF >= 6.0 (twai) and the CMake option DESKTOP_EXAMPLE_CANOPEN=ON"); #endif #if CONFIG_DESKTOP_EXAMPLE_ENABLE_I2C register_i2c_scanner_app(desktop); #endif #if CONFIG_DESKTOP_EXAMPLE_ENABLE_WIFI || CONFIG_DESKTOP_EXAMPLE_ENABLE_ETHERNET register_network_app(desktop); #endif register_settings_app(desktop, "espp Desktop"); desktop_example::apply_saved_settings(desktop); // USB composite device: a vendor/WebUSB function and a CDC function, both // carrying the same framed protocol. espp::UsbDevice::Config usb_cfg; usb_cfg.pid = 0x0d38; // distinct from the espp default so the webapp filter is specific usb_cfg.manufacturer = "espp"; usb_cfg.product = "espp Desktop"; usb_cfg.log_level = espp::Logger::Verbosity::WARN; espp::UsbDevice::CdcFunction cdc; cdc.interface_name = "espp Desktop (CDC)"; usb_cfg.cdc = cdc; espp::UsbDevice::VendorFunction vendor; vendor.interface_name = "espp Desktop (WebUSB)"; vendor.webusb = true; // advertise BOS / WebUSB / MS OS 2.0 descriptors vendor.landing_page_url = "esp-cpp.github.io/espp/apps/desktop.html"; usb_cfg.vendor = vendor; espp::UsbDevice usb(usb_cfg); // One send function per transport, serialized by one mutex each, so the // services' frames never interleave. write_vendor / write_cdc are // all-or-nothing: they wait (bounded, 250 ms) for FIFO room for the WHOLE // frame and never queue a partial one, and return false when the host did // not drain in time (unplugged, or the page is not reading) -- the desktop // then flags that transport as needing a resync, and the browser resyncs // with GET_DESKTOP when it reconnects. std::mutex vendor_tx_mutex, cdc_tx_mutex; auto vendor_send = [&](std::span<const uint8_t> frame) { std::lock_guard<std::mutex> lock(vendor_tx_mutex); return usb.write_vendor(frame); }; auto cdc_send = [&](std::span<const uint8_t> frame) { std::lock_guard<std::mutex> lock(cdc_tx_mutex); return usb.write_cdc(frame); }; // One service instance per transport (they are cheap; each replies on its // own stream). Declared BEFORE the workers that call into them. espp::DesktopService vendor_desktop( desktop, {.send = vendor_send, .log_level = espp::Logger::Verbosity::INFO}); espp::DesktopService cdc_desktop(desktop, {.send = cdc_send, .log_level = espp::Logger::Verbosity::INFO}); espp::SystemService vendor_system({.send = vendor_send}); espp::SystemService cdc_system({.send = cdc_send}); espp::MonitorService vendor_monitor( {.send = vendor_send, .task_config = {.name = "monitor_v", .stack_size_bytes = 6 * 1024}}); espp::MonitorService cdc_monitor( {.send = cdc_send, .task_config = {.name = "monitor_c", .stack_size_bytes = 6 * 1024}}); espp::OtaService vendor_ota(ota, {.send = vendor_send}); espp::OtaService cdc_ota(ota, {.send = cdc_send}); espp::CoreDumpService vendor_coredump(core_dump, {.send = vendor_send}); espp::CoreDumpService cdc_coredump(core_dump, {.send = cdc_send}); // One DispatcherWorker per byte stream: a bounded receive queue + worker // task feeding its Dispatcher, so the services never run on the TinyUSB // task. Registering a service routes its module to it and advertises it for // discovery; serve_discovery() answers the hub's ListModules query. espp::DispatcherWorker vendor_link( {.send = vendor_send, .on_overflow = [&]() { vendor_ota.on_rx_overflow(); }, .task_config = {.name = "desktop_rx_vendor", .stack_size_bytes = 8192}}); espp::DispatcherWorker cdc_link( {.send = cdc_send, .on_overflow = [&]() { cdc_ota.on_rx_overflow(); }, .task_config = {.name = "desktop_rx_cdc", .stack_size_bytes = 8192}}); for (auto *link : {&vendor_link, &cdc_link}) { const bool v = link == &vendor_link; link->register_module(v ? vendor_desktop : cdc_desktop); link->register_module(v ? vendor_system : cdc_system); link->register_module(v ? vendor_monitor : cdc_monitor); link->register_module(v ? vendor_ota : cdc_ota); link->register_module(v ? vendor_coredump : cdc_coredump); link->serve_discovery(usb_cfg.product); }
Public Types
-
using WidgetEvent = detail::dp::WidgetEvent
A widget event from the host: `kind` says which of value / text / key apply (Text events arrive reassembled, `text` holds the whole text).
-
using WindowEvent = detail::dp::WindowEvent
A window event from the host (Moved / Resized carry the new geometry).
-
using send_fn = std::function<bool(std::span<const uint8_t> frame)>
Transmits one encoded frame to a host (one per transport; see add_sink). Returns false when the frame could NOT be queued (the transport is gone, or its FIFO did not drain in time). The desktop then stops streaming to that sink (its host’s mirror is incomplete, and every further write would block the desktop task for the transport’s drain timeout), flags it (sink_needs_resync) and re-sends the full snapshot by itself once the transport takes frames again (retried with back-off, kResyncRetryMin..kResyncRetryMax) — or at the host’s next GET_DESKTOP, whichever comes first. A transport must never queue a partial frame (write a frame all-or-nothing).
Public Functions
-
inline AppId register_app(App app)
Register an app; returns its id, or 0 (logged) when kMaxApps are registered already or the DESKTOP records + full app list (with empty descriptions) would no longer fit one frame of Config::max_frame_bytes (apps are never trimmed on the wire, only the window list and the descriptions are). Name / icon / description longer than kMaxAppNameBytes / kMaxAppIconBytes / kMaxAppDescriptionBytes are truncated (logged).
-
inline bool unregister_app(AppId id)
Unregister an app: its open windows are closed (reason Shutdown, their on_close callbacks run on the desktop task) and its id is not handed out again while anything still references it.
-
inline void launch(AppId id)
Launch an app from the firmware (its launch callback runs on the desktop task; a single-instance app that is open is focused).
-
inline Window create_window(WindowConfig cfg)
Open a window (sent at the next flush).
Open a window (sent at the next flush); an invalid handle (logged) when the title is longer than kMaxStr8Bytes.
-
inline bool close_window(WindowId id, WindowCloseReason reason = WindowCloseReason::App)
Close a window; its on_close runs on the desktop task.
-
inline std::vector<Window> windows(AppId app = 0)
The open windows (of one app, or every app when id == 0).
-
inline DialogId message_box(MessageBoxConfig cfg)
Open a message box; returns its id, or 0 (logged) when it would not fit one frame (max_payload(): title / text / buttons too long).
-
inline DialogId input_box(InputBoxConfig cfg)
Open an input box; returns its id, or 0 (logged) when it would not fit one frame (max_payload()).
-
inline bool close_dialog(DialogId id)
Close a dialog from the firmware (its callback does not run).
-
inline bool notify(NotifyConfig cfg)
Show a toast; false (logged) when it would not fit one frame.
-
inline void post(std::function<void()> fn)
Run a function on the desktop task (soon).
-
inline TimerId add_timer(std::chrono::milliseconds period, std::function<void()> fn, WindowId owner = 0)
A periodic callback on the desktop task; `owner` (a window id) cancels it when that window closes. Returns the timer id.
-
inline bool on_desktop_task() const
Whether the caller is the desktop task (where app callbacks run).
-
inline void set_theme(std::string_view theme)
“auto” | “light” | “dark” (anything else is logged and ignored).
-
inline void set_device_name(std::string_view name)
Rename the device (at most kMaxDeviceNameBytes, else truncated).
-
inline SinkId add_sink(send_fn send, uint8_t module = detail::dp::kModule)
Register a transport. Events are sent to it once a GET_DESKTOP arrived through it (set_sink_active). Returns its id.
- Parameters:
module – The dispatcher module id stamped on the frames sent to it.
-
inline void set_sink_active(SinkId id, bool active)
Start / stop broadcasting events to a sink (a disconnected transport should be deactivated; the next GET_DESKTOP reactivates it).
-
inline void submit(Command cmd)
Queue a decoded host request for the desktop task. When the queue is full (Config::max_queued_commands) a request is refused with ERROR(EAGAIN) on its sink and an event is dropped (logged); what is already queued is never evicted.
-
inline bool sink_needs_resync(SinkId id) const
Whether a frame to this sink was dropped and the host’s mirror is still incomplete: streaming to it is paused until the desktop’s own snapshot gets through or the host sends GET_DESKTOP.
-
inline size_t max_text_bytes() const
The bound on a TextArea’s retained text (Config::max_text_bytes, normalized to at least 1).
-
inline size_t max_payload() const
The largest payload sent / accepted (Config::max_frame_bytes less the frame overhead, at most the codec’s limit).
-
inline bool set_columns(WindowId win, WidgetId widget, const std::vector<std::string> &columns)
Replace the column names; false (logged) when they are not representable.
-
inline bool set_items(WindowId win, WidgetId widget, uint16_t start, const std::vector<std::string> &items, bool replace_all)
Replace a range of items (`replace_all` first sets the count to the range’s size). Validated before anything changes: false (logged, model untouched) when the target is unknown, an entry is too large for a frame, or the range does not fit the wire’s u16 index space.
-
inline bool set_prop(WindowId win, WidgetId widget, detail::dp::Prop prop)
False (logged) when the target is unknown or the value cannot be carried on the wire (Title / Placeholder / Tooltip > kMaxShortTextBytes, Columns or an Items entry too large for a frame).
-
inline const std::string &get_name() const
Get the name of the component
Note
This is the tag of the logger
- Returns:
A const reference to the name of the component
-
inline void set_log_tag(const std::string_view &tag)
Set the tag for the logger
- Parameters:
tag – The tag to use for the logger
-
inline espp::Logger::Verbosity get_log_level() const
Get the log level for the logger
See also
See also
- Returns:
The verbosity level of the logger
-
inline void set_log_level(espp::Logger::Verbosity level)
Set the log level for the logger
See also
See also
- Parameters:
level – The verbosity level to use for the logger
-
inline void set_log_verbosity(espp::Logger::Verbosity level)
Set the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls set_log_level
- Parameters:
level – The verbosity level to use for the logger
-
inline espp::Logger::Verbosity get_log_verbosity() const
Get the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls get_log_level
- Returns:
The verbosity level of the logger
-
inline void set_log_rate_limit(std::chrono::duration<float> rate_limit)
Set the rate limit for the logger
See also
Note
Only calls to the logger that have _rate_limit suffix will be rate limited
- Parameters:
rate_limit – The rate limit to use for the logger
Public Static Attributes
-
static constexpr int32_t kNoSelection = -1
A list / table / select selection meaning “nothing”.
-
static constexpr size_t kMinFrameBytes = espp::stream_frame::kMaxHeaderSize + espp::stream_frame::kCrcSize + detail::dp::kMinPayloadBytes
Smallest Config::max_frame_bytes: the largest frame header + CRC + the smallest payload cap (detail::desktop_protocol::kMinPayloadBytes: the largest payload the API accepts that cannot be split — a WINDOW_OPEN head with a 255-byte title, a widget with a 255-byte placeholder / tooltip, the maximal DESKTOP record set — always fits it).
-
static constexpr std::chrono::milliseconds kResyncRetryMin = {1000}
Configuration for the Desktop. Retry period of the device-initiated resync after a dropped frame (see send_fn): starts at the minimum and doubles up to the maximum while the transport keeps refusing frames (each attempt costs one failed write).
-
struct App
An application the desktop lists (register_app) and launches on request.
Public Members
-
std::string name = {}
Shown under the icon / in the start menu.
-
std::string icon = {}
An emoji / short text, or “svg:<name>” from the built-in set.
-
std::string description = {}
Tooltip.
-
std::function<void(Desktop&, AppId)> launch = {nullptr}
Called on the desktop task when the user launches the app (or launch() is called): create the window(s) here.
-
bool single_instance = {true}
Launching again focuses the open window.
Not shown on the desktop (launch() only).
-
std::string name = {}
-
struct Command
A decoded host request, submitted by a DesktopService (or a test) and handled on the desktop task. Replies (if any) go to `sink`.
-
struct Config
Public Members
-
std::string device_name = {"espp"}
Shown in the browser’s tray / title (at most kMaxDeviceNameBytes, else truncated).
-
std::string firmware = {}
e.g. project name + version (DESKTOP record; at most kMaxFirmwareBytes).
-
std::string theme = {"auto"}
“auto” | “light” | “dark” (the browser’s initial theme; anything else is logged and replaced by “auto”).
-
uint32_t accent = {0x3b82f6}
Accent color, 0xRRGGBB.
-
std::chrono::milliseconds flush_period = {50}
How often pending changes are coalesced and sent (the latency of a set_text, and the period that bounds the frame rate of a busy app).
-
size_t max_frame_bytes = {4096}
Largest encoded frame (header + payload + CRC) a sink can carry in one write; every widget payload is split to fit (4096 = the TinyUSB FIFOs of the espp examples; the stream_frame maximum is kMaxFrameSize). At least kMinFrameBytes (a kMinPayloadBytes payload, the largest unsplittable payload the API accepts); a smaller value is clamped with a warning. Dialogs, notifications and the DESKTOP record set are single frames, so a small cap limits them (see the k* limits in the protocol).
-
size_t max_queued_commands = {64}
Bound on host commands queued for the desktop task (at least 1). When it is full a request (GET_DESKTOP / LAUNCH_APP / CLOSE_WINDOW) is refused with ERROR(EAGAIN) and an event is dropped (logged, rate-limited); queued commands are never evicted.
-
size_t max_text_bytes = {16 * 1024}
Bound on a TextArea’s retained text and on a text the host sends for one widget (Text events are reassembled up to this size). Advertised to the host (DESKTOP record MaxTextBytes), which applies the same bound.
- Task::BaseConfig task_config = {.name = "desktop", .stack_size_bytes = 8 * 1024}
The desktop task: every app callback runs on it, so size the stack for the apps (file I/O and fmt formatting comfortably fit 8 KiB).
-
std::string device_name = {"espp"}
-
struct InputBoxConfig
An input box (input_box()). `on_result(text)` gets the text when the default button (0) was pressed, nullopt when cancelled / dismissed.
-
struct MessageBoxConfig
A message box (message_box()). `on_result(button)` gets the index of the button pressed, or -1 when dismissed.
-
struct NotifyConfig
A toast notification (notify()).
Public Members
-
std::chrono::milliseconds timeout = {4000}
0 = sticky until dismissed.
-
std::chrono::milliseconds timeout = {4000}
-
class Widget
A handle to a widget: a value, safe to copy and keep; every method is a no-op once the widget (or its window) is gone.
Public Functions
-
inline void set_color(uint32_t rgb)
0xRRGGBB; 0xFFFFFFFF = the theme’s default.
-
inline void set_items(const std::vector<std::string> &items)
Replace every item (table rows: cells ‘\t’-separated).
-
inline void set_item(uint16_t index, std::string_view item)
Replace one item (the list grows to fit).
-
inline void set_items(uint16_t start, const std::vector<std::string> &items)
Replace a range of items starting at `start`. Refused (logged, nothing changes) when an entry is too large for a frame.
-
inline void set_selected(int32_t index)
Select an item (kNoSelection = none).
-
inline void set_columns(const std::vector<std::string> &columns)
Refused (logged) with more than 255 names, a name over 255 bytes, or a set that does not fit a frame.
-
inline void focus()
Give the widget keyboard focus.
-
inline void remove()
Remove the widget (and its children).
-
inline void on_event(widget_event_fn fn)
Replace the event handler.
-
inline void set_color(uint32_t rgb)
-
class Window
A handle to a window: a value, safe to copy and keep; every method is a no-op once the window is closed.
Public Functions
-
inline Widget add(WidgetConfig cfg)
Add any widget (the general form; the helpers below cover the common ones). Returns an invalid handle on failure (unknown parent, parent not a container, window gone).
-
inline Widget textbox(std::string_view text, std::function<void(const std::string&)> on_submit, WidgetId parent = 0, std::string_view placeholder = "", uint16_t flags = 0)
A single-line field; `on_submit` gets the text on Enter.
-
inline Widget list(const std::vector<std::string> &items, std::function<void(int32_t)> on_select, WidgetId parent = 0, uint8_t weight = 1)
`on_select(index)` on selection change; Activate (double-click / Enter) arrives through Widget::on_event.
-
inline void request_geometry(int16_t x, int16_t y, uint16_t w, uint16_t h)
Ask the browser to move / resize (-1 / 0 = keep); the host answers with a Moved / Resized event carrying the clamped result.
-
inline void focus()
Raise and focus the window.
-
inline void clear()
Remove every widget.
-
inline TimerId add_timer(std::chrono::milliseconds period, std::function<void()> fn)
A periodic callback on the desktop task, cancelled when the window closes.
-
inline Widget add(WidgetConfig cfg)
-
using WidgetEvent = detail::dp::WidgetEvent
-
class Widget
A handle to a widget: a value, safe to copy and keep; every method is a no-op once the widget (or its window) is gone.
Public Functions
-
inline void set_color(uint32_t rgb)
0xRRGGBB; 0xFFFFFFFF = the theme’s default.
-
inline void set_items(const std::vector<std::string> &items)
Replace every item (table rows: cells ‘\t’-separated).
-
inline void set_item(uint16_t index, std::string_view item)
Replace one item (the list grows to fit).
-
inline void set_items(uint16_t start, const std::vector<std::string> &items)
Replace a range of items starting at `start`. Refused (logged, nothing changes) when an entry is too large for a frame.
-
inline void set_selected(int32_t index)
Select an item (kNoSelection = none).
-
inline void set_columns(const std::vector<std::string> &columns)
Refused (logged) with more than 255 names, a name over 255 bytes, or a set that does not fit a frame.
-
inline void focus()
Give the widget keyboard focus.
-
inline void remove()
Remove the widget (and its children).
-
inline void on_event(widget_event_fn fn)
Replace the event handler.
-
inline void set_color(uint32_t rgb)
-
class Window
A handle to a window: a value, safe to copy and keep; every method is a no-op once the window is closed.
Public Functions
-
inline Widget add(WidgetConfig cfg)
Add any widget (the general form; the helpers below cover the common ones). Returns an invalid handle on failure (unknown parent, parent not a container, window gone).
-
inline Widget textbox(std::string_view text, std::function<void(const std::string&)> on_submit, WidgetId parent = 0, std::string_view placeholder = "", uint16_t flags = 0)
A single-line field; `on_submit` gets the text on Enter.
-
inline Widget list(const std::vector<std::string> &items, std::function<void(int32_t)> on_select, WidgetId parent = 0, uint8_t weight = 1)
`on_select(index)` on selection change; Activate (double-click / Enter) arrives through Widget::on_event.
-
inline void request_geometry(int16_t x, int16_t y, uint16_t w, uint16_t h)
Ask the browser to move / resize (-1 / 0 = keep); the host answers with a Moved / Resized event carrying the clamped result.
-
inline void focus()
Raise and focus the window.
-
inline void clear()
Remove every widget.
-
inline TimerId add_timer(std::chrono::milliseconds period, std::function<void()> fn)
A periodic callback on the desktop task, cancelled when the window closes.
-
inline Widget add(WidgetConfig cfg)
Header File
Classes
-
class DesktopService : public espp::BaseComponent
Serves an espp::Desktop over any framed byte stream (dispatcher module 9 by default; see Config::module).
GET_DESKTOP answers with the DESKTOP snapshot (apps, settings, open windows) followed by the full tree of every open window and every open dialog, and marks this transport attached: from then on the Desktop broadcasts its changes (WINDOW_OPEN / WIDGET_SET / … ) to it until detach() (e.g. on USB unmount) or until the next GET_DESKTOP after a reconnect. A frame the transport refuses pauses the broadcasts (the transport stays attached, needs_resync() is true) until the Desktop’s own retried snapshot gets through or the host sends GET_DESKTOP. LAUNCH_APP / CLOSE_WINDOW are acknowledged with OK / ERROR; the events (WINDOW_EVENT / WIDGET_EVENT / DIALOG_RESULT) are not.
**Threading**: an internal mutex covers the parser; the only frame this object sends itself is the ERROR for a malformed request, serialized on a send mutex that is also held while the Desktop’s task sends through this transport, so frames never interleave. `send` must not re-enter this object.
DesktopService Example
// The desktop: one per device. Apps register with it; every app callback // runs on its task. const auto sysinfo = espp::SystemInfo::collect(); espp::Desktop desktop( {.device_name = "espp Desktop", .firmware = fmt::format("{} {}", sysinfo.project_name, sysinfo.app_version), .task_config = {.name = "desktop", .stack_size_bytes = 10 * 1024}, .log_level = espp::Logger::Verbosity::INFO}); register_counter_app(desktop); register_about_app(desktop); register_system_monitor_app(desktop); register_task_manager_app(desktop); register_log_viewer_app(desktop); register_files_app(desktop); #if DESKTOP_EXAMPLE_CANOPEN_APP register_canopen_app(desktop); #elif CONFIG_DESKTOP_EXAMPLE_ENABLE_CANOPEN logger.warn("CONFIG_DESKTOP_EXAMPLE_ENABLE_CANOPEN is set but the CANopen app was not built: " "it needs IDF >= 6.0 (twai) and the CMake option DESKTOP_EXAMPLE_CANOPEN=ON"); #endif #if CONFIG_DESKTOP_EXAMPLE_ENABLE_I2C register_i2c_scanner_app(desktop); #endif #if CONFIG_DESKTOP_EXAMPLE_ENABLE_WIFI || CONFIG_DESKTOP_EXAMPLE_ENABLE_ETHERNET register_network_app(desktop); #endif register_settings_app(desktop, "espp Desktop"); desktop_example::apply_saved_settings(desktop); // USB composite device: a vendor/WebUSB function and a CDC function, both // carrying the same framed protocol. espp::UsbDevice::Config usb_cfg; usb_cfg.pid = 0x0d38; // distinct from the espp default so the webapp filter is specific usb_cfg.manufacturer = "espp"; usb_cfg.product = "espp Desktop"; usb_cfg.log_level = espp::Logger::Verbosity::WARN; espp::UsbDevice::CdcFunction cdc; cdc.interface_name = "espp Desktop (CDC)"; usb_cfg.cdc = cdc; espp::UsbDevice::VendorFunction vendor; vendor.interface_name = "espp Desktop (WebUSB)"; vendor.webusb = true; // advertise BOS / WebUSB / MS OS 2.0 descriptors vendor.landing_page_url = "esp-cpp.github.io/espp/apps/desktop.html"; usb_cfg.vendor = vendor; espp::UsbDevice usb(usb_cfg); // One send function per transport, serialized by one mutex each, so the // services' frames never interleave. write_vendor / write_cdc are // all-or-nothing: they wait (bounded, 250 ms) for FIFO room for the WHOLE // frame and never queue a partial one, and return false when the host did // not drain in time (unplugged, or the page is not reading) -- the desktop // then flags that transport as needing a resync, and the browser resyncs // with GET_DESKTOP when it reconnects. std::mutex vendor_tx_mutex, cdc_tx_mutex; auto vendor_send = [&](std::span<const uint8_t> frame) { std::lock_guard<std::mutex> lock(vendor_tx_mutex); return usb.write_vendor(frame); }; auto cdc_send = [&](std::span<const uint8_t> frame) { std::lock_guard<std::mutex> lock(cdc_tx_mutex); return usb.write_cdc(frame); }; // One service instance per transport (they are cheap; each replies on its // own stream). Declared BEFORE the workers that call into them. espp::DesktopService vendor_desktop( desktop, {.send = vendor_send, .log_level = espp::Logger::Verbosity::INFO}); espp::DesktopService cdc_desktop(desktop, {.send = cdc_send, .log_level = espp::Logger::Verbosity::INFO}); espp::SystemService vendor_system({.send = vendor_send}); espp::SystemService cdc_system({.send = cdc_send}); espp::MonitorService vendor_monitor( {.send = vendor_send, .task_config = {.name = "monitor_v", .stack_size_bytes = 6 * 1024}}); espp::MonitorService cdc_monitor( {.send = cdc_send, .task_config = {.name = "monitor_c", .stack_size_bytes = 6 * 1024}}); espp::OtaService vendor_ota(ota, {.send = vendor_send}); espp::OtaService cdc_ota(ota, {.send = cdc_send}); espp::CoreDumpService vendor_coredump(core_dump, {.send = vendor_send}); espp::CoreDumpService cdc_coredump(core_dump, {.send = cdc_send}); // One DispatcherWorker per byte stream: a bounded receive queue + worker // task feeding its Dispatcher, so the services never run on the TinyUSB // task. Registering a service routes its module to it and advertises it for // discovery; serve_discovery() answers the hub's ListModules query. espp::DispatcherWorker vendor_link( {.send = vendor_send, .on_overflow = [&]() { vendor_ota.on_rx_overflow(); }, .task_config = {.name = "desktop_rx_vendor", .stack_size_bytes = 8192}}); espp::DispatcherWorker cdc_link( {.send = cdc_send, .on_overflow = [&]() { cdc_ota.on_rx_overflow(); }, .task_config = {.name = "desktop_rx_cdc", .stack_size_bytes = 8192}}); for (auto *link : {&vendor_link, &cdc_link}) { const bool v = link == &vendor_link; link->register_module(v ? vendor_desktop : cdc_desktop); link->register_module(v ? vendor_system : cdc_system); link->register_module(v ? vendor_monitor : cdc_monitor); link->register_module(v ? vendor_ota : cdc_ota); link->register_module(v ? vendor_coredump : cdc_coredump); link->serve_discovery(usb_cfg.product); }
Public Types
-
using send_fn = std::function<bool(std::span<const uint8_t> frame)>
Transmits one encoded frame to the host, all-or-nothing, and returns whether it was queued. Unlike the other espp services’ `send`, which is void, this one must report a refused frame: the desktop streams state, so it then pauses streaming to this transport and re-sends the full snapshot by itself once frames go through again, see needs_resync(). UsbDevice::write_vendor / write_cdc have exactly this contract (bounded wait for FIFO room, never a partial frame).
Public Functions
-
inline explicit DesktopService(Desktop &desktop, const Config &config)
Construct the service and register its transport with the desktop.
-
inline uint8_t module_id() const
The dispatcher module id this service answers on (Config::module).
-
inline Dispatcher::ModuleInfo module_info() const
Discovery metadata for registering this service on a Dispatcher.
-
inline void detach()
Stop broadcasting to this transport (call when it disconnects, e.g. on USB unmount); the next GET_DESKTOP re-attaches it.
-
inline bool attached() const
Whether a host asked for the desktop on this transport (and it was not detached since).
-
inline bool needs_resync() const
Whether a frame to this transport was dropped (send returned false) and the host’s mirror is still incomplete: streaming is paused (the transport stays attached()) until the desktop’s own snapshot gets through (retried with back-off) or the host sends GET_DESKTOP.
-
inline void handle(const espp::stream_frame::Frame &frame)
Dispatcher entry point: handle one routed frame. Frames for other modules and reply-flagged frames are ignored, so this can be registered directly: `dispatcher.register_module(service)`.
-
inline void feed(std::span<const uint8_t> data)
Feed received transport bytes (standalone use, without a Dispatcher).
-
inline bool handle_frame(uint8_t type, std::span<const uint8_t> payload, std::optional<uint16_t> correlation = std::nullopt)
Handle one already-parsed request frame.
- Parameters:
correlation – The request frame’s correlation id, if it carried one; the reply (DESKTOP / OK / ERROR) echoes it.
- Returns:
true if the type belongs to the desktop protocol (it was queued for the desktop task, or answered with ERROR), false if ignored.
-
inline const std::string &get_name() const
Get the name of the component
Note
This is the tag of the logger
- Returns:
A const reference to the name of the component
-
inline void set_log_tag(const std::string_view &tag)
Set the tag for the logger
- Parameters:
tag – The tag to use for the logger
-
inline espp::Logger::Verbosity get_log_level() const
Get the log level for the logger
See also
See also
- Returns:
The verbosity level of the logger
-
inline void set_log_level(espp::Logger::Verbosity level)
Set the log level for the logger
See also
See also
- Parameters:
level – The verbosity level to use for the logger
-
inline void set_log_verbosity(espp::Logger::Verbosity level)
Set the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls set_log_level
- Parameters:
level – The verbosity level to use for the logger
-
inline espp::Logger::Verbosity get_log_verbosity() const
Get the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls get_log_level
- Returns:
The verbosity level of the logger
-
inline void set_log_rate_limit(std::chrono::duration<float> rate_limit)
Set the rate limit for the logger
See also
Note
Only calls to the logger that have _rate_limit suffix will be rate limited
- Parameters:
rate_limit – The rate limit to use for the logger
Public Static Attributes
-
static constexpr uint8_t kModule = espp::detail::desktop_protocol::kModule
Default dispatcher module id (9). A routing key only: Config::module serves on any id, and hosts find it through discovery (by kProtocol).
-
static constexpr const char *kProtocol = espp::detail::desktop_protocol::kProtocol
Stable protocol identifier + version advertised through discovery.
-
struct Config
Configuration for the DesktopService.
Public Members
-
uint8_t module = {kModule}
Dispatcher module id this instance answers on (and stamps on every frame it sends). A routing key only (0x00..0xEF).
-
uint8_t module = {kModule}
-
using send_fn = std::function<bool(std::span<const uint8_t> frame)>
Header File
Classes
-
class ConsoleCapture
Captures stdout / stderr into a bounded byte ring while teeing them to the original console.
A process-wide singleton (there is one stdout): install() once, then any task may call read_since() with its own cursor to page through the log without blocking the writers. The cursor is an absolute byte position (total_bytes()); when the ring overwrote bytes the reader had not consumed, read_since() reports how many were dropped and resumes at the oldest byte still kept.
ConsoleCapture Example
// First thing: tee stdout / stderr into a ring the Log Viewer app reads, so // the boot log is captured too. The UART console keeps working. #if CONFIG_DESKTOP_EXAMPLE_LOG_CAPTURE { std::error_code ec; espp::ConsoleCapture::install( {.capacity_bytes = CONFIG_DESKTOP_EXAMPLE_LOG_CAPTURE_BYTES, .tee_to_console = true}, ec); } #endif
Public Static Functions
-
static inline bool install(const Config &config, std::error_code &ec)
Register the capture device and re-point stdout / stderr at it.
- Parameters:
config – Ring size and tee / strip options.
ec – Set on failure (io_error: the VFS could not be registered or stdout could not be re-opened — the original console is restored on a best-effort basis).
- Returns:
true on success, or when already installed (the config of the first install stays; the tee / strip flags are updated).
-
static inline size_t capacity()
The ring capacity in bytes.
-
static inline uint64_t total_bytes()
Bytes captured since boot (monotonic; a cursor value).
-
static inline size_t available()
Bytes currently kept in the ring (readable).
-
static inline size_t read_since(uint64_t *cursor, std::string &out, size_t max_bytes, size_t *dropped = nullptr)
Copy the bytes captured after `*cursor` (at most max_bytes) and advance the cursor.
- Parameters:
cursor – In: where the reader is (0 = from the oldest readable byte); out: the position after the bytes returned.
out – Appended with the bytes.
max_bytes – Upper bound on the bytes appended in this call.
dropped – If not null, set to the number of bytes the ring EVICTED (capacity overwrite) before the reader got to them (0 when none); bytes hidden by clear() are skipped without being counted.
- Returns:
The number of bytes appended.
-
static inline void clear()
Forget everything captured so far (readers resume at total_bytes()).
-
static inline void set_tee_to_console(bool enable)
Switch the tee to the original console on / off.
Public Static Attributes
-
static constexpr const char *kVfsPath = "/dev/logcap"
VFS path of the capture device (must be <= ESP_VFS_PATH_MAX).
-
struct Config
Configuration for install().
Public Members
-
size_t capacity_bytes = {16 * 1024}
Ring size: the newest bytes kept.
-
bool tee_to_console = {true}
Keep writing to the original console (the UART / USB-Serial-JTAG monitor). Off = capture only (the console goes quiet).
-
bool strip_ansi = {false}
Remove ANSI CSI escape sequences (colors) from the captured bytes; the tee still gets them. Leave off when the consumer renders ANSI itself (the desktop Log Viewer does).
-
size_t capacity_bytes = {16 * 1024}
-
static inline bool install(const Config &config, std::error_code &ec)