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
This commit is contained in:
maziggy
2026-05-30 13:34:10 +02:00
parent 40cd45e2d5
commit 597762685c
20 changed files with 1612 additions and 100 deletions
@@ -64,6 +64,11 @@ class BindServer:
self._servers: list[asyncio.Server] = []
self._running = False
# Set after at least one bind port is listening — see ftp_server.py
# for rationale. Bind server is best-effort across BIND_PORTS, so
# "ready" means "at least one port bound", matching the existing
# serve_forever path.
self.ready = asyncio.Event()
def _create_tls_context(self) -> ssl.SSLContext | None:
"""Create SSL context for the TLS bind port (3002)."""
@@ -122,6 +127,7 @@ class BindServer:
if not self._servers:
logger.error("Bind server: could not bind to any port")
return
self.ready.set()
# Serve all successfully bound ports
await asyncio.gather(*(s.serve_forever() for s in self._servers))
@@ -137,6 +143,7 @@ class BindServer:
"""Stop the bind server."""
logger.info("Stopping bind server")
self._running = False
self.ready.clear()
for server in self._servers:
try:
@@ -176,7 +183,14 @@ class BindServer:
logger.warning("Bind server: unexpected command from %s: %s", client_id, request)
return
# Build response
# Build response. `sequence_id` is an INTEGER counter chosen by
# the printer side (not an echo of the slicer's string seq_id).
# The protocol docstring at the top of this file documents the
# asymmetry: slicer sends `"20000"` (string), printer replies
# with an int. The hardcoded 3021 mirrors real-firmware-captured
# value; an earlier audit suggesting we echo the slicer's seq_id
# was wrong and would have broken slicers that validate the
# type (int vs string).
response = {
"login": {
"bind": "free",