The archives page only fetched the 50 most recent prints due to a
hardcoded API limit default. Added client-side pagination with
configurable page sizes (25/50/100/200/All) and bumped the fetch
limit to return all archives. Page size is persisted to localStorage.
Corrected left/right nozzle labels in ams_mapping2 comment
(255=main/right, 254=deputy/left). Added [0.2.2.2] release header
and #836 changelog entry noting the nozzle routing fix was already
included via the #797 ams_mapping2 format change.
Replace Chromium with cog (WPE WebKit) for the kiosk browser. Cog is
purpose-built for embedded kiosk displays with a fraction of Chromium's
CPU and memory footprint on Pi hardware.
Add React Query `select` to SpoolBuddyLayout and SpoolBuddyDashboard
printer status queries so only `connected` is extracted. Temperature,
fan, and progress changes no longer trigger re-renders on every MQTT
tick.
Expand service/package stripping to disable pipewire audio stack, CUPS
printing, rpcbind, upower, polkit, accounts-daemon, xdg-desktop-portal,
and mpris-proxy. Add user-level service masking for pipewire/portals.
Update SSH update cache clearing to handle both WPE WebKit and legacy
Chromium cache paths.
Filenames with spaces (e.g. "Junktion Box PRO 90.3mf") broke the MQTT
print command because the url field (ftp://file name.3mf) contained
unencoded spaces the firmware can't parse. FTP upload succeeded but the
printer silently ignored the command and stayed IDLE. Replace spaces
with underscores in the remote filename before upload and print command.
Auto-off now has a "Keep Enabled" toggle that keeps it active between
prints instead of disabling after each use (one-shot). Useful for HA
accessories like BentoBox filters that should always power off after
prints. Default behavior (one-shot) is unchanged.
The stale connection fix force-closed the socket on every frontend
status poll, causing a tight close→reconnect→stale→close loop. Added
a 30-second cooldown between stale reconnect attempts so paho has time
to re-establish the connection. Also added reconnect backoff (1-30s)
and unique-per-process MQTT client IDs to prevent broker session
takeovers.
The stale connection fix (6ac375a) force-closed the socket to trigger
paho reconnect, but paho's disconnect callback redundantly broadcast
state changes, causing rapid connected/disconnected cycling in the UI.
Now sets a flag before the force-close so _on_disconnect skips the
redundant broadcast. Also relaxed keepalive from 15s to 30s — the
aggressive interval caused spurious disconnects on transient hiccups.
Moved _disconnection_event.set() to the top of _on_disconnect so no
early-return filter can prevent it from unblocking disconnect() callers.
Removed leftover debug payload dump.
Some P1S/P1P firmware reports stg_cur=0 when idle, which maps to
the "Printing" stage name and overrides the correct IDLE gcode_state
on the printer card. Extend the existing A1/A1 Mini stg_cur idle bug
workaround to also cover P1S and P1P models.
Two issues prevented recovery from stale printer MQTT connections:
1. The spurious-disconnect filter (30s window) could suppress real
keepalive-timeout disconnects (which fire at ~22.5s with keepalive=15s).
Now checks rc.is_failure so error disconnects are never suppressed,
and tightens the window from 30s to 10s.
2. Stale detection (60s no messages) set state.connected=False but left
the TCP socket open, so paho kept publishing into the void. Now
force-closes the socket so paho's loop thread detects the break and
auto-reconnects.
When a printer enters FINISH/FAILED without Bambuddy ever seeing
RUNNING (e.g. after a restart), mark _completion_triggered = True
so the flag is clean for the next print cycle.
BambuStudio converts virtual tray IDs (254/255) to -1 in the flat
ams_mapping and relies on ams_mapping2 for external spool routing.
Bambuddy was passing raw 254/255 in the flat array, which H2D firmware
rejects with "Failed to get AMS mapping table".
- Convert external tray IDs to -1 in flat ams_mapping (match BambuStudio)
- Fix ams_mapping2 for external trays: each virtual tray is its own AMS
unit with slot_id 0, not a shared unit differentiated by slot
- Fix main/deputy nozzle ID comment (255=main, 254=deputy per BambuStudio)
- Add 8 unit tests for start_print() mapping construction
Some printers (e.g. H2D) only send {id, state} in incremental MQTT
tray updates — no tray_type, tray_color, or other fields. When
filament is unloaded (state changes from 11 to 10), the old tray
data persisted indefinitely because the merge logic only updates
fields present in the incoming message.
Backend:
- Detect tray state != 11 without tray_type in incremental updates
and clear stale tray data (bambu_mqtt.py)
- Expose tray `state` field via REST API and WebSocket broadcasts
(printer.py schema, printers.py route, printer_manager.py)
- Include AMS tray content in WebSocket dedup key so load/unload
transitions trigger broadcasts (main.py)
Frontend:
- Add `state` to AMSTray interface (client.ts)
- Show configure/assign buttons for state=10 (spool present, not
loaded) but hide for state=9 (truly empty) on AMS/HT slots
- Hide fill level bar on empty slots
Tests:
- 5 new tests covering state-based clearing, preservation, reload,
and idempotency
When a Bambu Lab spool is detected in the AMS but no tag match exists,
check for an untagged inventory spool with matching material/subtype/color
before creating a new entry. Links the RFID tag to the existing spool
(data_origin="rfid_linked") to prevent duplicate inventory entries.
Diagnostic log added in 0.2.2.1 fired on every MQTT update while a
printer was in FINISH/FAILED state, not just on the state transition.
For farms with multiple idle printers, this produced thousands of log
lines per minute. Guard the log to only fire once when the state first
changes to FINISH/FAILED.
The kiosk touchscreen has no way to hard-refresh, and the service worker
served stale cached JS after updates. SpoolBuddy pages now unregister
any existing SW and skip registration entirely. Regular desktop/mobile
users still get the SW. Restored kiosk restart in SSH update flow since
SW is no longer an obstacle.
Check button had no visual feedback because isFetching doesn't trigger
reliably with cached data — replaced with manual loading state. Removed
kiosk restart via getty; the frontend now detects daemon re-registration
via WebSocket and calls window.location.reload(), keeping the user on
the same page and fetching all fresh data.
The SSH update set status to "complete" after the daemon had already
restarted and re-registered, overwriting the cleared state so it stuck
forever. Removed the post-restart "complete" write — daemon
re-registration is now the completion signal, clearing any update status.
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. Install script updated with SSH access, sudoers entry,
and --ssh-pubkey flag for pairing.
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.
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.
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.
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.
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
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
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.
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
FTP PASS commands were logged with the plaintext password visible in
log files. Since support packages include logs and are shared publicly
on GitHub issues, this exposed user access codes. Now redacted as
PASS ********.
Print complete notifications were chained behind the finish photo
capture task with no timeout. If photo capture hung, the notification
would never send. Added a 45-second timeout so notifications always
fire regardless of photo outcome.
Also added diagnostic logging to MQTT state detection and upgraded
notification error logging to include stack traces for easier
debugging.
X1C and X1 virtual printers used legacy SSDP model codes
(3DPrinter-X1-Carbon, 3DPrinter-X1) that BambuStudio doesn't
recognize, causing "incompatible printer preset" errors when
sending prints. Changed to the correct codes (BL-P001, BL-P002)
that real printers report via SSDP.
Also fixed proxy mode auto-inherit storing printer display names
(e.g. "X1C") instead of SSDP codes, by adding a resolution layer
that maps display names to model codes.
DB migration auto-converts existing VPs on startup.
Add tests, docs, and ruff fixes for per-user email notifications
- Fix missing timezone import in email_service.py (F821)
- Fix unused lambda arg in main.py asyncio done_callback (ARG005)
- Fix E302 blank line spacing for mark_printer_stopped_by_user
- Fix F821/UP037 forward reference in user_email_pref model
- Fix SettingsPage test for duplicate "Notifications" text
- Add backend unit tests for permissions, schemas, and templates
- Add backend integration tests for user-notifications API
- Add frontend tests for NotificationsPage
- Add user_email_pref model import to test conftest
- Update CHANGELOG and README
Webhook providers did not include image data (e.g. camera snapshots
from first layer complete notifications) even though other providers
like Telegram, Pushover, and Discord already attached them. The webhook
payload now includes a base64-encoded "image" field when a snapshot is
available (generic format only, excluded from Slack format).
The Debian ffmpeg package uses GnuTLS, whose hardened defaults reject
TLS renegotiation and legacy ciphers that some Bambu printer firmwares
(notably P2S) rely on — causing RTSP sessions to drop after a few
seconds.
Add a local TLS termination proxy (Python ssl/OpenSSL) that handles
the TLS connection to the printer and exposes a plain RTSP port to
ffmpeg. The proxy rewrites RTSP request-line URLs (rtsp://proxy →
rtsps://printer) while preserving Authorization headers so Digest
auth hashes remain valid.
Also:
- Reduce RTSP reconnect delay from 1.0s to 0.2s
- Add ffmpeg fast-start flags (-probesize 32, -analyzeduration 0,
-fflags nobuffer, -flags low_delay)
- Fix external camera double rate-limiting causing choppy streams
- Apply TLS proxy to external camera rtsps:// URLs and snapshot capture
- Update orphan ffmpeg cleanup to match rtsp:// (proxied) URLs
- Add unit tests for RTSP URL rewriting and proxy lifecycle