Skip to content

wasm demo: run WebUIs against FluidNC's own web server - #1913

Merged
MitchBradley merged 9 commits into
mainfrom
WasmDemoWebServer
Oct 12, 2026
Merged

MitchBradley merged 9 commits into
mainfrom
WasmDemoWebServer

Conversation

@MitchBradley

@MitchBradley MitchBradley commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

What this does

The WASM demo used to run a WebUI by loading it into an iframe and talking to it over a postMessage bridge. Each WebUI needed bridge code of its own, and that code had to be maintained separately in WebUI 2, WebUI 3 and FigUI.

This PR removes the bridge. The wasm module now contains FluidNC's real web server, and the demo behaves like a board on the network. You browse to fluidnc.local, FluidNC serves its WebUI from LocalFS, and the WebUI talks ordinary HTTP and WebSocket to it, unmodified. Stock releases of WebUI 2, WebUI 3 and FigUI run this way.

How

A web page can't listen on a socket, so the server's sockets come from JavaScript instead:

device iframe (WebUI, served by FluidNC)
  fetch / XHR / links ─► vnet-sw.js (Service Worker) ─┐
  new WebSocket(...)  ─► vnet-ws-shim.js ─────────────┤  MessagePorts
                                                      ▼
demo page: vnet-host.js (HTTP/1.1 + WebSocket client) ─► fluidnc_vconn_* ─► BrowserAsyncTCP ─► ESPAsyncWebServer ─► WebUIServer / WSChannel / WebDAV

Commits

  1. wasm: run FluidNC's own web server inside the wasm module
    • BrowserAsyncTCP/: AsyncTCP over virtual connections driven from JS.
    • VirtualNet.cpp: the network, at 192.168.0.1 and <$Hostname>.local.
    • A single env:wasm, on Arduino-Emulator like the posix envs.
    • Removes the bridge sources: ShimChannel.cpp, wasm_fs_bridge.cpp, FwInfo.cpp.
  2. wasm demo: a virtual network from the page to FluidNC's web server
    • vnet-sw.js, a Service Worker that relays the iframe's HTTP.
    • vnet-ws-shim.js, a WebSocket stand-in.
    • vnet-host.js, an HTTP/WebSocket client with a cookie jar.
  3. wasm demo: run WebUIs the way a controller does

Related

Testing

  • Headless, in Node (no browser): vconn_smoke_test.mjs, and vnet_client_test.mjs with 18 checks, both pass. They cover gzip, cookies, chunked replies, uploads with the size check, the WebUI install sequence, and a WebSocket session. Ad hoc runs also covered WebDAV GET/PUT on both volumes, directory GETs, and repeated [ESP400] reads.
  • In a browser:
    • FluidNC's built-in file manager.
    • WebUI 2 from a local build; WebUI 3 and FigUI from their GitHub releases.
    • Reading settings in WebUI 2 and WebUI 3.
    • Installing through the menu, the address bar, and escaped navigations.
  • ESP32: wifi_s3 builds with the shared files touched here; no runtime change on hardware.

Known limits

  • WebUI 2's menu entry needs a non-pre-release WebUI 2 build on MitchBradley/ESP3D-WEBUI.
  • Telnet isn't carried, and the WebUI has to run inside the demo page's frame.
  • The Netlify function changed (it now passes the .gz through unchanged), so the site needs a redeploy.

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings October 9, 2026 23:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

WebSocket cancellation, truncated HTTP handling, multi-tab routing, and WebUI installation have unresolved correctness issues.

5 open findings
What changed in this PR

Reworks the WASM demo to serve unmodified WebUIs through FluidNC’s real HTTP, WebSocket, WebDAV, and filesystem stack.

Changes:

  • Adds BrowserAsyncTCP and JavaScript HTTP/WebSocket virtual networking.
  • Adds persistent seeded filesystems and WebUI installation/file controls.
  • Removes the former postMessage command/filesystem bridge.
