mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 19:21:33 +02:00
#1429 (reported by @TrickShotMLG02, confirmed by @Mape6 on a flat single-LAN that rules out subnet / mDNS-reflector theories): with the physical printer off the slicer's "Send" landed in Bambuddy's archive; once the printer powered on every subsequent "Send" went straight to the printer's SD card and bypassed Bambuddy. Bundle analysis: mape6-before showed clean FTP receive + archive lines, mape6-after had zero FTP attempts to Bambuddy once the printer was online. Cause: mqtt_bridge.py::_resolve_client encoded _target_ip_uint32_le / _vp_ip_uint32_le ONLY on client-identity change and early-returned on every refresh tick when the same client object was still bound. If target_client.ip_address was empty at first bind (DB row stale, or client constructed before SSDP refresh filled it in), the encoding stayed None, the net.info[*].ip rewrite block was skipped, the cache filled with the real printer IP, sticky-key preservation kept the poisoned net value alive across every subsequent incremental push, and the slicer followed the leaked IP. Only Bambuddy-restart-with-printer-off cleared it — the workaround both reporters independently arrived at. Same shape on multi-NIC printers (X1C, H2D Pro): the rewrite only matched entries whose ip equalled _target_ip_uint32_le, so a secondary interface IP Bambuddy never saw would leak through unchanged. Bridge fix: - _resolve_client calls a new _refresh_ip_encoding() on every refresh tick, even when client identity is unchanged; self-heals once ip_address becomes valid. - _refresh_ip_encoding() sweeps the existing _latest_print_state when encoding becomes valid for the first time. Without the sweep, sticky-key preservation keeps the pre-arm poisoned cache alive forever — incremental pushes that don't include net carry the bad value forward. - _rewrite_net_info_ips() rewrites EVERY non-zero net.info[].ip entry that doesn't already equal the VP IP, not just entries matching _target_ip_uint32_le. Multi-NIC printers stop leaking secondary interfaces. Zero-IP placeholders are left alone so "active interface" detection still works. - INFO logging on encoding arm/update and on cache sweep so future bundles directly answer "did the rewrite fire?". Mode wire-value rename (#1429 follow-up, separate confusion source): - Both reporters' support bundles showed mode: immediate while the UI said "Archive"; @TrickShotMLG02 quoted: "I have no idea why it says immediate in the support-info.json file. In the webui the printer is set to archive". UI button "Archive" had always saved immediate, and "Queue" had always saved print_queue. Canonical wire values are now archive / review / queue / proxy, matching the button labels 1:1. - New normalize_vp_mode() + VP_MODE_* constants in models/virtual_printer.py; manager.py normalises on construction so a legacy row read pre-migration still dispatches correctly. - core/database.py::run_migrations rewrites existing virtual_printers and settings rows; idempotent (re-runs are no-ops); identical SQL under SQLite and Postgres. - API routes accept both legacy and canonical on input, normalise before storage. GET /settings/virtual-printer normalises on read so the frontend's mode-button highlight works for stale legacy values. - Three frontend VP components (VirtualPrinterSettings, VirtualPrinterCard, VirtualPrinterAddDialog) switched click handlers and type aliases to canonical; each got its own normalizeMode() helper so a stale-cached settings payload still highlights the right button. Two pre-existing `printer.mode === 'queue' ? 'review'` legacy mappings in VirtualPrinterCard were the source of a test failure caught mid-implementation where the new canonical 'queue' was being mis-aliased back to 'review' and hiding the auto-dispatch + force-color-match toggles. mode handler is NOT the dispatch bug: manager.py::_archive_file (the handler for archive mode) doesn't dispatch to the physical printer. The "files end up on the printer's SD card" symptom was the IP-leak from the bridge cache. The mode rename is purely clarity / support- bundle accuracy.
66 lines
3.2 KiB
Python
66 lines
3.2 KiB
Python
from datetime import datetime
|
|
|
|
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String, func
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from backend.app.core.database import Base
|
|
|
|
# Canonical VP mode values. The legacy values `immediate` (→ archive) and
|
|
# `print_queue` (→ queue) shipped before the UI labels were aligned with the
|
|
# wire format. `normalize_vp_mode()` translates input from either form and
|
|
# the DB migration in `core/database.py` rewrites existing rows once at boot.
|
|
VP_MODE_ARCHIVE = "archive"
|
|
VP_MODE_REVIEW = "review"
|
|
VP_MODE_QUEUE = "queue"
|
|
VP_MODE_PROXY = "proxy"
|
|
VP_MODE_VALUES = (VP_MODE_ARCHIVE, VP_MODE_REVIEW, VP_MODE_QUEUE, VP_MODE_PROXY)
|
|
|
|
# Legacy → canonical map. Kept narrow on purpose so unrelated typos surface
|
|
# instead of getting silently re-pointed at a default.
|
|
_VP_MODE_ALIASES = {
|
|
"immediate": VP_MODE_ARCHIVE,
|
|
"print_queue": VP_MODE_QUEUE,
|
|
}
|
|
|
|
|
|
def normalize_vp_mode(value: str | None) -> str | None:
|
|
"""Map legacy wire values (`immediate`, `print_queue`) to canonical names.
|
|
|
|
Returns `None` unchanged so callers can decide whether to apply a default.
|
|
Returns unknown values unchanged so validators still see them and reject.
|
|
"""
|
|
if value is None:
|
|
return None
|
|
return _VP_MODE_ALIASES.get(value, value)
|
|
|
|
|
|
class VirtualPrinter(Base):
|
|
"""Virtual printer configuration for multi-instance support."""
|
|
|
|
__tablename__ = "virtual_printers"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
name: Mapped[str] = mapped_column(String(100), default="Bambuddy")
|
|
enabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
mode: Mapped[str] = mapped_column(String(20), default=VP_MODE_ARCHIVE) # archive|review|queue|proxy
|
|
auto_dispatch: Mapped[bool] = mapped_column(Boolean, server_default="true") # queue mode: auto-start or manual
|
|
queue_force_color_match: Mapped[bool] = mapped_column(
|
|
Boolean, server_default="false"
|
|
) # queue mode: pin per-slot type+color from the 3MF onto the queue
|
|
# item so the scheduler refuses to dispatch onto a printer with the wrong
|
|
# filament loaded (#1188).
|
|
model: Mapped[str | None] = mapped_column(String(50), nullable=True) # SSDP model code (server mode)
|
|
access_code: Mapped[str | None] = mapped_column(String(8), nullable=True) # 8 chars (server mode)
|
|
target_printer_id: Mapped[int | None] = mapped_column(
|
|
Integer, ForeignKey("printers.id", ondelete="SET NULL"), nullable=True
|
|
) # proxy mode
|
|
bind_ip: Mapped[str | None] = mapped_column(String(45), nullable=True) # dedicated IP (proxy mode)
|
|
remote_interface_ip: Mapped[str | None] = mapped_column(String(45), nullable=True) # SSDP advertise IP
|
|
tailscale_disabled: Mapped[bool] = mapped_column(
|
|
Boolean, server_default="true"
|
|
) # opt-in: user must explicitly enable; auto-detect only runs then
|
|
serial_suffix: Mapped[str] = mapped_column(String(9), default="391800001") # unique per printer
|
|
position: Mapped[int] = mapped_column(Integer, default=0)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now())
|