Summary
sensesp::UIButton (src/sensesp/ui/ui_button.h, ui_button.cpp) is
documented as:
Each UIButton object creates a button in the "Control" tab of the
web UI. When the button is clicked, the object's observers are
notified.
This does not happen. UIButton::add() registers the button into a
static std::map<String, std::shared_ptr<UIButton>>
(ui_buttons_/get_ui_buttons()), but nothing else in the codebase —
neither the HTTP backend nor the bundled web UI frontend — reads that
map, serves it to the browser, or handles a click. An app that
constructs a UIButton and attach()es a callback to it will compile
and run fine, but the button never appears anywhere in the web UI, and
the callback never fires.
Environment
- SensESP version: 3.5.0 (resolved via
SignalK/SensESP @ ^3.2.0 in
platformio.ini)
- Also checked against the current
main branch on GitHub as of
2026-08 — same result.
- Platform: ESP32-C3, PlatformIO/Arduino framework.
How to reproduce
#include "sensesp/ui/ui_button.h"
auto* btn = sensesp::UIButton::add("my_action", "Do The Thing");
btn->attach([]() { Serial.println("clicked!"); });
Flash this alongside a normal SensESPAppBuilder setup, open the
device's web UI, and look for a "Control" tab or any rendering of "Do
The Thing." Nothing appears.
Investigation
1. Backend: no HTTP handler serves or consumes the button registry.
Grepped every .cpp/.h under src/sensesp/net/ (the HTTP
server/handler layer) for UIButton, ui_button, ui_buttons_, and
get_ui_buttons:
$ grep -rln "UIButton\|ui_buttons\|get_ui_buttons" src/sensesp/net/
(no results)
The only files referencing UIButton at all are ui_button.h and
ui_button.cpp themselves. config_handler.cpp only implements
/api/config* (get/put persisted config items via
ConfigItem/FileSystemSaveable); app_command_handler.cpp and
base_command_handler.cpp implement fixed, hardcoded endpoints
(/api/device/reset, /api/device/restart, /api/info, /api/log,
/api/routes, WiFi scan, SignalK TOFU reset) — nothing generic or
pluggable for app-registered commands/buttons.
2. Frontend: the actual served bundle has no button-rendering
mechanism either.
The web UI HTML/CSS/JS actually served at / (and /status,
/configuration, /system, /log, /wifi, /signalk — see
add_routes_handlers() in base_command_handler.cpp) comes from
src/sensesp/net/web/autogen/frontend_files.h, a generated
kFrontendFiles[] array of gzip-compressed static assets (a
Preact-based SPA, judging by the bundle's
__federation_shared_preact-*.js chunk name).
I extracted and decompressed the main JS bundle
(/assets/index-*.js, ~34KB gzipped / ~100KB decompressed) out of the
compiled firmware and searched it directly:
$ grep -o '/api/[a-zA-Z_/-]*' index_bundle.js | sort -u
/api/config
/api/device/reset
/api/device/restart
/api/info
/api/log
/api/routes
/api/signalk
/api/signalk/reset-tofu
/api/wifi/scan
/api/wifi/scan-results
No /api/command, /api/buttons, or anything resembling a generic
custom-action endpoint. The only two buttons that exist anywhere in
the actual UI are hardcoded, fixed "Restart the device" and "Reset the
device to factory defaults" (wired to the two fixed /api/device/*
endpoints above) — there is no loop or component in the bundle that
reads a dynamic list of registered commands/buttons and renders one
per entry.
3. A separate, older, apparently-unused frontend file exists in the
source tree (src/sensesp/net/web/js_sensesp.h, index.h,
css_bootstrap.h, static_file_data.h, served via
add_static_file_handlers()). Decompressing that JS shows a
different, older single-page-app implementation that does have a
showControl() function referencing deviceInfo.Commands and calling
GET /command?id=<name> — a shape that lines up conceptually with
UIButton's title_/name_/must_confirm_ fields. But:
- Nothing populates
Commands in any /info-family JSON response on
the backend (checked add_http_info_handler() in
base_command_handler.cpp; it emits StatusPageItemBase entries
only, no Commands key).
- Nothing handles
GET /command on the backend either.
- This older bundle isn't what's served at the root/default routes
that add_routes_handlers() registers — the newer
autogen/frontend_files.h bundle is.
So even this older, seemingly-more-complete-looking client-side
mechanism is dead on both ends in the current codebase — it may be
the vestige of a previous web UI generation that UIButton was
originally built against, before the web UI was replaced with the
current Preact SPA (which never got equivalent support added).
Impact
Any application code that follows UIButton's documented usage (the
class's own doc comment, and presumably any examples/docs pointing at
it) silently does nothing — no compile error, no runtime error, no log
message, just a button that never appears. This is a confusing trap:
it looks like a supported, working feature.
Suggested fix (one of)
- Wire it up properly: add a backend endpoint that serves
UIButton::get_ui_buttons() as JSON and a POST/GET endpoint to
invoke notify() on the matching button by name, then add the
corresponding rendering + click-handling to the current Preact
bundle's "Control"-equivalent page (there doesn't currently appear
to be one at all in the new frontend — showControl()'s
functionality may need to be reintroduced from scratch, not just
reconnected).
- Mark it clearly non-functional: update the class doc comment to
say it currently has no UI backing in this version, so people don't
build against it expecting it to work.
- Remove it, if there's no near-term plan to wire it up, rather
than leave working-looking dead code in the public API.
Happy to test a fix against real hardware if one gets proposed — this
came up while building a compass-interface firmware on top of SensESP
3.5.0/ESP32-C3 and needing a one-click "start calibration" action;
ended up keeping the existing config-checkbox-based trigger mechanism
instead, since that's the only mechanism that's actually confirmed to
work end-to-end.
Summary
sensesp::UIButton(src/sensesp/ui/ui_button.h,ui_button.cpp) isdocumented as:
This does not happen.
UIButton::add()registers the button into astatic
std::map<String, std::shared_ptr<UIButton>>(
ui_buttons_/get_ui_buttons()), but nothing else in the codebase —neither the HTTP backend nor the bundled web UI frontend — reads that
map, serves it to the browser, or handles a click. An app that
constructs a
UIButtonandattach()es a callback to it will compileand run fine, but the button never appears anywhere in the web UI, and
the callback never fires.
Environment
SignalK/SensESP @ ^3.2.0inplatformio.ini)mainbranch on GitHub as of2026-08 — same result.
How to reproduce
Flash this alongside a normal
SensESPAppBuildersetup, open thedevice's web UI, and look for a "Control" tab or any rendering of "Do
The Thing." Nothing appears.
Investigation
1. Backend: no HTTP handler serves or consumes the button registry.
Grepped every
.cpp/.hundersrc/sensesp/net/(the HTTPserver/handler layer) for
UIButton,ui_button,ui_buttons_, andget_ui_buttons:The only files referencing
UIButtonat all areui_button.handui_button.cppthemselves.config_handler.cpponly implements/api/config*(get/put persisted config items viaConfigItem/FileSystemSaveable);app_command_handler.cppandbase_command_handler.cppimplement fixed, hardcoded endpoints(
/api/device/reset,/api/device/restart,/api/info,/api/log,/api/routes, WiFi scan, SignalK TOFU reset) — nothing generic orpluggable for app-registered commands/buttons.
2. Frontend: the actual served bundle has no button-rendering
mechanism either.
The web UI HTML/CSS/JS actually served at
/(and/status,/configuration,/system,/log,/wifi,/signalk— seeadd_routes_handlers()inbase_command_handler.cpp) comes fromsrc/sensesp/net/web/autogen/frontend_files.h, a generatedkFrontendFiles[]array of gzip-compressed static assets (aPreact-based SPA, judging by the bundle's
__federation_shared_preact-*.jschunk name).I extracted and decompressed the main JS bundle
(
/assets/index-*.js, ~34KB gzipped / ~100KB decompressed) out of thecompiled firmware and searched it directly:
No
/api/command,/api/buttons, or anything resembling a genericcustom-action endpoint. The only two buttons that exist anywhere in
the actual UI are hardcoded, fixed "Restart the device" and "Reset the
device to factory defaults" (wired to the two fixed
/api/device/*endpoints above) — there is no loop or component in the bundle that
reads a dynamic list of registered commands/buttons and renders one
per entry.
3. A separate, older, apparently-unused frontend file exists in the
source tree (
src/sensesp/net/web/js_sensesp.h,index.h,css_bootstrap.h,static_file_data.h, served viaadd_static_file_handlers()). Decompressing that JS shows adifferent, older single-page-app implementation that does have a
showControl()function referencingdeviceInfo.Commandsand callingGET /command?id=<name>— a shape that lines up conceptually withUIButton'stitle_/name_/must_confirm_fields. But:Commandsin any/info-family JSON response onthe backend (checked
add_http_info_handler()inbase_command_handler.cpp; it emitsStatusPageItemBaseentriesonly, no
Commandskey).GET /commandon the backend either.that
add_routes_handlers()registers — the newerautogen/frontend_files.hbundle is.So even this older, seemingly-more-complete-looking client-side
mechanism is dead on both ends in the current codebase — it may be
the vestige of a previous web UI generation that
UIButtonwasoriginally built against, before the web UI was replaced with the
current Preact SPA (which never got equivalent support added).
Impact
Any application code that follows
UIButton's documented usage (theclass's own doc comment, and presumably any examples/docs pointing at
it) silently does nothing — no compile error, no runtime error, no log
message, just a button that never appears. This is a confusing trap:
it looks like a supported, working feature.
Suggested fix (one of)
UIButton::get_ui_buttons()as JSON and a POST/GET endpoint toinvoke
notify()on the matching button by name, then add thecorresponding rendering + click-handling to the current Preact
bundle's "Control"-equivalent page (there doesn't currently appear
to be one at all in the new frontend —
showControl()'sfunctionality may need to be reintroduced from scratch, not just
reconnected).
say it currently has no UI backing in this version, so people don't
build against it expecting it to work.
than leave working-looking dead code in the public API.
Happy to test a fix against real hardware if one gets proposed — this
came up while building a compass-interface firmware on top of SensESP
3.5.0/ESP32-C3 and needing a one-click "start calibration" action;
ended up keeping the existing config-checkbox-based trigger mechanism
instead, since that's the only mechanism that's actually confirmed to
work end-to-end.