File Description
platformio.ini Enables the WebUI stack and WASM networking dependencies.
FluidNC/​wasm/​wasm_main.cpp Removes shim-channel initialization and exports.
FluidNC/​wasm/​wasm_fs_bridge.cpp Removes the filesystem bridge.
FluidNC/​wasm/​vnet_client_test.mjs Tests HTTP, uploads, and WebSockets headlessly.
FluidNC/​wasm/​VirtualNet.cpp Adds the WASM virtual network module.
FluidNC/​wasm/​vconn_smoke_test.mjs Adds raw virtual-connection smoke coverage.
FluidNC/​wasm/​ShimChannel.cpp Removes the postMessage channel.
FluidNC/​wasm/​README.md Documents persistence and virtual networking.
FluidNC/​wasm/​pio_toolchain_wasm.py Exports FS and links IDBFS support.
FluidNC/​wasm/​IPAddress.h Uses Arduino-Emulator’s IPAddress when available.
FluidNC/​wasm/​FwInfo.cpp Removes the superseded firmware-info module.
FluidNC/​wasm/​functions/​webui-proxy.js Proxies release gzip files unchanged.
FluidNC/​wasm/​demo/​vnet-ws-shim.js Implements iframe WebSocket relaying.
FluidNC/​wasm/​demo/​vnet-sw.js Relays iframe HTTP through a service worker.
FluidNC/​wasm/​demo/​vnet-host.js Implements the virtual HTTP/WebSocket client.
FluidNC/​wasm/​demo/​serve.py Adds a local WebUI release proxy.
FluidNC/​wasm/​demo/​seed-sd/​square.nc Adds sample SD G-code.
FluidNC/​wasm/​demo/​seed-sd/​Rotor.nc Adds a larger sample job.
FluidNC/​wasm/​demo/​seed-sd/​manifest.json Lists SD seed files.
FluidNC/​wasm/​demo/​seed-localfs/​preferences.json Adds seeded WebUI preferences.
FluidNC/​wasm/​demo/​seed-localfs/​manifest.json Lists LocalFS seed files.
FluidNC/​wasm/​demo/​seed-localfs/​config.yaml Adds the demo machine configuration.
FluidNC/​wasm/​demo/​index.html Adds browsing, installation, files, and persistence.
FluidNC/​wasm/​BrowserAsyncTCP/​AsyncTCP.h Defines the virtual AsyncTCP API.
FluidNC/​wasm/​BrowserAsyncTCP/​AsyncTCP.cpp Implements threaded virtual connections.
FluidNC/​src/​WebUI/​WebCommands.cpp Removes obsolete ShimChannel documentation.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread FluidNC/wasm/demo/vnet-sw.js Outdated
Comment thread FluidNC/wasm/demo/index.html
Comment thread FluidNC/wasm/demo/vnet-host.js
Comment thread FluidNC/wasm/demo/vnet-host.js
Comment thread FluidNC/wasm/IPAddress.h Outdated
MitchBradley and others added 9 commits October 11, 2026 19:59
The wasm build now includes the real WebUI/ web server -- HTTP routes,
WSChannel and WebDAV on unmodified ESPAsyncWebServer -- so a browser can
use it exactly as it uses a controller.  A web page cannot listen on a
socket, so the sockets come from JS instead:

- BrowserAsyncTCP/: the AsyncTCP API that ESPAsyncWebServer builds on,
  over numbered virtual connections that JS opens, feeds and closes with
  fluidnc_vconn_open/send/close; output returns through
  self.fluidncOnVconnData/Close.  All callbacks run on one async_tcp
  thread, as on lwIP's task on ESP32.
- VirtualNet.cpp: the network module -- always up, at 192.168.0.1 (the
  access-point address) and <$Hostname>.local -- providing ESP800/ESP111
  and the $I and ESP420 network lines.
- env:wasm uses Arduino-Emulator, like the posix envs.  Library builds
  are switched to emcc too (previously only project sources were), FS is
  exported to JS, and wasm/IPAddress.h defers to Arduino-Emulator's.
