Sliced files previewed through a vendored copy of PrettyGCode in an
iframe. It drew each move as a screen-space line -- a line has no
thickness in the scene, so it cannot occlude the layer behind it, which
is why prints came out stringy and shimmered where layers crossed. Being
a separate app in a frame, it could be neither themed nor translated, and
carried its own machinery for detecting a proxy refusing the embed.
Now built on libvgcode, the renderer OrcaSlicer draws its own preview
with, vendored from three-slicer (AGPL, same as us). It takes the THREE
namespace as an argument and imports nothing, so it runs on our 0.181
rather than the 0.160 its package pins.
The parser is ours; upstream renders its own kernel's output and ships no
G-code parser at all. Two things it has to get right, both found by
checking a real plate rather than assuming:
- BambuStudio does not use the OrcaSlicer/PrusaSlicer annotations. It
writes "; FEATURE:", "; LINE_WIDTH:", "; CHANGE_LAYER" and
"; Z_HEIGHT:", not ";TYPE:", ";WIDTH:" and ";LAYER_CHANGE". Reading
only the latter showed a 52-layer print as 23,165 layers in one colour,
because with no layer marker recognised every travel Z-hop split a
layer and every segment took the fallback feature.
- It emits a tenth of its moves as G2/G3 arcs -- 706 extruding ones in a
single plate. Ignoring them punched holes through curved walls and tree
supports. Arcs with no X/Y are the helical travel lift and lay down
nothing, so they interpolate as travels.
Four colour modes: filament (default, from the AMS slots the file was
sliced with), feature, layer height, line width. Speed, fan and
temperature are deliberately absent -- upstream derives those from
settings rather than the toolpath, and guesses dressed as measurements
are worse than an honest omission. The parser now carries the data to do
them properly later.
Legend entries are switches. Hiding removes the records before the mesh
is built rather than recolouring them: the shader packs colour into a
single float with no alpha, so there is no transparent to set, and
removal is the useful behaviour anyway -- a hidden support stops
occluding what it covered.
The scene is built once and only the toolpath rebuilds. Doing otherwise
constructed a new WebGLRenderer on every render, because the buildVolume
default is an object literal and so a fresh identity each time; browsers
cap live WebGL contexts and drop the oldest, which blanked the canvas
after a few interactions.
utils/framing.ts goes with the iframe, along with six now-orphaned
strings in all 13 locales. src/lib/vendor is excluded from eslint --
acting on findings in vendored code makes it impossible to re-copy on the
next upstream release.
Two defects, both invisible until you ask the app to stop.
Docker never shut down gracefully at all. CMD ["sh","-c","uvicorn ..."] left
the shell as PID 1 with uvicorn as its child, and dash does not forward
signals, so docker stop SIGTERMed the shell and uvicorn never heard about it.
Measured on the shipped image: the full 10s grace period, exit 137, and no
"Shutting down" line in the log. Every stop, restart and image update was a
hard kill -- no WAL checkpoint, no MQTT disconnect, no virtual-printer
teardown. `exec` makes uvicorn PID 1; the rebuilt image now stops in 1s with
exit 0 and checkpoints the WAL.
Separately, uvicorn's timeout_graceful_shutdown defaults to None -- wait
forever for in-flight requests. An MJPEG camera stream is a response that
never completes (httptools' connection shutdown() only flips keep_alive on an
in-flight cycle, it never closes the transport), so one open camera tile
pinned the process until systemd SIGKILLed at 90s. The ordering makes it
unfixable from inside the app: uvicorn fires the lifespan shutdown -- the code
that tears the streams down -- only after connections drain.
All six launchers now pass --timeout-graceful-shutdown 5: Dockerfile,
deploy/bambuddy.service, the systemd unit and launchd plist from
install/install.sh, the SpoolBuddy installer's unit, and the Windows NSSM
registration. On timeout uvicorn cancels the request tasks; the camera
generators already unwind cleanly on CancelledError.
TimeoutStopSec raised to 30s on the units and stop_grace_period: 30s added to
compose, as backstops rather than the mechanism. On Windows NSSM's default
1500ms AppStopMethodConsole was force-killing uvicorn mid-teardown; raised to
15s, with the WM_CLOSE and thread-message stages skipped (uvicorn is a console
app with neither a window nor a message loop).
A clean Windows 10 install crashed on startup: init_db() -> SQLAlchemy async
engine -> greenlet failed with "DLL load failed while importing _greenlet:
The specified module could not be found", so uvicorn never bound :8000 and the
dashboard refused all connections while the NSSM service still showed running.
greenlet's _greenlet.pyd is C++ and needs vcruntime140_1.dll, which the
python.org embeddable distribution does not ship (it includes only
vcruntime140.dll, enough for the pure-C python313.dll). Machines with the VC++
2015-2022 redistributable already installed have the DLL in System32, which
masked the bug in testing.
Stage vcruntime140_1.dll and msvcp140.dll next to python.exe at build time,
from a vendored copy or the runner's System32, failing loudly if absent. The
Inno Setup [Files] step already copies staging\python\* recursively.
Native (non-Docker) installs launched uvicorn without --loop asyncio, so
uvicorn[standard] auto-selected uvloop. uvloop's SSL layer drops
already-received but still-buffered data when the client closes the data
connection without a TLS close_notify while the reader is flow-control
paused on slow storage. cmd_STOR writes each chunk to disk inside the read
loop, so a slow consumer falls behind, the tail is lost, read() returns a
clean EOF, and the loop exits with no exception -- the server acked 226 for
a file it truncated itself, then archived, queued, and forwarded the corrupt
3MF to the real printer.
Fix in two independent layers:
1. Remove the trigger: add --loop asyncio to every native launch path,
matching the Dockerfile -- deploy/bambuddy.service, install/install.sh
(systemd + launchd), spoolbuddy/install/install.sh, the Windows NSSM
service, README, CONTRIBUTING dev command.
2. Defense in depth (loop-independent): cmd_STOR now validates that a
received .3mf opens as a ZIP (reads the central directory, no
decompression) before replying 226. A truncated/corrupt file is dropped
and answered with 426, and on_file_received never runs -- so a broken
upload surfaces as an immediate slicer-side send error instead of being
archived and pushed to the printer. Scoped to .3mf; other filetypes pass
through unchanged.
When the Windows build is triggered by a tag push (the path
docker-publish.sh and docker-publish-daily-beta.sh both take), use
the tag as the installer version instead of APP_VERSION. So a daily
tag v0.2.5b1-daily.20260610 produces
bambuddy-0.2.5b1-daily.20260610-windows-x64-setup.exe
matching the docker `daily` image and the GitHub prerelease. The
stable docker-publish.sh path is unchanged (tag v0.2.5b1 →
bambuddy-0.2.5b1-windows-x64-setup.exe). Manual workflow_dispatch
and local builds fall back to APP_VERSION as before.
Silences Inno Setup's "missing RunOnceId" warning. Both commands
(stop service, remove firewall rule) are already idempotent, so the
behavioural effect is nil — this is purely a cleanliness fix to keep
the build log warning-free.
nssm.cc returned 503 mid-CI-run, breaking the staging step. NSSM 2.24
hasn't shipped a new release since 2014, so the binary is effectively
static — vendoring under installers/windows/vendor/nssm.exe makes
builds reproducible and removes the only single-source download in
the pipeline. SHA-256 pinned in the comment in build.py for audit.
ffmpeg stays fetched from BtbN's GitHub mirror — it's actively
maintained and large (~80MB) so vendoring it would be unreasonable;
GitHub Releases are also far more reliable than nssm.cc.
Upgrading over a running install was failing with permission-denied
errors on python.exe / .pyd / nssm.exe — the service held file locks
during the [Files] copy phase. Add a PrepareToInstall hook that
detects an existing install ({app}\bin\nssm.exe present) and stops
the service before the overwrite. FileExists guards a no-op on
first-time installs; the post-install [Run] step re-registers and
starts the service fresh either way.
- build.py now reads APP_VERSION from backend/app/core/config.py
(the canonical source used everywhere else — /system, support
bundles, FastAPI title) instead of pyproject.toml's stale 0.1.5.
Next installer is bambuddy-0.2.5b1-windows-x64-setup.exe.
- installers/windows/bambuddy.ico is a multi-resolution .ico
(16/32/48/64/128/256) generated from frontend/public/img/
favicon.png. Wired into SetupIconFile (installer .exe icon),
UninstallDisplayIcon (Add/Remove Programs), and the Start Menu /
desktop shortcuts — replaces the NSSM placeholder everywhere a
user sees Bambuddy on Windows.
vite.config.ts sets outDir to '../static' so the bundle lands at
<repo>/static/ — not frontend/dist/. Matches the runtime expectation
in config.py where static_dir = _app_dir / "static".
Also stage gcode_viewer/ next to static/ so the 3D preview iframe
routes in main.py (resolved via static_dir.parent / "gcode_viewer")
find their assets.
Strip macOS metadata files (.DS_Store, ._.*) from both copies — they
leak in from dev boxes and would only bloat the installer.
get-pip.py installs only pip itself; the embedded Python distribution
ships without setuptools or wheel. pip needs setuptools.build_meta as
the PEP 517 build backend for any sdist-only package — Bambuddy's
requirements.txt hits this on pyftpdlib 2.2.0 (sdist-only on PyPI).
Install both right after the pip bootstrap so requirements.txt installs
cleanly.
Lays down the Inno Setup + embedded Python pipeline for producing a
self-contained Bambuddy Windows installer .exe. The installer ships
an embedded Python 3.13, the pre-built React bundle, NSSM (service
supervisor) and ffmpeg — no host Python or Node required on the
target machine.
Architecture:
- Install: C:\Program Files\Bambuddy (admin install, one-time UAC)
- Data: C:\ProgramData\Bambuddy\data (preserved on uninstall)
- Service: registered via NSSM, runs as LocalSystem, autostart on boot
- UI: browser at http://localhost:8000 (Start Menu shortcut)
Files:
- installers/windows/build.py stages embedded Python + deps,
frontend bundle, NSSM, ffmpeg
- installers/windows/bambuddy.iss Inno Setup compiler script
- installers/windows/service/*.bat NSSM register/deregister
- .github/workflows/windows-installer.yml CI build on tag push + manual
dispatch, uploads .exe artifact
build.py hard-fails on non-Windows hosts; Wine cross-build is an
unsupported escape hatch behind --allow-non-windows. v1 ships unsigned
(SmartScreen warns on first run) — production signing will be wired up
via SignPath OSS once the application is approved.
See installers/windows/README.md for build prerequisites and the
embedded-Python ._pth gotchas.