mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 19:21:33 +02:00
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.
210 lines
5.3 KiB
Python
210 lines
5.3 KiB
Python
"""Printer model normalization utilities.
|
|
|
|
Converts 3MF printer model names (e.g., "Bambu Lab X1 Carbon") to
|
|
normalized short names (e.g., "X1C") that match database storage.
|
|
"""
|
|
|
|
# Map from 3MF printer_model strings to normalized short names
|
|
PRINTER_MODEL_MAP = {
|
|
"Bambu Lab X1 Carbon": "X1C",
|
|
"Bambu Lab X1": "X1",
|
|
"Bambu Lab X1E": "X1E",
|
|
"Bambu Lab P1S": "P1S",
|
|
"Bambu Lab P1P": "P1P",
|
|
"Bambu Lab P2S": "P2S",
|
|
"Bambu Lab A1": "A1",
|
|
"Bambu Lab A1 Mini": "A1 Mini",
|
|
"Bambu Lab A1 mini": "A1 Mini",
|
|
"Bambu Lab H2D": "H2D",
|
|
"Bambu Lab H2D Pro": "H2D Pro",
|
|
"Bambu Lab H2C": "H2C",
|
|
"Bambu Lab H2S": "H2S",
|
|
"Bambu Lab X2D": "X2D",
|
|
}
|
|
|
|
# Map from printer_model_id (internal codes in slice_info.config) to short names
|
|
# These are the codes Bambu Studio uses internally
|
|
PRINTER_MODEL_ID_MAP = {
|
|
# X1 series
|
|
"C11": "X1C",
|
|
"C12": "X1",
|
|
"C13": "X1E",
|
|
# P1 series
|
|
"P1P": "P1P",
|
|
"P1S": "P1S",
|
|
# P2 series
|
|
"P2S": "P2S",
|
|
# X2 series
|
|
"N6": "X2D",
|
|
# A1 series
|
|
"A11": "A1",
|
|
"A12": "A1 Mini",
|
|
"N1": "A1",
|
|
"N2S": "A1 Mini",
|
|
"A04": "A1 Mini",
|
|
# H2 series (Office/H series)
|
|
"O1D": "H2D",
|
|
"O1E": "H2D Pro", # Some devices report O1E
|
|
"O2D": "H2D Pro", # Some devices report O2D
|
|
"O1C": "H2C",
|
|
"O1C2": "H2C",
|
|
"O1S": "H2S",
|
|
}
|
|
|
|
|
|
# Rod/rail type classification for maintenance tasks.
|
|
# Carbon rods: X1, P1 series (CoreXY with carbon fiber rods)
|
|
# Steel rods: P2S, X2D series (hardened steel linear shafts)
|
|
# Linear rails: A1, H2 series (linear rail motion system)
|
|
# Values must be uppercase with spaces stripped for normalized comparison.
|
|
CARBON_ROD_MODELS = frozenset(
|
|
[
|
|
# Display names (uppercase, no spaces)
|
|
"X1",
|
|
"X1C",
|
|
"X1E",
|
|
"P1P",
|
|
"P1S",
|
|
# Internal codes
|
|
"C11", # X1C
|
|
"C12", # X1
|
|
"C13", # X1E
|
|
]
|
|
)
|
|
|
|
STEEL_ROD_MODELS = frozenset(
|
|
[
|
|
# Display names (uppercase, no spaces)
|
|
"P2S",
|
|
"X2D",
|
|
# Internal codes
|
|
"N7", # P2S
|
|
"N6", # X2D
|
|
]
|
|
)
|
|
|
|
LINEAR_RAIL_MODELS = frozenset(
|
|
[
|
|
# Display names (uppercase, no spaces)
|
|
"A1",
|
|
"A1MINI",
|
|
"H2D",
|
|
"H2DPRO",
|
|
"H2C",
|
|
"H2S",
|
|
# Internal codes
|
|
"N1", # A1
|
|
"N2S", # A1 Mini
|
|
"A04", # A1 Mini (alternate)
|
|
"A11", # A1
|
|
"A12", # A1 Mini
|
|
"O1D", # H2D
|
|
"O1E", # H2D Pro
|
|
"O2D", # H2D Pro (alternate)
|
|
"O1C", # H2C
|
|
"O1C2", # H2C (dual nozzle variant)
|
|
"O1S", # H2S
|
|
]
|
|
)
|
|
|
|
|
|
# Models with an ethernet port.
|
|
# X1, P1P, A1, A1 Mini do NOT have ethernet.
|
|
ETHERNET_MODELS = frozenset(
|
|
[
|
|
# Display names (uppercase, no spaces)
|
|
"X1C",
|
|
"X1E",
|
|
"X2D",
|
|
"P1S",
|
|
"P2S",
|
|
"H2D",
|
|
"H2DPRO",
|
|
"H2C",
|
|
"H2S",
|
|
# Internal codes
|
|
"C11", # X1C
|
|
"C13", # X1E
|
|
"N6", # X2D
|
|
"P1S", # P1S
|
|
"O1D", # H2D
|
|
"O1E", # H2D Pro
|
|
"O2D", # H2D Pro (alternate)
|
|
"O1C", # H2C
|
|
"O1C2", # H2C (dual nozzle variant)
|
|
"O1S", # H2S
|
|
]
|
|
)
|
|
|
|
|
|
def has_ethernet(model: str | None) -> bool:
|
|
"""Return True if the printer model has an ethernet port."""
|
|
if not model:
|
|
return False
|
|
normalized = model.strip().upper().replace(" ", "").replace("-", "")
|
|
return normalized in ETHERNET_MODELS
|
|
|
|
|
|
def get_rod_type(model: str | None) -> str | None:
|
|
"""Return the rod/rail type for a printer model.
|
|
|
|
Returns:
|
|
"carbon" for X1/P1 series (carbon fiber rods),
|
|
"steel_rod" for P2S/X2D series (hardened steel rods),
|
|
"linear_rail" for A1/H2 series (linear rails),
|
|
None for unknown models.
|
|
"""
|
|
if not model:
|
|
return None
|
|
normalized = model.strip().upper().replace(" ", "").replace("-", "")
|
|
if normalized in CARBON_ROD_MODELS:
|
|
return "carbon"
|
|
if normalized in STEEL_ROD_MODELS:
|
|
return "steel_rod"
|
|
if normalized in LINEAR_RAIL_MODELS:
|
|
return "linear_rail"
|
|
return None
|
|
|
|
|
|
def normalize_printer_model_id(model_id: str | None) -> str | None:
|
|
"""Convert printer_model_id (internal code) to normalized short name.
|
|
|
|
Args:
|
|
model_id: The printer_model_id from slice_info.config (e.g., "C11", "O1D")
|
|
|
|
Returns:
|
|
Normalized short name (e.g., "X1C", "H2D") or the original ID if unknown.
|
|
"""
|
|
if not model_id:
|
|
return None
|
|
|
|
# Check known mappings
|
|
if model_id in PRINTER_MODEL_ID_MAP:
|
|
return PRINTER_MODEL_ID_MAP[model_id]
|
|
|
|
# Return original if unknown (might already be a short name)
|
|
return model_id
|
|
|
|
|
|
def normalize_printer_model(raw_model: str | None) -> str | None:
|
|
"""Convert 3MF printer_model to normalized short name.
|
|
|
|
Args:
|
|
raw_model: The printer_model string from 3MF metadata
|
|
(e.g., "Bambu Lab X1 Carbon")
|
|
|
|
Returns:
|
|
Normalized short name (e.g., "X1C") or None if input is empty.
|
|
Unknown models have "Bambu Lab " prefix stripped.
|
|
"""
|
|
if not raw_model:
|
|
return None
|
|
|
|
# Check known mappings first
|
|
if raw_model in PRINTER_MODEL_MAP:
|
|
return PRINTER_MODEL_MAP[raw_model]
|
|
|
|
# Strip "Bambu Lab " prefix for unknown models
|
|
stripped = raw_model.replace("Bambu Lab ", "").strip()
|
|
return stripped or None
|