- ESPAsyncWebServer's chunked in-flight pacing is off for this env: with
  instant acks it stalls responses whose source is briefly empty, which
  truncated /command replies such as [ESP400] (upstream fix proposed in
  ESP32Async/ESPAsyncWebServer#488).
- The postMessage WebUI bridge this replaces is removed: ShimChannel.cpp,
  wasm_fs_bridge.cpp and FwInfo.cpp.
- vconn_smoke_test.mjs: a headless Node test of HTTP, upload and
  WebSocket over virtual connections.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A page can't listen on a socket, so these carry a WebUI's traffic to the
web server inside the wasm module over its virtual connections.  A WebUI
loaded from FluidNC then talks ordinary HTTP and WebSocket, unmodified.

- vnet-sw.js: a Service Worker (scope device/) that relays every
  same-origin request from the device frame -- /, /command, /files,
  /upload, /sd/... -- to the demo page and streams the response back.  It
  decodes gzip (browsers don't decode a Response a worker builds), adds the
  COEP header the isolated page requires of a nested document, injects the
  WebSocket stand-in into HTML, and keeps redirects inside its scope.
- vnet-ws-shim.js: replaces window.WebSocket in the device frame, since
  Service Workers can't see WebSockets.  It treats the device's names and
  the page's own host (on any port) as the device, keeps same-device links
  and window.open inside the frame, and reports in-page URL changes.
- vnet-host.js: on the demo page, the HTTP/1.1 and WebSocket client over
  fluidnc_vconn_*, with a cookie jar (browsers drop Set-Cookie from
  worker-built responses), and helpers for device names and URLs.  Its
  createClient() core has no DOM dependencies.
- vnet_client_test.mjs: drives that client against the wasm server in
  Node -- gzip, cookies, chunked replies, uploads, the WebUI install
  sequence, and a WebSocket session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The demo now behaves like a board on the network.  A WebUI runs only by
browsing to FluidNC, which serves it -- or its built-in file manager --
from LocalFS; the demo needs no WebUI-specific code.

- An address bar preset to fluidnc.local, with Back/Forward/Reload:
  paths work as on a board (/?forcefallback=yes, /files?path=/,
  /command?plain=...), only <$Hostname>.local and the device's IP
  resolve, and other names, https: and other ports fail as a browser
  would report it.  ?browse=<address> opens the demo at an address, and a
  navigation that escapes the device frame is sent back into it.
- Install WebUI: fetches a project's latest GitHub release (through
  webui-proxy, which now returns the release's index.html.gz unchanged and
  which serve.py now also implements for local use) or a local build, and
  uploads it to LocalFS as index.html.gz through FluidNC's /files route,
  removing any old index.html/index.html.gz.  As after an upload to a
  board, the user browses to FluidNC to run it.
- The Upload/Download panel goes through FluidNC too: /files and /upload
  for uploads, their JSON for listings, WebDAV /flash and /sd for
  downloads.
- Files and NVS settings persist across reloads in IndexedDB (one saving
  tab, coordinated with Web Locks), seeded from seed-localfs/ and
  seed-sd/, with a button to reset to the built-in copies.

This supersedes #1900, whose persistence and seeding are included here;
its WebUI bridge features are replaced by the above.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- vnet-sw.js: never route a frame's requests to another tab's FluidNC.
  The frame-to-page mapping is lost when the browser stops the idle
  worker; it used to fall back to any demo page.  It now recovers by
  asking the frame for its page's token (vnet-ws-shim.js answers) and the
  pages which one owns it, and without an answer guesses only when a
  single demo page is open -- otherwise the request fails with 503.
- vnet-host.js: a Content-Length or chunked body cut short by the server
  closing is now an error, not a short success; only close-delimited
  bodies end at close.  A WebSocket closed while connect() is still
  waiting for the server no longer opens an upgrade afterwards.
- index.html: the WebUI installer checks each delete and fails if an old
  index.html survives, since FluidNC would keep serving it.
- IPAddress.h: comment named the old env:wasm_webui.
- vnet_unit_test.mjs: tests for the above against a fake module and fake
  browser clients (they fail on the previous code); the integration test
  also checks the delete replies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- vnet-host.js: the cookie jar now follows RFC 6265 path rules instead of
  keeping one cookie per name.  A cookie belongs to its Path (or the
  directory of the request that set it), is sent only on requests at or
  under that path, most specific first, and Max-Age=0 or a past Expires
  removes it.  FluidNC's sessionId is set for "/", and the HTTP/UIDir
  WebUIs of #1885 set a uiSession cookie for "/ui/<name>/" that must reach
  only that WebUI; sending one UI's uiSession everywhere defeated that
  isolation.
- BrowserAsyncTCP: keep the client and local addresses in network byte
  order, as lwIP does and IPAddress(uint32_t) expects; the log showed
  requests "from 1.0.0.127".
- vnet_unit_test.mjs: cookie-path tests (they fail on the previous jar).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1906 made WebUIServer and TelnetServer register mDNS services
unconditionally and added a no-op posix/Mdns.cpp so native builds still
link.  env:wasm takes only selected posix/ files, so it needs this one
too, or WebUIServer's Mdns::add/remove are undefined at link time.  (On a
base before #1906 the file doesn't exist yet and the filter line matches
nothing.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Service Worker served the device frame under device/, and the prefix
showed: a WebUI at /ui/webui2/ saw /device/ui/webui2/ as its own path.
WebUI 2's HTTP/UIDir support derives its base from location.pathname, so
it fell back to root-UI behavior and saved its preferences to LocalFS.
Navigations to a bare "/" also escaped the scope to the demo page.

The worker now covers the whole site and routes each request by who made
it: frame navigations and requests from nested windows go to FluidNC with
exactly their paths; the demo page, its workers and top-level tabs use the
network.  The device frame therefore sees the same URLs as on a
controller -- /, /ui/<name>/, /#/dashboard and the reload helper pages all
behave as they would on a board.  The frame guard and Location rewriting
are gone, the old device/-scoped registration is removed, and the demo
must be served from the root of its origin.

The virtual network's traffic is now logged to the DevTools console: a
line per HTTP exchange (method, path, status, bytes each way, time; the
demo page's own requests marked "(demo)") and per WebSocket open/close
with message and byte counts.  FluidNCVnet.logFrames = true adds each
WebSocket message; FluidNCVnet.log = false silences it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The demo's buttons, drop-downs and address bar get a larger
sans-serif font, padding and rounded corners: blue action buttons,
grey navigation buttons, and a red outline on Reset, whose
explanation moves into its tooltip.  The terminal and WebUI frames get
a heavier border.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Install WebUI to" chooses LocalFS index.html.gz, as before, or
/ui/<name> (HTTP/UIDir), with a name defaulting to the project's (figui,
webui2, webui3) or the local file's.  The latter installs through
FluidNC's HTTP interface as a browser on the LAN would: a WebDAV DELETE
of any old index.html, which would be served in preference, then
PUT /ui/<name>/index.html.gz, which creates the directory.  The name
localfs is FluidNC's for the LocalFS root, so it installs there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MitchBradley
MitchBradley merged commit 768b0c9 into main Oct 12, 2026
6 checks passed
@MitchBradley
MitchBradley deleted the WasmDemoWebServer branch October 12, 2026 06:06
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.

2 participants