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
Ambient drying: automatically dry filament on idle printers when
humidity exceeds threshold, regardless of queue state. Separate toggle
from queue auto-drying — both can run simultaneously. Uses the same
presets, humidity threshold, and power constraint detection.
Fix: block mode (wait for drying) previously skipped the humidity
auto-stop check for already-drying printers, causing drying to
continue indefinitely. Now only prevents starting new drying.
Queue auto-drying: scheduler automatically starts drying on idle printers
with scheduled queue prints when AMS humidity exceeds the configured
threshold. Uses conservative parameters (lowest temp, longest duration)
for mixed filaments. Drying stops when humidity drops below threshold
(30-minute minimum prevents oscillation), when scheduled items are
removed, or when the feature is disabled. Optional "block queue" mode
delays the next print until drying completes.
Configurable presets: temperature and duration per filament type,
editable in Settings → Print Queue, used by both manual drying popover
and queue auto-drying. Separate presets for AMS 2 Pro (n3f) and AMS-HT
(n3s) reflecting different heating capabilities.
PSU detection: drying button disabled with tooltip when dry_sf_reason
indicates insufficient power. Parses drying status bits and dry_sf_reason
from AMS info hex string via MQTT.
Backend: print_scheduler.py (+316 lines), bambu_mqtt.py, printer_manager,
schemas, settings route. Frontend: PrintersPage drying presets prop,
SettingsPage drying config UI, i18n (7 locales). Tests: 27 new tests in
test_scheduler_auto_drying.py covering conservative params, presets,
state sync, stop logic, minimum drying time, and auto-stop regressions.
Fix P2S camera stream dropping and snapshot capture race (#661)
P2S firmware's TLS renegotiation is rejected by Debian's hardened GnuTLS
defaults, causing ffmpeg RTSP sessions to drop after ~3 seconds. Add
GnuTLS config allowing unsafe renegotiation and legacy ciphers. Also add
ffmpeg fast-start flags, reduce reconnect delay from 1.0s to 0.2s,
remove double rate-limiting on external camera streams, and fix orphan
cleanup killing snapshot capture ffmpeg processes (exit code -9).
Or as a single combined commit:
Fix P2S camera streaming, snapshot race, and energy stats (#661, #695)
Camera: P2S firmware's TLS renegotiation rejected by Debian's hardened
GnuTLS defaults, dropping RTSP sessions after ~3s. Add GnuTLS compat
config, ffmpeg fast-start flags, reduce reconnect delay to 0.2s, remove
external camera double rate-limiting, and register snapshot ffmpeg PIDs
with the orphan tracker to prevent SIGKILL during capture.
Non-script HA entities (switch/light/input_boolean) on the printer card
always sent turn_on when clicked, which is a no-op when the switch is
already on. Now sends toggle for non-script entities so the badge click
actually changes the switch state. Script entities still use turn_on.
Also fix SmartPlugManager._get_plug_for_printer() using
scalar_one_or_none() which crashes with MultipleResultsFound when
multiple plugs are assigned to the same printer (e.g., Tasmota + HA
switch). Now fetches all plugs and returns the main non-script plug.
Add tests for both fixes: _get_plug_for_printer multi-plug handling
(5 tests) and canonicalFilamentType/filamentTypesCompatible/autoMatch
PA-CF equivalence (8 tests).
Bambu Lab firmware treats PA-CF, PA12-CF, and PAHT-CF as interchangeable,
but the print scheduler and filament override UI used exact string matching.
A 3MF requiring PA-CF wouldn't match PA12-CF in the AMS, leaving the
override dropdown disabled and preventing scheduler assignment.
Add a filament type equivalence system (both backend and frontend) so
these PA variants are treated as compatible in scheduler assignment,
AMS slot matching, force color match validation, and the override dropdown.
Also fix the "Force color match" checkbox click target being too wide
(entire row instead of just the checkbox/label) by changing flex to
inline-flex on the label element.
When all higher-priority slot-to-tray mapping methods failed (MQTT
mapping, print command mapping, queue mapping, color matching), the
internal usage tracker fell back to slot_id - 1 which can never reach
external spool IDs (254/255) or AMS-HT IDs (128+). Added position-based
resolution using sorted available tray IDs, matching the Spoolman fix
from PR #686.
Notify users when the first layer finishes printing so they can check
adhesion remotely. Triggers once per print when layer 2 begins
(layer_num >= 2, capped at <= 5 to handle reconnects). Includes a
camera snapshot attachment. Adds the on_first_layer_complete toggle
to all notification providers, with backend/frontend/i18n support
across all 7 locales.
Start, monitor, and stop drying sessions for AMS 2 Pro (n3f) and
AMS-HT (n3s) directly from the Printers page. Flame icon in AMS card
header opens a popover with filament-based temperature/duration presets
from BambuStudio. Live countdown status bar shows time remaining.
Backend: cache module_type from MQTT get_version, expose dry_time and
module_type in both WebSocket and REST paths, add supports_drying()
with firmware gating, add server-side guard on start_drying endpoint.
Frontend: drying button, popover with filament select + temp/duration
sliders, status bar, start/stop mutations. i18n for all 7 locales.
Supported: X1/X1C (fw 01.09+), P1P/P1S (fw 01.08+), H2D (fw 01.02.30+),
H2D Pro, X1E. Not supported: P2S, A1, A1 Mini, H2S, H2C.
Three bugs: (1) home_flag bit 18 is set on all printers regardless
of connection type, so every ethernet-capable model showed the badge
even on WiFi. Replaced with wifi_signal heuristic — ethernet printers
with WiFi disabled report a hardcoded -90 dBm sentinel. (2) The lazy
import used `from app.utils.printer_models` which crashes with
ModuleNotFoundError in paho-mqtt's background thread (correct path:
backend.app.utils.printer_models). This killed the thread, causing
all printers to go stale after 60s and disconnect/reconnect in a
loop. (3) WiFi-only models excluded via model-based gating.
home_flag bit 18 is set on all printers regardless of hardware,
causing the ethernet badge to appear on WiFi-only models (A1, P1P,
etc.). The previous fix removed the feature entirely, so ethernet
printers lost their badge too.
Now only trusts bit 18 on models with an ethernet port (X1C, X1E,
P1S, P2S, H2D, H2D Pro, H2C, H2S). WiFi-only models always show
the WiFi signal badge.