Skip to content

feat(wasm): browser-preview emulator for real LVGL widgets (Emscripten) - #9

Draft
physicsG wants to merge 5 commits into
develop/snapmaker-multiacefrom
tooling/lvgl-web-emulator
Draft

physicsG wants to merge 5 commits into
develop/snapmaker-multiacefrom
tooling/lvgl-web-emulator

Conversation

@physicsG

Copy link
Copy Markdown
Owner

Compile HelixScreen's actual LVGL 9.5 widgets to WebAssembly and run them in a browser — a fast, headless-screenshotable way to iterate on widget visuals and animation without a device or the full app. build_widget.sh links a harness main() (smoke / widget / ace — the last a full interactive ACE-page mockup with a mocked load/unload run) against LVGL + helix-xml + a small stub layer for the app-side symbols the widgets touch. launch.sh builds + serves; pw_shot.py / ace_shot.py drive Playwright/Chromium for autonomous screenshots. Full docs in wasm/README.md; CLAUDE.md points future agents at it.

physicsG and others added 5 commits August 19, 2026 09:57
Compile HelixScreen's actual LVGL 9.5 widgets to WebAssembly and run them in a
browser — a fast, headless-screenshotable way to iterate on widget visuals and
animation without a device or the full app. build_widget.sh links a harness
main() (smoke / widget / ace — the last a full interactive ACE-page mockup with
a mocked load/unload run) against LVGL + helix-xml + a small stub layer for the
app-side symbols the widgets touch. launch.sh builds + serves; pw_shot.py /
ace_shot.py drive Playwright/Chromium for autonomous screenshots. Full docs in
wasm/README.md; CLAUDE.md points future agents at it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
set_multiace_mode() wrote names, display names and per-unit topologies behind
`if (system_info_.units.size() >= 3)`, but nothing in it ever grew that vector
past the single entry the constructor pushes, so the guard was always false and
the block dead. get_system_info() overlays system_info_.units onto the
registry-built ones, so unit 0 kept the generic "Mock MMU" and the U1 screen
captioned itself with a name that belongs to no product; only unit_topologies_
kept the ACEs drawing as combiners. Build the metadata the way every other
multi-unit profile does, with "SnapSwap" scoped to this profile and the ACE
names mirroring AmsBackendMultiAce (display "ACE 1"/"ACE 2" — the model name is
what a lone ACE shows, and the dead block put it on both).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Compiles ~600 app TUs unmodified for wasm32 and boots them from app_main.cpp
the way Application::run() boots the desktop, so the three Snapmaker U1 screens
— Multi-Filament, SnapSwap, multiACE — render as production XML driven by the
real AmsOverviewPanel, theme_manager, subject graph and AmsState. Only the
printer is mocked. app_srcs.txt is derived, not curated: the link closure of the
AMS panel objects taken from a native build with nm, filtered to what emcc
accepts; stubs_app.cpp holds the whole remainder (libhv's sync HTTP client, the
wpa_supplicant socket) — 26 symbols the AMS pages never call.

helix_ctl exports the `helix-screen ctl` command surface into the page through
RemoteControlServer::serve_inproc(), so the same JSON-RPC vocabulary and widget
locators drive a browser; ctl_shot.py walks all three screens headlessly. Its
single-threaded arm queues onto UpdateQueue and pumps rather than calling
inline, because handlers that rebuild widget subtrees expect to run inside
process_pending(), and pumps on the CLOCK because hundreds of idle
lv_timer_handler() calls fit inside the queue timer's 1 ms period.

wifi_backend/wifi_manager gain __EMSCRIPTEN__ arms beside the existing
ESP_PLATFORM ones (no radio in a tab), and HistoryListPanel::PAGE_SIZE is
renamed — it collides with a macro in Emscripten's limits.h. Build flags that
are load-bearing rather than tuning (the C++-only PCH force-include,
-fexceptions, the non-unwinding main-loop form) are documented in wasm/README.md
along with the two known limitations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
topmost_visible() packs its ranking key with a field at bit 40, in a `long`.
That is 64 bits on the x86-64 desktop this was written on and 32 bits on every
32-bit target we build for, where a shift wider than the type is undefined
behaviour and clang emits an unconditional trap. So `ctl click btn_unload` --
any name that exists in more than one panel, which is most of them -- killed the
process on armhf/MIPS devices instead of picking the visible copy. Found by
running the command surface under wasm32, which has the same 32-bit long.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ScriptedU1 derives from the real AmsBackendMultiAce and replaces only the two
ends of the wire: execute_gcode() is intercepted rather than reaching Moonraker,
and the reply arrives as handle_status_update() frames on an lv_timer, in the
channel_state vocabulary classify_channel_state() reads. Everything between --
the dispatch rules, the load latch, the phase-to-step mapping, the ACE-fed vs
feeder split -- is production code, so an unload on an ACE-fed head ends at
preload_finish because the ACE performs the retract, and loading a bay onto an
occupied head emits ACE_UNLOAD_HEAD + ACE_LOAD_HEAD into one 6-step LOAD_SWAP
bar. Frame shapes follow the live capture in tests/fixtures/snapmaker_u1/.

Heaters are modelled rather than scripted: frames set a target and never a
temperature, and one 5 Hz owner interpolates toward it -- deadline-based, so a
ramp lands on target at exactly the requested time whatever distance it covers.
Scripts that wrote temperatures directly while a second timer wrote the same
field made the Heat step's live readout look like noise. A heater held at an
active target is now dead steady; only a cold one wanders. HEAT_MS is 3 s for a
full ambient-to-working span and proportionally less for a shorter ramp, so a
swap's second half does not sit through a heat cycle for a temperature its first
half already reached.

Each frame is published to both the AMS backend and PrinterState, as the notify
stream does; feeding only the backend left every temperature in the UI at 0 C
and the sidebar's preheat gate never opened. ops_shot.py drives all three
operations headlessly through the helix_ctl bridge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant