The daemon's self-update mechanism (git fetch/reset on its own code) was
fragile: .git permission errors, self-modifying code mid-run, hardcoded
main branch. Bambuddy now SSHes into the SpoolBuddy Pi and drives the
update remotely — matching its own branch, with step-by-step progress
via WebSocket. After updating the daemon, the kiosk browser is also
restarted so it loads the updated frontend.
SSH key pairing is automatic: Bambuddy generates an ED25519 keypair and
returns the public key in the registration response. The daemon deploys
it to authorized_keys on first connect — no manual setup needed.
Changes:
- New: backend/app/services/spoolbuddy_ssh.py
- Rewritten: trigger_daemon_update endpoint (SSH instead of pending_command)
- New: GET /spoolbuddy/ssh/public-key endpoint
- Auto SSH key deployment via registration response + daemon
- Removed: daemon _perform_update() and cmd=="update" handler
- Install script: bash shell, sudoers for daemon + kiosk restart, .ssh/ setup
- Dockerfile: added openssh-client
- Frontend: SSH key display, force update button
- Fixed: update check compares APP_VERSION, not GitHub releases
- Fixed: kiosk browser restart after update
The daemon's self-update mechanism (git fetch/reset on its own code) was
fragile: .git permission errors, self-modifying code mid-run, hardcoded
main branch. Bambuddy now SSHes into the SpoolBuddy Pi and drives the
update remotely — matching its own branch, with step-by-step progress
via WebSocket.
SSH key pairing is automatic: Bambuddy generates an ED25519 keypair and
returns the public key in the registration response. The daemon deploys
it to authorized_keys on first connect — no manual setup needed.
- New: backend/app/services/spoolbuddy_ssh.py (keypair, SSH commands, update orchestration)
- Rewritten: trigger_daemon_update endpoint uses SSH instead of pending_command
- New: GET /spoolbuddy/ssh/public-key endpoint for manual pairing
- Removed: daemon _perform_update() and cmd=="update" heartbeat handler
- Updated: install.sh — bash shell, sudoers for systemctl restart, .ssh/ setup
- Updated: Dockerfile — added openssh-client
- Updated: frontend — SSH key display, force update button
- Fixed: update check now compares against APP_VERSION, not GitHub releases
The daemon's self-update mechanism (git fetch/reset on its own code) was
fragile: .git permission errors, self-modifying code mid-run, hardcoded
main branch. Bambuddy now SSHes into the SpoolBuddy Pi and drives the
update remotely — matching its own branch, with step-by-step progress
via WebSocket. Install script updated with SSH access, sudoers entry,
and --ssh-pubkey flag for pairing.
The SpoolBuddy daemon update endpoint fetched GitHub releases and compared
them against device.firmware_version, which always showed "up to date"
because the comparison source was wrong. Now compares directly against
APP_VERSION from the running backend, so a daemon at 0.2.3b1 correctly
sees 0.2.3 as an available update.
BambuStudio connects to undocumented proprietary ports 2024-2026
on A1/P1S models during the print flow. The proxy wasn't forwarding
these ports, causing connection refused (RST) and triggering the
access code dialog instead of printing. MQTT and FTP worked fine —
port 2024 was the sole blocker.
Added transparent TCP pass-through proxies for ports 2024-2026,
following the same pattern as the existing FileTransfer (6000) and
RTSP (322) proxies. Silently ignored on models that don't use them.
Remove "Assign Spool" and "Configure" buttons from empty AMS slot
hover popups (standard AMS and HT AMS). Assigning a spool to a
physically empty slot created a stuck state — no unassign button
exists for empty slots, so the assignment couldn't be removed.
External spool holders are unaffected.
The OTA update feature added update_status and update_message to the
SpoolBuddyDevice model but no ALTER TABLE migration, causing
"no such column: spoolbuddy_devices.update_status" on existing databases.
The OTA update feature added update_status and update_message to the
SpoolBuddyDevice model but no ALTER TABLE migration, causing
"no such column: spoolbuddy_devices.update_status" on existing databases.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Main had VP #735 hotfix commits that were also on dev. All conflicts
resolved by keeping 0.2.2.1 (superset of main). Verified: 2098 backend
tests pass, frontend builds clean, no conflict markers remain.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pyOpenSSL 25.3.0 → 26.0.0 (CVE-2026-27448, CVE-2026-27459)
pyasn1 0.6.2 → 0.6.3 (CVE-2026-30922)
No breaking changes — Python 3.7 drop is irrelevant (we use 3.13),
cryptography >=46.0.0 requirement already satisfied (we have 46.0.5),
and we don't use set_tlsext_servername_callback (the behavioral change).
The SpoolBuddy layout now auto-checks for daemon updates every 5
minutes and shows "Update available: v{version}" in the status bar.
Removed the beta toggle since SpoolBuddy follows Bambuddy's release
channel. The daemon version is now read from backend APP_VERSION
instead of a stale hardcoded string.
The daemon had a hardcoded __version__ = "0.2.2b1" that was never
bumped, causing the update check to always show an update available.
Changed to read APP_VERSION from backend/app/core/config.py at import
time so the daemon version stays in sync automatically.
SpoolBuddy devices can now be updated from Settings → Updates without
SSH access. The daemon picks up an "update" command via its existing
heartbeat, runs git fetch/reset + pip install, reports progress back
to the backend, then exits for systemd to restart with the new code.
Backend: update_status/update_message fields, trigger + status endpoints
Daemon: _perform_update() handler, report_update_status() API method
Frontend: "Apply Update" button with live progress in UpdatesTab
When targeting a specific printer, the scheduler's power-on-wait loop
created new MQTT clients on each attempt. Each new client re-tried the
request topic subscription, which some brokers (e.g. A1) reject by
disconnecting. This caused a 170s thrash loop leaving the connection
fragile, so the eventual print command silently failed to reach the
printer.
Cache request topic support per serial number at the class level so
new client instances inherit the knowledge and skip the subscription.
Multi-plate 3MF files now support selecting a subset of plates to queue
via checkboxes, instead of the binary "one plate" or "all plates" toggle.
In add-to-queue mode, each plate gets a checkbox for multi-select with a
Select All / Deselect All toggle. Reprint and edit modes remain single-select.
The saved preset bypassed the search filter entirely, so when a slot's
DB mapping was stale (e.g. previously Matte, now physically Silk), the
old preset always appeared regardless of search query. Remove the search
bypass — saved/current presets still bypass the printer model filter but
must match the search text like all other presets.
Add visual indicators so printers with HMS errors stand out in large
print farms:
- Red "Problem" counter in the status summary bar
- Status pip turns red (fatal/serious) or amber (warning) for HMS errors
- Progress bar turns amber when a print is paused
- Status sort prioritizes printers with HMS errors at the top
When a printer shuts down it sends a final MQTT message with
tray_exist_bits=0 and power_on_flag=false. The tray_exist_bits
clearing code processed this all-zero value, wiping every AMS
slot's filament data. On reconnect, the auto-unlink check saw
empty tray data (no color, no type) and deleted all spool
assignments as "fingerprint mismatch".
Fix: skip tray_exist_bits slot clearing when power_on_flag is
false. Defaults to true when absent for backwards compatibility.
Adds 3 regression tests covering shutdown preservation, genuine
removal still working, and missing power_on_flag fallback.
Replace fixed 30-second debug log collection with an interactive
3-step flow: start logging, reproduce the issue, stop & submit.
Users now control timing instead of racing a countdown.
Backend: split _collect_debug_logs() into POST /start-logging and
POST /stop-logging endpoints; add debug_logs field to submit request.
Frontend: 3-step progress indicator with elapsed timer, pulsing
active state, and 5-minute auto-stop. Updated all 7 locale files.
Add a "Rotate spool during drying" checkbox to the manual drying popover
for AMS 2 Pro and AMS-HT units. The firmware-level rotate_tray field was
already sent (hardcoded to false) — this makes it user-configurable.
The checkbox defaults to unchecked and resets each time the popover opens.
Firmware silently disables rotation if filament is currently loaded.
When all matching printers were busy and a job was queued with ASAP
timing, the scheduler immediately fired a "Job Waiting for Filament"
notification even though the job was just waiting for a printer to
finish — no user action required.
Added _is_busy_only() check to skip the waiting notification when the
only reason is "Busy". Notifications still fire for actionable reasons
(missing filament, offline, wrong color). Also renamed the default
notification template title to "Queue Job Waiting" and updated
descriptions across all 7 locales.
Renaming a file in the File Manager included the extension in the
editable text, letting users accidentally strip it and break the file.
The rename modal now separates the base name from the extension
(.3mf, .gcode, .gcode.3mf), showing the extension as a non-editable
gray suffix and re-appending it on save.
A1/A1 Mini printers fail to connect through VP proxy mode while
X1C/P2S/H2C work fine with identical transparent TCP proxy code.
Root cause unknown — the proxy is model-agnostic, so the failure
suggests BambuStudio uses a different connection flow for A1 models
that hits ports we don't proxy.
Add diagnostic probe listeners on ports 21, 80, and 443 on each
proxy VP's dedicated bind IP. If the slicer connects to any of
these un-proxied ports, a WARNING is logged so debug logs and
tcpdump can reveal what's missing.
The closed-source bambu_networking DLL validates TLS connection parameters
and rejects connections where the certificate doesn't match the printer's
real BBL CA certificate. The TLS-terminating proxy presented Bambuddy's
own certificate, causing X1C/X1 prints to silently fail after verify_job.
Switch to transparent TCP proxying for FTP, FileTransfer, Camera, and FTP
data — only MQTT remains TLS-terminated (required for IP rewriting). The
slicer now gets end-to-end TLS directly with the printer's real certificate.
Changes:
- SlicerProxyManager uses TCPProxy for FTP (990), FileTransfer (6000),
Camera (322), and pre-listens on FTP data ports (50000-50100)
- Only MQTT (8883) uses TLSProxy for IP rewriting
- Remove debug logging from MQTT and FTP proxy code
- Fix install.sh missing AmbientCapabilities=CAP_NET_BIND_SERVICE
- Update module docstring, migration docs, README proxy description
- Add tests verifying transparent proxy architecture
- Add ports 6000 (file transfer) and 322 (RTSP camera) to Dockerfile
EXPOSE and docker-compose.yml bridge mode port mapping
- Update migration doc with new proxy mode port requirements
- Regenerate proxy-mode-diagram.png with all proxied ports
When the slicer and printer are on different VLANs, Bambu Studio could
not send prints through the proxy because the printer's real IP leaked
through MQTT payloads, the bind protocol forwarded the real printer's
identity, file transfer and camera ports were not proxied, and FTP
data connections raced the TLS handshake on zero-byte uploads.
- Rewrite IP addresses in MQTT PUBLISH payloads (string + integer)
with proper packet framing and cross-chunk buffering
- Respond to bind/detect with VP identity via BindServer
- Add TLS proxies for port 6000 (file transfer) and 322 (RTSP camera)
- Buffer slicer FTP data during printer connection setup
- Advertise configured VP name in SSDP proxy
- Add cross-subnet SSDP wildcard listener for VPN setups
- Register UserEmailPreference model in models/__init__.py
- Add 11 unit tests for MQTT rewrite, IP conversion, SSDP name
When the slicer and printer are on different VLANs, Bambu Studio could
not send prints through the proxy because the printer's real IP leaked
through MQTT payloads, the bind protocol forwarded the real printer's
identity, the port 6000 file transfer tunnel was not proxied, and FTP
data connections raced the TLS handshake on zero-byte uploads.
- Rewrite IP addresses in MQTT PUBLISH payloads (string + integer)
with proper packet framing and cross-chunk buffering
- Respond to bind/detect with VP identity via BindServer
- Add TLS proxy for port 6000 (file transfer tunnel)
- Buffer slicer FTP data during printer connection setup
- Advertise configured VP name in SSDP proxy
- Add cross-subnet SSDP wildcard listener for VPN setups
- Register UserEmailPreference model in models/__init__.py
- Add 11 unit tests for MQTT rewrite, IP conversion, SSDP name
Carbon rods use plain bearings — lubricating them degrades print quality.
Removed the lubrication task from defaults; only "Clean Carbon Rods"
remains. Existing entries are auto-removed on next startup via
ensure_default_types(). Updated wiki link mapping and tests.
When searching for a non-existent profile in the AMS slot config modal,
presets matching the current slot's tray_info_idx bypassed the search
filter, showing e.g. all "Generic PLA" variants instead of no results.
Narrow the search bypass to only the exact saved preset — the broader
trayIdx match still bypasses the model filter as intended.
After sending a print command via MQTT, monitor whether the printer's
gcode_state changes within 15 seconds. If not, log a warning visible in
support packages. Addresses silent command drops observed on P1S firmware
01.09.01.00 where the printer ignores project_file commands while
continuing to send status updates.
Add a speed control badge to the printer monitoring card controls row
that lets users switch between Silent (50%), Standard (100%), Sport
(124%), and Ludicrous (166%) presets during active prints. The badge
displays a gauge icon with the current speed percentage, always visible
but disabled when idle. Includes backend endpoint, optimistic UI
updates, i18n for all 7 locales, and full test coverage.
Add a "Spool" column to the filament inventory table that displays
the spool catalog entry name associated with each spool. The column
is hidden by default and can be enabled via the column visibility
menu. Also add a spool name filter dropdown next to the brand filter,
shown when any spools have catalog entries assigned. No backend
changes needed — catalog data is fetched and joined on the frontend.
Ntfy notifications with camera snapshots failed when the printer name
or filename contained non-ASCII characters. httpx enforces ASCII
encoding on string header values, but the Title and Message headers
can contain printer names with accented letters or CJK characters.
Encode these header values as UTF-8 bytes, which ntfy handles correctly.
Test notifications were unaffected because they use a hardcoded ASCII
title and no image attachment.
When running multiple virtual printers with different access codes on
separate bind IPs, FTP connections were always routed to the wrong VP.
Root cause: the iptables REDIRECT rule (990→9990) rewrites the
destination IP to the incoming interface's primary address. With Linux's
weak host model (arp_filter=0), packets for secondary IPs arrive on the
primary interface, and REDIRECT sends them all to the first VP's FTP
server. MQTT was unaffected because port 8883 had no redirect.
Fix: FTP server now binds directly to port 990 (standard implicit FTPS),
eliminating the iptables redirect entirely. Requires CAP_NET_BIND_SERVICE
(already set in the systemd service file and Docker image).
Also removed a global asyncio set_exception_handler() in the MQTT server
that was overwritten by each VP instance, causing spurious "Unhandled
exception in client_connected_cb" errors on startup.
Changes:
- FTP_PORT: 9990 → 990 (ftp_server.py)
- Removed set_exception_handler() from MQTT server
- Updated Dockerfile, docker-compose.yml port mappings
- Deprecated --redirect-990 in install script
- Updated wiki: removed iptables instructions for all platforms
- Added migration guide (docs/migration-vp-ftp-port.md)
- Added unit tests for port constant and no-global-state invariant
When running multiple virtual printers with different access codes on
separate bind IPs, FTP connections were always routed to the wrong VP.
Root cause: the iptables REDIRECT rule (990→9990) rewrites the
destination IP to the incoming interface's primary address. With Linux's
weak host model (arp_filter=0), packets for secondary IPs arrive on the
primary interface, and REDIRECT sends them all to the first VP's FTP
server. MQTT was unaffected because port 8883 had no redirect.
Fix: FTP server now binds directly to port 990 (standard implicit FTPS),
eliminating the iptables redirect entirely. Requires CAP_NET_BIND_SERVICE
(already set in the systemd service file and Docker image).
Also removed a global asyncio set_exception_handler() in the MQTT server
that was overwritten by each VP instance, causing spurious "Unhandled
exception in client_connected_cb" errors on startup.
Changes:
- FTP_PORT: 9990 → 990 (ftp_server.py)
- Removed set_exception_handler() from MQTT server
- Updated Dockerfile, docker-compose.yml port mappings
- Deprecated --redirect-990 in install script
- Updated wiki: removed iptables instructions for all platforms
- Added migration guide (docs/migration-vp-ftp-port.md)
- Added unit tests for port constant and no-global-state invariant