mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-09 07:25:44 +02:00
[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.
This commit is contained in:
-1396
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,23 @@ class VirtualPrinterUpdate(BaseModel):
|
||||
remote_interface_ip: str | None = None
|
||||
|
||||
|
||||
def _resolve_printer_model(printer_model: str | None) -> str | None:
|
||||
"""Map a printer's model (display name or SSDP code) to a valid VP SSDP model code.
|
||||
|
||||
Printers store display names like 'X1C' while VPs need SSDP codes like 'BL-P001'.
|
||||
"""
|
||||
if not printer_model:
|
||||
return None
|
||||
from backend.app.services.virtual_printer import VIRTUAL_PRINTER_MODELS
|
||||
from backend.app.services.virtual_printer.manager import DISPLAY_NAME_TO_MODEL_CODE
|
||||
|
||||
# Already a valid SSDP model code
|
||||
if printer_model in VIRTUAL_PRINTER_MODELS:
|
||||
return printer_model
|
||||
# Map display name to SSDP code
|
||||
return DISPLAY_NAME_TO_MODEL_CODE.get(printer_model)
|
||||
|
||||
|
||||
def _vp_to_dict(vp, status: dict | None = None) -> dict:
|
||||
"""Convert VirtualPrinter model to response dict."""
|
||||
from backend.app.services.virtual_printer import VIRTUAL_PRINTER_MODELS
|
||||
@@ -172,7 +189,7 @@ async def create_virtual_printer(
|
||||
enabled=body.enabled,
|
||||
mode=body.mode,
|
||||
model=body.model
|
||||
or (target_printer.model if target_printer and target_printer.model and body.mode == "proxy" else None)
|
||||
or _resolve_printer_model(target_printer.model if target_printer and body.mode == "proxy" else None)
|
||||
or DEFAULT_VIRTUAL_PRINTER_MODEL,
|
||||
access_code=body.access_code,
|
||||
target_printer_id=body.target_printer_id,
|
||||
@@ -276,7 +293,7 @@ async def update_virtual_printer(
|
||||
vp.target_printer_id = body.target_printer_id
|
||||
# Auto-inherit model from target printer in proxy mode (unless user explicitly set model)
|
||||
if body.model is None and vp.mode == "proxy" and target_printer.model:
|
||||
vp.model = target_printer.model
|
||||
vp.model = _resolve_printer_model(target_printer.model) or target_printer.model
|
||||
if body.auto_dispatch is not None:
|
||||
vp.auto_dispatch = body.auto_dispatch
|
||||
if body.bind_ip is not None:
|
||||
@@ -291,7 +308,7 @@ async def update_virtual_printer(
|
||||
result = await db.execute(select(PrinterModel).where(PrinterModel.id == vp.target_printer_id))
|
||||
existing_target = result.scalar_one_or_none()
|
||||
if existing_target and existing_target.model:
|
||||
vp.model = existing_target.model
|
||||
vp.model = _resolve_printer_model(existing_target.model) or existing_target.model
|
||||
|
||||
# Determine final enabled state
|
||||
explicitly_enabling = body.enabled is True
|
||||
|
||||
@@ -1321,7 +1321,7 @@ async def run_migrations(conn):
|
||||
|
||||
result = await conn.execute(text("SELECT value FROM settings WHERE key = 'virtual_printer_model'"))
|
||||
row = result.fetchone()
|
||||
old_model = row[0] if row else "3DPrinter-X1-Carbon"
|
||||
old_model = row[0] if row else "BL-P001"
|
||||
|
||||
result = await conn.execute(
|
||||
text("SELECT value FROM settings WHERE key = 'virtual_printer_target_printer_id'")
|
||||
@@ -1440,6 +1440,33 @@ async def run_migrations(conn):
|
||||
except OperationalError:
|
||||
pass # Already applied
|
||||
|
||||
# Migration: Fix VP model codes — convert legacy SSDP codes and display names to correct SSDP codes
|
||||
# Legacy codes (from multi-VP refactor) and display names (from proxy auto-inherit)
|
||||
vp_model_fixes = {
|
||||
"3DPrinter-X1-Carbon": "BL-P001",
|
||||
"3DPrinter-X1": "BL-P002",
|
||||
"X1C": "BL-P001",
|
||||
"X1": "BL-P002",
|
||||
"X1E": "C13",
|
||||
"P1P": "C11",
|
||||
"P1S": "C12",
|
||||
"P2S": "N7",
|
||||
"A1": "N2S",
|
||||
"A1 Mini": "N1",
|
||||
"H2D": "O1D",
|
||||
"H2C": "O1C",
|
||||
"H2S": "O1S",
|
||||
}
|
||||
for old_val, new_val in vp_model_fixes.items():
|
||||
await conn.execute(
|
||||
text("UPDATE virtual_printers SET model = :new WHERE model = :old"),
|
||||
{"old": old_val, "new": new_val},
|
||||
)
|
||||
await conn.execute(
|
||||
text("UPDATE settings SET value = :new WHERE key = 'virtual_printer_model' AND value = :old"),
|
||||
{"old": old_val, "new": new_val},
|
||||
)
|
||||
|
||||
# Migration: Add per-user Bambu Cloud credential columns
|
||||
try:
|
||||
await conn.execute(text("ALTER TABLE users ADD COLUMN cloud_token VARCHAR(500)"))
|
||||
|
||||
@@ -28,8 +28,8 @@ logger = logging.getLogger(__name__)
|
||||
# - https://github.com/psychoticbeef/BambuLabOrcaSlicerDiscovery
|
||||
VIRTUAL_PRINTER_MODELS = {
|
||||
# X1 Series
|
||||
"3DPrinter-X1-Carbon": "X1C", # X1 Carbon
|
||||
"3DPrinter-X1": "X1", # X1
|
||||
"BL-P001": "X1C", # X1 Carbon
|
||||
"BL-P002": "X1", # X1
|
||||
"C13": "X1E", # X1E
|
||||
# P Series
|
||||
"C11": "P1P", # P1P
|
||||
@@ -56,8 +56,8 @@ VIRTUAL_PRINTER_MODELS = {
|
||||
# UUUUU = Unit number (5 chars)
|
||||
MODEL_SERIAL_PREFIXES = {
|
||||
# X1 Series
|
||||
"3DPrinter-X1-Carbon": "00M00A", # X1C
|
||||
"3DPrinter-X1": "00M00A", # X1
|
||||
"BL-P001": "00M00A", # X1C
|
||||
"BL-P002": "00M00A", # X1
|
||||
"C13": "03W00A", # X1E
|
||||
# P Series
|
||||
"C11": "01S00A", # P1P
|
||||
@@ -73,8 +73,11 @@ MODEL_SERIAL_PREFIXES = {
|
||||
"O1S": "09400A", # H2S
|
||||
}
|
||||
|
||||
# Reverse mapping: display name → SSDP model code (for auto-inheriting from printer model)
|
||||
DISPLAY_NAME_TO_MODEL_CODE = {v: k for k, v in VIRTUAL_PRINTER_MODELS.items()}
|
||||
|
||||
# Default model
|
||||
DEFAULT_VIRTUAL_PRINTER_MODEL = "3DPrinter-X1-Carbon" # X1C
|
||||
DEFAULT_VIRTUAL_PRINTER_MODEL = "BL-P001" # X1C
|
||||
|
||||
|
||||
def _get_serial_for_model(model: str, serial_suffix: str) -> str:
|
||||
|
||||
@@ -18,8 +18,8 @@ MQTT_PORT = 8883
|
||||
|
||||
# Model code → product_name for version response (must match what slicer expects)
|
||||
MODEL_PRODUCT_NAMES = {
|
||||
"3DPrinter-X1-Carbon": "X1 Carbon",
|
||||
"3DPrinter-X1": "X1",
|
||||
"BL-P001": "X1 Carbon",
|
||||
"BL-P002": "X1",
|
||||
"C13": "X1E",
|
||||
"C11": "P1P",
|
||||
"C12": "P1S",
|
||||
|
||||
@@ -54,7 +54,7 @@ class TestVirtualPrinterInstance:
|
||||
vp_id=2,
|
||||
name="X1C",
|
||||
mode="immediate",
|
||||
model="3DPrinter-X1-Carbon",
|
||||
model="BL-P001",
|
||||
access_code="12345678",
|
||||
serial_suffix="391800002",
|
||||
base_dir=tmp_path,
|
||||
@@ -1305,7 +1305,7 @@ class TestVirtualPrinterInstanceIPOverride:
|
||||
vp_id=20,
|
||||
name="IPTest",
|
||||
mode="immediate",
|
||||
model="3DPrinter-X1-Carbon",
|
||||
model="BL-P001",
|
||||
access_code="12345678",
|
||||
serial_suffix="391800020",
|
||||
bind_ip="192.168.1.50",
|
||||
@@ -1342,7 +1342,7 @@ class TestVirtualPrinterInstanceIPOverride:
|
||||
vp_id=21,
|
||||
name="NoRemote",
|
||||
mode="immediate",
|
||||
model="3DPrinter-X1-Carbon",
|
||||
model="BL-P001",
|
||||
access_code="12345678",
|
||||
serial_suffix="391800021",
|
||||
bind_ip="192.168.1.50",
|
||||
@@ -1368,7 +1368,7 @@ class TestVirtualPrinterInstanceIPOverride:
|
||||
vp_id=22,
|
||||
name="NoIPs",
|
||||
mode="immediate",
|
||||
model="3DPrinter-X1-Carbon",
|
||||
model="BL-P001",
|
||||
access_code="12345678",
|
||||
serial_suffix="391800022",
|
||||
base_dir=tmp_path,
|
||||
@@ -1396,7 +1396,7 @@ class TestBindServer:
|
||||
|
||||
return BindServer(
|
||||
serial="01S00C000000001",
|
||||
model="3DPrinter-X1-Carbon",
|
||||
model="BL-P001",
|
||||
name="Bambuddy",
|
||||
)
|
||||
|
||||
@@ -1464,7 +1464,7 @@ class TestBindServer:
|
||||
def test_bind_server_stores_config(self, bind_server):
|
||||
"""Verify config is stored correctly."""
|
||||
assert bind_server.serial == "01S00C000000001"
|
||||
assert bind_server.model == "3DPrinter-X1-Carbon"
|
||||
assert bind_server.model == "BL-P001"
|
||||
assert bind_server.name == "Bambuddy"
|
||||
assert bind_server.version == "01.00.00.00"
|
||||
|
||||
@@ -1474,7 +1474,7 @@ class TestBindServer:
|
||||
|
||||
server = BindServer(
|
||||
serial="01S00C000000001",
|
||||
model="3DPrinter-X1-Carbon",
|
||||
model="BL-P001",
|
||||
name="Bambuddy",
|
||||
version="01.09.00.10",
|
||||
)
|
||||
@@ -1501,7 +1501,7 @@ class TestBindServer:
|
||||
vp_id=99,
|
||||
name="Bambuddy",
|
||||
mode="immediate",
|
||||
model="3DPrinter-X1-Carbon",
|
||||
model="BL-P001",
|
||||
access_code="12345678",
|
||||
serial_suffix="391800099",
|
||||
bind_ip="192.168.1.50",
|
||||
@@ -1524,9 +1524,46 @@ class TestBindServer:
|
||||
|
||||
mock_bind_cls.assert_called_once_with(
|
||||
serial=inst.serial,
|
||||
model="3DPrinter-X1-Carbon",
|
||||
model="BL-P001",
|
||||
name="Bambuddy",
|
||||
bind_address="192.168.1.50",
|
||||
cert_path=Path("/tmp/cert.pem"), # nosec B108
|
||||
key_path=Path("/tmp/key.pem"), # nosec B108
|
||||
)
|
||||
|
||||
|
||||
class TestResolveModelCodes:
|
||||
"""Tests for model code resolution (display name → SSDP code)."""
|
||||
|
||||
def test_display_name_to_model_code_maps_all_models(self):
|
||||
"""Verify reverse mapping covers all VIRTUAL_PRINTER_MODELS entries."""
|
||||
from backend.app.services.virtual_printer.manager import DISPLAY_NAME_TO_MODEL_CODE, VIRTUAL_PRINTER_MODELS
|
||||
|
||||
for _code, display_name in VIRTUAL_PRINTER_MODELS.items():
|
||||
assert display_name in DISPLAY_NAME_TO_MODEL_CODE
|
||||
# For non-duplicate display names, should map back to a valid code
|
||||
assert DISPLAY_NAME_TO_MODEL_CODE[display_name] in VIRTUAL_PRINTER_MODELS
|
||||
|
||||
def test_resolve_printer_model_with_ssdp_code(self):
|
||||
"""SSDP codes pass through unchanged."""
|
||||
from backend.app.api.routes.virtual_printers import _resolve_printer_model
|
||||
|
||||
assert _resolve_printer_model("BL-P001") == "BL-P001"
|
||||
assert _resolve_printer_model("O1D") == "O1D"
|
||||
assert _resolve_printer_model("N2S") == "N2S"
|
||||
|
||||
def test_resolve_printer_model_with_display_name(self):
|
||||
"""Display names resolve to SSDP codes."""
|
||||
from backend.app.api.routes.virtual_printers import _resolve_printer_model
|
||||
|
||||
assert _resolve_printer_model("X1C") == "BL-P001"
|
||||
assert _resolve_printer_model("H2D") == "O1D"
|
||||
assert _resolve_printer_model("A1") == "N2S"
|
||||
assert _resolve_printer_model("P1S") == "C12"
|
||||
|
||||
def test_resolve_printer_model_with_none_or_unknown(self):
|
||||
"""None and unknown values return None."""
|
||||
from backend.app.api.routes.virtual_printers import _resolve_printer_model
|
||||
|
||||
assert _resolve_printer_model(None) is None
|
||||
assert _resolve_printer_model("UnknownModel") is None
|
||||
|
||||
@@ -30,7 +30,7 @@ vi.mock('../../api/client', () => ({
|
||||
import { multiVirtualPrinterApi } from '../../api/client';
|
||||
|
||||
const models: Record<string, string> = {
|
||||
'3DPrinter-X1-Carbon': 'X1C',
|
||||
'BL-P001': 'X1C',
|
||||
'C12': 'P1S',
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ const createMockPrinter = (overrides: Partial<VirtualPrinterConfig> = {}): Virtu
|
||||
name: 'Test VP',
|
||||
enabled: false,
|
||||
mode: 'immediate',
|
||||
model: '3DPrinter-X1-Carbon',
|
||||
model: 'BL-P001',
|
||||
model_name: 'X1C',
|
||||
access_code_set: false,
|
||||
serial: '00M00A391800001',
|
||||
|
||||
@@ -27,7 +27,7 @@ vi.mock('../../api/client', () => ({
|
||||
updateSettings: vi.fn(),
|
||||
getModels: vi.fn().mockResolvedValue({
|
||||
models: {
|
||||
'3DPrinter-X1-Carbon': 'X1C',
|
||||
'BL-P001': 'X1C',
|
||||
'C12': 'P1S',
|
||||
'N7': 'P2S',
|
||||
},
|
||||
@@ -43,7 +43,7 @@ const createMockSettings = (overrides = {}) => ({
|
||||
enabled: false,
|
||||
access_code_set: false,
|
||||
mode: 'immediate' as const,
|
||||
model: '3DPrinter-X1-Carbon',
|
||||
model: 'BL-P001',
|
||||
target_printer_id: null as number | null,
|
||||
remote_interface_ip: null as string | null,
|
||||
status: {
|
||||
@@ -52,7 +52,7 @@ const createMockSettings = (overrides = {}) => ({
|
||||
mode: 'immediate',
|
||||
name: 'Bambuddy',
|
||||
serial: '00M00A391800001',
|
||||
model: '3DPrinter-X1-Carbon',
|
||||
model: 'BL-P001',
|
||||
model_name: 'X1C',
|
||||
pending_files: 0,
|
||||
},
|
||||
@@ -158,7 +158,7 @@ describe('VirtualPrinterSettings', () => {
|
||||
mode: 'immediate',
|
||||
name: 'Bambuddy',
|
||||
serial: '00M00A391800001',
|
||||
model: '3DPrinter-X1-Carbon',
|
||||
model: 'BL-P001',
|
||||
model_name: 'X1C',
|
||||
pending_files: 0,
|
||||
},
|
||||
@@ -512,7 +512,7 @@ describe('VirtualPrinterSettings', () => {
|
||||
mode: 'proxy',
|
||||
name: 'Bambuddy (Proxy)',
|
||||
serial: '00M00A391800001',
|
||||
model: '3DPrinter-X1-Carbon',
|
||||
model: 'BL-P001',
|
||||
model_name: 'X1C',
|
||||
pending_files: 0,
|
||||
proxy: {
|
||||
|
||||
@@ -17,7 +17,7 @@ export function VirtualPrinterSettings() {
|
||||
const [localEnabled, setLocalEnabled] = useState(false);
|
||||
const [localAccessCode, setLocalAccessCode] = useState('');
|
||||
const [localMode, setLocalMode] = useState<LocalMode>('immediate');
|
||||
const [localModel, setLocalModel] = useState('3DPrinter-X1-Carbon');
|
||||
const [localModel, setLocalModel] = useState('BL-P001');
|
||||
const [localTargetPrinterId, setLocalTargetPrinterId] = useState<number | null>(null);
|
||||
const [localRemoteInterfaceIp, setLocalRemoteInterfaceIp] = useState('');
|
||||
const [showAccessCode, setShowAccessCode] = useState(false);
|
||||
|
||||
Reference in New Issue
Block a user