Commit Graph
21 Commits
Author SHA1 Message Date
maziggy 4ffefa60f4 chore(vp): per-minute MQTT status-push counter for idle-disconnect triage (#1548 follow-up)
The 1 Hz status push was silent at INFO, so support bundles couldn't show
  whether the push task was actually reaching a specific slicer connection.
  Now ``_periodic_status_push`` emits one line per minute per connected
  slicer ("1Hz status push: N pushes/min to <client>") and stays silent when
  no slicer is attached. No behaviour change to the push itself — counters
  are local to the task and reset every 60 ticks.

  Motivated by the open follow-up on #1548: keepalive parser shipped (b6636053)
  and the symptom moved 60 s → 90 s, but OrcaSlicer still disconnects on idle.
  This adds the missing observability so the reporter's next support bundle
  shows whether our outbound 1 Hz push is alive for the disconnecting
  connection.
2026-05-30 14:47:06 +02:00
maziggy 597762685c fix(virtual-printer): #1558 Send pre-flight + slicer-surface audit bundle
#1558: cached-as-base push_status only forced gcode_state=IDLE while letting
  the real printer's live-progress fields (mc_percent, stg_cur, layer_num, ...)
  leak through. Bambu Studio's Send pre-flight read them as busy and refused.
  The cached branch now overrides the activity-field set the same way it
  already overrode storage indicators (#1228) and protocol fields.

  Same bundle ships a multi-round VP audit that found adjacent bugs in the
  same family:

  - #1558: cached branch zeroes mc_print_stage / mc_percent / mc_remaining_time / stg / stg_cur / layer_num / total_layer_num / print_error
  - MQTT auth: per-IP rate-limit (5/60s lockout), hmac.compare_digest, access_code redacted in DEBUG log
  - FTP cmd_STOR streams chunks to disk + 4 GiB cap (was buffering whole upload)
  - Sticky-keys allowlist extended with upgrade_state / xcam / hw_switch_state / nozzle_diameter / nozzle_type / online / ams_status
  - _pending_files cleanup in finally for archive / queue / dispatch handlers
  - _add_to_print_queue position uses MAX+1 (was hardcoded 1)
  - DELETE VP removes orphan PendingUpload rows + upload_dir from disk
  - Per-VP cert regenerates on shared-CA rotation (real signature verification, not DN match)
  - DHCP target-IP refresh + queue_force_color_match toggle now restart proxy VPs
  - Per-slicer bridge-response routing (multi-slicer cross-leak fix via sequence_id map)
  - Child-service readiness barrier (FTP / MQTT / Bind / SSDP) — no false is_running before sockets bind
  - H2D Pro O1E / O2D model codes added (experimental, needs field confirmation)
  - FTP passive port range widened 50000-51000; docker-compose + wiki updated
  - VP refresh_loop crash now unbinds raw_message_handler; tailscale catches asyncio.TimeoutError; SlicerProxyManager lifecycle hardening
2026-05-30 13:34:10 +02:00
maziggy b663605318 fix(virtual-printer): honour client-negotiated MQTT keepalive instead of hardcoded 60s (#1548)
OrcaSlicer connects, exchanges pushall + get_version, then sits idle waiting
  for status pushes from the (virtual) printer. The VP MQTT server's read
  loop used `asyncio.wait_for(reader.read(1), timeout=60)` regardless of what
  the client negotiated, and `_handle_connect` explicitly skipped the
  keepalive field in the CONNECT payload, so every idle slicer connection was
  torn down at exactly 60s.

  - Parse the 2-byte big-endian keepalive from CONNECT; return it from
    _handle_connect alongside the auth bool.
  - Use 1.5x the negotiated keepalive as the per-packet read timeout per
    MQTT spec sec 4.4. Treat keep_alive == 0 as no timeout (spec sec 3.1.2.10).
  - Retain the 60s default for the initial read before CONNECT arrives, so
    a TCP-connect-without-CONNECT still gets reaped.
  - 7 new tests: 4 unit-level for the parser (success, opt-out=0, auth-fail
    tuple shape, malformed CONNECT) + 3 integration-style for the read loop
    (long keepalive survives the old 60s mark, short keepalive closes idle
    in ~3s, PINGREQ resets the window so DISCONNECT decides the exit).
2026-05-28 09:13:44 +02:00
maziggy ceffcfaef6 fix(vp): overlay storage indicators on cached push so slicer pre-flight passes for P1S/A1 targets (issue #1228)
Slicer "Send to printer" worked on 0.2.3.2 with a queue-mode VP and
  started failing on 0.2.4b3 with BambuStudio's generic "storage needs
  to be inserted before send to printer" error. Multiple users
  reported it across P1S, P2S, Docker bridge, macvlan, and host
  networking. @rtadams89's debug-level support archive showed the
  smoking gun: slicer establishes MQTT TLS, gets pushall +
  get_version, then never opens an FTP connection — pre-flight
  rejects before any data transfer.

  The 0.2.3.2 synthetic stub baked in three SD/storage indicators
  that BambuStudio's "Send" pre-flight reads: home_flag with bit 8
  (HAS_SDCARD_NORMAL, 0x100), sdcard=True, and a storage:{free,total}
  block. The 0.2.4b3 cached-as-base slicer-mirror (7dea33d0) passes
  the live target's push_status through with only an IP rewrite — if
  the real firmware doesn't report those fields (P1S/A1 with no SD
  card, older field shapes, confirmed on P1S firmware 01.10.00.00),
  the slicer sees "no storage" and aborts. H2D and X1C reproductions
  worked because those firmwares do report the indicators.

  In _send_status_report's cached-as-base branch, after copying the
  cache and applying the existing protocol/upload-state overrides:

  - home_flag |= 0x100 (preserves any other bits the real printer set)
  - sdcard = True (force-set even when real says False)
  - storage = setdefault(...) (only fills in if missing — real values
    pass through unchanged when the printer reports them)

  For VP usage the slicer uploads via FTPS to Bambuddy's filesystem
  at /app/data/virtual_printer/uploads/<vpid>/; the printer's actual
  SD card is irrelevant on that path, so forcing "storage available"
  is correct for the queue / immediate / review modes the
  cached-as-base path covers.
2026-05-08 09:19:09 +02:00
maziggy 7dea33d0d8 feat(vp): mirror live target printer state to slicer in non-proxy modes
In non-proxy VP modes (Immediate / Review / Print Queue), the slicer now
sees real AMS / FTS / nozzle / k-profile state from the target printer
and streams the live camera — full slicer-as-remote functionality without
giving up Bambuddy's queue / archive / dispatch features.

Architecture (cached-as-base, single source of truth). The bridge caches
the latest real push_status and info.get_version response from Bambuddy's
existing per-printer MQTT subscription — no second session on the printer,
firmware in-flight budget unaffected (#1164). _send_status_report serves
a near-byte-identical copy of the cached push with only the upload-state-
machine fields overridden. Command responses (extrusion_cali_get, AMS
write acks, xcam) fan out raw — they carry sequence_ids the slicer is
waiting on. Slicer-issued commands forward to the printer except
project_file / gcode_file, which still terminate locally because the file
lives on Bambuddy. Camera is a raw TCPProxy on bind_ip:322 → printer:322,
same approach proxy mode uses.

Field-shape gotchas pinned in the bridge module's docstring and the
new test file:
  - Real Bambu pushes use json.dumps(indent=4) wire format. Compact JSON
    fails BambuStudio's Send pre-flight silently.
  - net.info[*].ip is the FTP destination IP (little-endian uint32).
    Without rewriting to the VP bind IP, the slicer FTPs straight to
    the real printer.
  - upgrade_state.sn rewritten to VP serial; AMS-hardware sn fields
    (n3f/0.sn etc.) left alone.
  - ipcam.rtsp_url passes through unchanged; BambuStudio overrides the
    URL host with the device IP it bound on, so :322 lands on the VP's
    TCPProxy.
  - extrusion_cali_get must forward; answering it locally hides the
    user's stored per-filament k-profiles.

Setup nuance for camera: the VP's access code must match the target
printer's because the slicer authenticates RTSPS with whatever access
code is in its profile. MQTT and FTP work either way.

Tested e2e with BambuStudio and OrcaSlicer against H2D (dual-nozzle,
AMS 2 Pro + AMS HT) and X1C across all three non-proxy modes — sync,
send, k-profile lookup, AMS configuration from slicer, and live camera
all work. Proxy mode is untouched: SlicerProxyManager owns its own
proxies and never instantiates SimpleMQTTServer or MQTTBridge.

25 new tests in backend/tests/unit/test_vp_mqtt_bridge.py cover lifecycle,
caching, identity / IP rewriting, wire format, slicer→printer routing,
and the LE-uint32 IP encoder against the real H2D capture value.
2026-05-03 13:43:54 +02:00
maziggy 68920f8c62 Fix virtual printer dropping null-terminated MQTT payloads from OrcaSlicer Linux (#927)
OrcaSlicer's Linux BBLNetworkPlugin publishes MQTT payloads with the
  C-string null terminator included in the length, so decoded messages
  arrived as `{…}\x00`. The strict json.loads() raised JSONDecodeError
  and the publish handler silently returned — pushall, get_version, and
  project_file were never answered, and the slicer hit its 60 s sync
  timeout. Print_queue mode only (proxy mode tunnels MQTT). The b069b521
  serial-adaptation fix was correct but ran past this earlier silent
  failure.

  _handle_publish now strips trailing \x00/whitespace before parsing and
  logs the raw payload on any remaining decode failure so future silent
  variants are visible in support bundles.
2026-04-19 08:04:05 +02:00
maziggy 6fb814c5ea feat(printer): add X2D support — camera, dual-nozzle, K-profile, maintenance (#988)
The Bambu Lab X2D (launched April 2026, dual-nozzle, enclosed, hardened
  steel rod gantry, AMS 2 Pro compatible) identifies itself as internal
  model code N6 via SSDP/MQTT, and real serials begin with 20P9. None of
  these identifiers existed in Bambuddy's registries, so the camera
  service fell back to the chamber-image protocol on port 6000 (X2D
  doesn't speak it), firmware-check logged "Unknown printer model: N6",
  and the dual-nozzle K-profile paths — gated on the H2D serial prefix
  "094" — would have treated X2D as single-nozzle.

  Backend:
  - Register N6 → X2D across every registry (PRINTER_MODEL_ID_MAP,
    PRINTER_MODEL_MAP, STEEL_ROD_MODELS, ETHERNET_MODELS,
    CHAMBER_TEMP_SUPPORTED_MODELS, firmware-check API keys + wiki path,
    virtual-printer SSDP/product/serial tables, DB vp_model_fixes).
  - supports_rtsp(): match the X2 display-name prefix and the N6 internal
    code; camera now routes to RTSP on port 322.
  - Dual-nozzle serial prefix check in bambu_mqtt.delete_kprofile and
    kprofiles.set_kprofile broadened to ("094", "20P9") — X2D now takes
    the H2D-style cali_idx in-place edit path.
  - is_h2d model gate in bambu_mqtt.start_print extended with "X2D" so
    timelapse / bed_leveling / flow_cali / vibration_cali / layer_inspect
    are sent as integers and external-spool ams_id 254/255 routing is
    preserved (H2D-style deputy-nozzle addressing).

  X2D uses hardened steel rods like P2S — it is intentionally placed in
  STEEL_ROD_MODELS, not CARBON_ROD_MODELS. A regression-guard test pins
  the classification.

  Frontend:
  - mapModelCode in PrintersPage and SpoolBuddyAmsPage handle N6 and X2D.
  - Enclosure-door badge and airduct-mode whitelists include X2D.
  - MaintenancePage.getMaintenanceWikiUrl routes X2D to P2S wiki URLs for
    steel-rod lubrication, belt tension, cold-pull, and PTFE tube
    (exported to enable direct unit testing).

  Tests:
  - test_printer_models.py: TestX2DModel (10 assertions).
  - test_bambu_mqtt.py: X2D in start_print ams_mapping and is_h2d gate;
    TestDeleteKProfileDualNozzleDetection across H2D, X2D, P2S, X1C.
  - MaintenancePageWikiUrls.test.tsx: 15 assertions covering X2D, P2S
    regression, X1C/H2D/A1Mini regression, and model-name normalisation.

  Docs:
  - README: added X2 series to the supported printers table.
  - CHANGELOG: new entry under 0.2.3b4 Fixed.

  Credit to @krautech for the report and debug bundle, and to @legend813
  for PR #989 which seeded most of the registry changes — rod-type
  classification was corrected (steel, not carbon) and the dual-nozzle /
  K-profile / is_h2d gaps were added on top.
2026-04-16 10:40:32 +02:00
maziggy b069b5217c y Fix virtual printer "Synchronizing device information" timeout in Orca (#927)
OrcaSlicer's "Send job" flow sat on "Synchronizing device information…"
  until it gave up, even though FTP upload worked when the user clicked
  "Send job anyway". The virtual printer's MQTT server gated all incoming
  command handling on `f"device/{self.serial}/request" in topic` — if the
  slicer's cached serial for the VP didn't exactly equal the VP's computed
  self.serial (model prefix + per-VP serial_suffix), every get_version,
  pushall, and project_file publish was silently dropped. Nothing was
  logged past the initial "MQTT publish to …" line, so the slicer never
  received a push_status or get_version response on its subscribed
  device/{serial}/report topic and hit its sync timeout. Responses were
  also unconditionally published on device/{self.serial}/report, so even
  when the inbound check happened to pass, replies targeted a topic the
  slicer wasn't listening on if its serial had drifted.

  Both directions are now serial-adaptive:

  - `_handle_publish` accepts any authenticated publish on a
    `device/*/request` topic and extracts the serial from the topic itself
    rather than comparing against self.serial.
  - A per-connection `_client_serials` dict tracks the serial the slicer
    actually uses, populated from the first SUBSCRIBE or PUBLISH seen on
    each connection and cleared on disconnect/stop.
  - `_send_status_report`, `_send_version_response`, `_send_print_response`
    now take an optional `serial` parameter (defaulting to self.serial)
    so every outgoing publish — including the periodic 1-second status
    push — targets the topic the slicer subscribed to.
  - The version response's embedded `module[].sn` fields now also carry
    the client's serial so the payload is internally consistent with the
    topic.
  - When the client's serial differs from self.serial an INFO log records
    the adaptation so it's visible in future support bundles.

  The working case (slicer's cached serial equals self.serial, as in my
  own H2D-1 Proxy setup) is bit-for-bit identical to the old behavior —
  the new check is strictly more permissive and only affects cases the
  old code silently dropped.

  Regression tests cover:
  - `_extract_serial_from_topic` valid/invalid topic shapes
  - mismatched-serial publish → handler runs, response topic and sn field
    both use the client's serial
  - non-`/request` topics → still rejected
  - pushall → status_report routed to the client's subscribed topic
  - `_client_serials` cleared on stop()
2026-04-10 12:13:54 +02:00
maziggy 82d329d85c [Fix] Virtual Printer FTP routed to wrong VP with different access codes (#735)
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
2026-03-18 09:04:31 +01:00
maziggy b2335a2592 Added VP name prefix (e.g. [Virtual - X1C]) to all FTP and MQTT
log lines so issues can be traced to a specific virtual printer, and
  demoted FTP protocol chatter from info to debug.
2026-03-17 17:36:47 +01:00
maziggy 4f617c21e1 [Fix] X1C Virtual Printer not accepting sends (#735)
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.
2026-03-17 16:25:31 +01:00
maziggy ce97a47627 Add H2C dual nozzle variant O1C2 model support (#489)
The H2C dual nozzle variant reports model code O1C2 via MQTT, but only
  O1C was recognized. This caused the camera to use the wrong protocol
  (chamber image on port 6000 instead of RTSP on port 322), producing a
  reconnect loop. Added O1C2 to all model ID maps across 8 files.
2026-02-28 11:53:38 +01:00
maziggy b02e9da943 WIP: multi virtual printer support 2026-02-18 06:37:27 +01:00
maziggy 3f7d2bf619 Fix virtual printer FTP transfer failure with connection reset (#58)
Large 3MF uploads intermittently failed with [Errno 104] Connection
reset by peer while the 16-byte verify_job always succeeded. The
_handle_data_connection callback returned immediately, letting the
asyncio task complete while cmd_STOR was still reading from the data
connection. The passive port listener also stayed open during transfers.

- Keep _handle_data_connection alive via _transfer_done event so the
  asyncio task holds strong references throughout the transfer
- Close passive port listener after accepting the data connection
- Reject duplicate data connections with a warning log
- Add drain timeout (5s) to MQTT status pushes to prevent blocking
  when the slicer is busy with FTP upload
- Improve error logging with bytes received and exception type
2026-02-08 14:37:50 +01:00
maziggy 70622e6e53 Add proxy mode for virtual printer (cross-LAN slicer support)
Enable Bambu Studio on a remote network to print through BamBuddy
acting as a TLS-terminating proxy for both MQTT and FTP connections.

- Add TLSProxy base class and FTPTLSProxy with PASV response rewriting,
  EPSV→PASV translation, PROT P/C tracking, and one-shot data proxies
- Add SlicerProxyManager to coordinate per-slicer MQTT + FTP proxy pairs
- Support additional SAN IPs in certificate generation for proxy mode
- Broadcast SSDP on LAN B so slicers discover the proxy as a printer
- Narrow FTP passive port range to 50000-50100 with retry logic
- Expose proxy ports (8883, 9990, 50000-50100) in Dockerfile
- Document passive port range in docker-compose.yml
2026-02-07 15:17:19 +01:00
maziggy 7b90c743c2 Strip explanatory text from nosec comments to silence Bandit warnings
Bandit parses all words after `# nosec BXXX` as test IDs, producing
~35 "not a test name or id" warnings. Trim to just `# nosec BXXX`.
2026-02-06 12:57:37 +01:00
maziggy 5b0a985da2 Add explanatory comments to 265 empty except blocks
CodeQL flags except blocks where `pass` has no comment explaining
why the exception is silently ignored (py/empty-except rule).

Added context-specific comments to all 265 instances across 31 files:
- database.py (~112): ALTER TABLE migrations — "Already applied"
- archive/library/3MF parsing (~64): "Skip unparseable metadata"
- virtual_printer network cleanup (~32): "Best-effort socket cleanup"
- discovery/SSDP (~13): "SO_REUSEPORT not available" / socket cleanup
- bambu_ftp/mqtt (~13): FTP cleanup, JSON decode, signal parsing
- remaining routes/services (~31): context-specific comments
2026-02-06 11:58:38 +01:00
maziggy a0133fb43b Fix safe security findings: hashlib, log injection, broad excepts, bandit suppressions
- Add usedforsecurity=False to MD5 (AMS fingerprint) and SHA1 (git blob
  hash) calls to silence Bandit B303 / CodeQL weak-crypto findings
- Convert ~996 f-string logging calls to parameterized %s-style across
  55 files to prevent log injection (Bandit G201 / CodeQL log-injection)
- Narrow ~199 broad except Exception blocks to specific types:
  OperationalError for DB migrations, OSError for network/file cleanup,
  (OSError, ftplib.error_reply) for FTP, and targeted tuples for
  ZIP/XML/JSON parsing — 36 intentionally left broad (mixed async,
  re-raise patterns)
- Add # nosec comments to 9 known-safe lines (0.0.0.0 virtual printer
  binds, ftplib imports) and exclude backend/tests/ from bandit scan
- Bandit now reports 0 medium/high findings
2026-02-06 11:45:12 +01:00
maziggy 53bd4fadb3 Fix safe security findings: hashlib, log injection, broad excepts
- Add usedforsecurity=False to MD5 (AMS fingerprint) and SHA1 (git blob
  hash) calls to silence Bandit B303 / CodeQL weak-crypto findings
- Convert ~996 f-string logging calls to parameterized %s-style across
  55 files to prevent log injection (Bandit G201 / CodeQL log-injection)
- Narrow ~199 broad except Exception blocks to specific types:
  OperationalError for DB migrations, OSError for network/file cleanup,
  (OSError, ftplib.error_reply) for FTP, and targeted tuples for
  ZIP/XML/JSON parsing — 36 intentionally left broad (mixed async,
  re-raise patterns)
2026-02-06 11:37:59 +01:00
maziggy bb37a8c8a8 Fix virtual printer model codes and serial prefixes
- Correct SSDP model codes: C11=P1P, C12=P1S, N7=P2S, C13=X1E
  - Fix serial prefixes based on actual Bambu serial format
  - Add confirmation modal for pending upload discard
  - Sort model dropdown alphabetically, remove internal codes
  - Add "Setup Required" warning with link to wiki documentation
  - Update wiki with certificate installation and platform setup guides
2026-01-04 14:05:08 +01:00
maziggy 88e84ca45a - Add Virtual Printer feature - emulate Bambu printer on network
- Virtual printer appears in Bambu Studio/Orca Slicer via SSDP discovery
  - Secure TLS/MQTT communication with auto-generated certificates
  - Queue mode (pending uploads) or auto-start mode
  - Configurable access code for authentication
  - Docker support with network_mode: host and certificate persistence
  - Fix backup/restore for virtual printer settings (auto-save no longer overwrites)
2025-12-29 15:52:48 +01:00