printer_models.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. """Printer model normalization utilities.
  2. Converts 3MF printer model names (e.g., "Bambu Lab X1 Carbon") to
  3. normalized short names (e.g., "X1C") that match database storage.
  4. """
  5. # Map from 3MF printer_model strings to normalized short names
  6. PRINTER_MODEL_MAP = {
  7. "Bambu Lab X1 Carbon": "X1C",
  8. "Bambu Lab X1": "X1",
  9. "Bambu Lab X1E": "X1E",
  10. "Bambu Lab P1S": "P1S",
  11. "Bambu Lab P1P": "P1P",
  12. "Bambu Lab P2S": "P2S",
  13. "Bambu Lab A1": "A1",
  14. "Bambu Lab A1 Mini": "A1 Mini",
  15. "Bambu Lab A1 mini": "A1 Mini",
  16. # Bambu cloud rolled out a terse model-code rename mid-2026 (#1649);
  17. # 3MFs prepared with newer cloud presets may carry this short form.
  18. "Bambu Lab A1M": "A1 Mini",
  19. "Bambu Lab H2D": "H2D",
  20. "Bambu Lab H2D Pro": "H2D Pro",
  21. "Bambu Lab H2C": "H2C",
  22. "Bambu Lab H2S": "H2S",
  23. "Bambu Lab X2D": "X2D",
  24. "Bambu Lab A2L": "A2L",
  25. }
  26. # Map from printer_model_id (internal codes in slice_info.config) to short names
  27. # These are the codes Bambu Studio uses internally
  28. PRINTER_MODEL_ID_MAP = {
  29. # X1 series
  30. "C11": "X1C",
  31. "C12": "X1",
  32. "C13": "X1E",
  33. # P1 series
  34. "P1P": "P1P",
  35. "P1S": "P1S",
  36. # P2 series
  37. "P2S": "P2S",
  38. # X2 series
  39. "N6": "X2D",
  40. # A2 series (A2L is single-FDM + integrated cutter/plotter — single nozzle)
  41. "N9": "A2L",
  42. # A1 series
  43. "A11": "A1",
  44. "A12": "A1 Mini",
  45. "N1": "A1 Mini",
  46. "N2S": "A1",
  47. "A04": "A1 Mini",
  48. # H2 series (Office/H series)
  49. "O1D": "H2D",
  50. "O1E": "H2D Pro", # Some devices report O1E
  51. "O2D": "H2D Pro", # Some devices report O2D
  52. "O1C": "H2C",
  53. "O1C2": "H2C",
  54. "O1S": "H2S",
  55. }
  56. # Rod/rail type classification for maintenance tasks.
  57. # Carbon rods: X1, P1 series (CoreXY with carbon fiber rods)
  58. # Steel rods: P2S, X2D series (hardened steel linear shafts)
  59. # Linear rails: A1, H2 series (linear rail motion system)
  60. # Values must be uppercase with spaces stripped for normalized comparison.
  61. CARBON_ROD_MODELS = frozenset(
  62. [
  63. # Display names (uppercase, no spaces)
  64. "X1",
  65. "X1C",
  66. "X1E",
  67. "P1P",
  68. "P1S",
  69. # Internal codes
  70. "C11", # X1C
  71. "C12", # X1
  72. "C13", # X1E
  73. ]
  74. )
  75. STEEL_ROD_MODELS = frozenset(
  76. [
  77. # Display names (uppercase, no spaces)
  78. "P2S",
  79. "X2D",
  80. # Internal codes
  81. "N7", # P2S
  82. "N6", # X2D
  83. ]
  84. )
  85. LINEAR_RAIL_MODELS = frozenset(
  86. [
  87. # Display names (uppercase, no spaces)
  88. "A1",
  89. "A1MINI",
  90. "A2L",
  91. "H2D",
  92. "H2DPRO",
  93. "H2C",
  94. "H2S",
  95. # Internal codes
  96. "N1", # A1 Mini
  97. "N2S", # A1
  98. "N9", # A2L
  99. "A04", # A1 Mini (alternate)
  100. "A11", # A1
  101. "A12", # A1 Mini
  102. "O1D", # H2D
  103. "O1E", # H2D Pro
  104. "O2D", # H2D Pro (alternate)
  105. "O1C", # H2C
  106. "O1C2", # H2C (dual nozzle variant)
  107. "O1S", # H2S
  108. ]
  109. )
  110. # Models sold with a single nozzle flow variant, so a Standard / High Flow
  111. # choice on a K-profile is meaningless there. Derived from the slicer's own
  112. # rule (len(nozzle_volume) // len(nozzle_diameter) > 1 over the bundled Bambu
  113. # machine presets), not from nozzle count — P1P/P1S/P2S/X1/X1C/X1E/H2S are
  114. # single-nozzle and all carry two variants. Only the A-series has one.
  115. SINGLE_NOZZLE_FLOW_MODELS = frozenset(
  116. [
  117. # Display names (uppercase, no spaces)
  118. "A1",
  119. "A1MINI",
  120. "A2L",
  121. # Internal codes
  122. "N1", # A1 Mini
  123. "N2S", # A1
  124. "N9", # A2L
  125. "A04", # A1 Mini (alternate)
  126. "A11", # A1
  127. "A12", # A1 Mini
  128. ]
  129. )
  130. # Models without any external storage (MicroSD / SD card slot).
  131. # The A1 and A1 Mini ship with internal storage only — there is no
  132. # firmware-side "Store sent files on external storage" toggle and no
  133. # slicer-side equivalent surfaces one. The connection diagnostic's
  134. # external_storage check (printer_diagnostic.py) must skip on these
  135. # models instead of reporting fail from a 0-valued home_flag bit.
  136. NO_EXTERNAL_STORAGE_MODELS = frozenset(
  137. [
  138. # Display names (uppercase, no spaces)
  139. "A1",
  140. "A1MINI",
  141. # Internal codes
  142. "N1", # A1 Mini
  143. "N2S", # A1
  144. "A04", # A1 Mini (alternate)
  145. "A11", # A1
  146. "A12", # A1 Mini
  147. ]
  148. )
  149. # Models that HAVE a MicroSD slot but expose NO reachable control to enable
  150. # the "Store sent files on external storage" option. The toggle only renders
  151. # in Bambu Studio when the printer publishes the
  152. # `support_save_remote_print_file_to_storage` capability in its live status;
  153. # current P1-series firmware (through 01.10.00.00) never publishes it, and
  154. # the P1S/P1P have no on-printer screen, so `store_to_sdcard` (home_flag bit
  155. # 11) is stuck at False with no way for the user to change it. The
  156. # external_storage diagnostic must therefore skip (not fail) on these models
  157. # — a hard fail would be permanently unresolvable (#2524). If a future
  158. # firmware surfaces the capability, remove the model here and the check
  159. # reactivates. Bambu Lab's own storage-cache wiki lists P1 Series as "Not
  160. # Supported", corroborating this.
  161. NO_REMOTE_STORAGE_TOGGLE_MODELS = frozenset(
  162. [
  163. # Display names (uppercase, no spaces)
  164. "P1S",
  165. "P1P",
  166. ]
  167. )
  168. # Models with an ethernet port.
  169. # X1, P1P, A1, A1 Mini do NOT have ethernet.
  170. ETHERNET_MODELS = frozenset(
  171. [
  172. # Display names (uppercase, no spaces)
  173. "X1C",
  174. "X1E",
  175. "X2D",
  176. "P1S",
  177. "P2S",
  178. "H2D",
  179. "H2DPRO",
  180. "H2C",
  181. "H2S",
  182. # Internal codes
  183. "C11", # X1C
  184. "C13", # X1E
  185. "N6", # X2D
  186. "P1S", # P1S
  187. "O1D", # H2D
  188. "O1E", # H2D Pro
  189. "O2D", # H2D Pro (alternate)
  190. "O1C", # H2C
  191. "O1C2", # H2C (dual nozzle variant)
  192. "O1S", # H2S
  193. ]
  194. )
  195. # Dual-nozzle (dual-extruder) printers. Single source of truth for nozzle
  196. # class — consumed by ``BambuMQTTClient.start_print``, the K-profile routes,
  197. # and the re-slice nozzle-class guard (previously an inline model tuple
  198. # duplicated across all three). Re-slicing a model laid out for a single-nozzle
  199. # printer onto one of these — or vice versa — is not yet supported: the source
  200. # 3MF's embedded single-nozzle filament/extruder layout is not a valid
  201. # dual-nozzle project and BambuStudio's multi-extruder validator rejects it.
  202. DUAL_NOZZLE_MODELS = frozenset(
  203. [
  204. # Display names (uppercase, no spaces)
  205. "H2D",
  206. "H2DPRO",
  207. "H2C",
  208. "X2D",
  209. # Internal codes
  210. "O1D", # H2D
  211. "O1E", # H2D Pro
  212. "O2D", # H2D Pro (alternate)
  213. "O1C", # H2C
  214. "O1C2", # H2C (dual nozzle variant)
  215. "N6", # X2D
  216. ]
  217. )
  218. # Models where Bambu's own firmware/UI names the enclosure fan (big_fan2 /
  219. # airduct part id 3) "Exhaust" rather than "Chamber". On these the printer's
  220. # touchscreen and Bambu Studio both call it the exhaust fan, and on the P2S it
  221. # is an add-on kit rather than built-in hardware. Other enclosed models
  222. # (X1 / P1S / H2 series) keep the "Chamber" naming.
  223. EXHAUST_FAN_LABEL_MODELS = frozenset(
  224. [
  225. # Display names (uppercase, no spaces)
  226. "P2S",
  227. "X2D",
  228. # Internal codes
  229. "N7", # P2S
  230. "N6", # X2D
  231. ]
  232. )
  233. def uses_exhaust_fan_label(model: str | None) -> bool:
  234. """Return True if this model calls the big_fan2 enclosure fan "Exhaust".
  235. P2S/X2D name that fan "Exhaust" in Bambu's firmware/UI; everything else
  236. enclosed calls it the chamber fan. Used so the UI badge and the API
  237. response message agree on what the user sees.
  238. """
  239. if not model:
  240. return False
  241. normalized = model.strip().upper().replace(" ", "").replace("-", "")
  242. return normalized in EXHAUST_FAN_LABEL_MODELS
  243. def has_ethernet(model: str | None) -> bool:
  244. """Return True if the printer model has an ethernet port."""
  245. if not model:
  246. return False
  247. normalized = model.strip().upper().replace(" ", "").replace("-", "")
  248. return normalized in ETHERNET_MODELS
  249. def has_external_storage(model: str | None) -> bool:
  250. """Return True if the printer model can have a MicroSD / external storage slot.
  251. Defaults to True when the model is unknown — the diagnostic only flips
  252. its check on for the explicit no-storage list. New models added to the
  253. Bambu lineup without a slot must be added to ``NO_EXTERNAL_STORAGE_MODELS``
  254. or the diagnostic will continue to evaluate ``store_to_sdcard`` against
  255. a hardware feature the printer doesn't have.
  256. """
  257. if not model:
  258. return True
  259. normalized = model.strip().upper().replace(" ", "").replace("-", "")
  260. return normalized not in NO_EXTERNAL_STORAGE_MODELS
  261. def has_remote_storage_toggle(model: str | None) -> bool:
  262. """Return True if the model exposes a reachable control for the
  263. "Store sent files on external storage" option.
  264. False for P1-series (has an SD slot, but no on-printer screen and no
  265. published `support_save_remote_print_file_to_storage` capability, so the
  266. Bambu Studio toggle never renders). The external_storage diagnostic uses
  267. this to skip rather than report an unresolvable fail (#2524). Defaults to
  268. True for unknown models so the check keeps working on anything not
  269. explicitly listed.
  270. """
  271. if not model:
  272. return True
  273. normalized = model.strip().upper().replace(" ", "").replace("-", "")
  274. return normalized not in NO_REMOTE_STORAGE_TOGGLE_MODELS
  275. def is_dual_nozzle_model(model: str | None) -> bool:
  276. """Return True if the printer model has two nozzles (H2D family / X2D)."""
  277. if not model:
  278. return False
  279. normalized = model.strip().upper().replace(" ", "").replace("-", "")
  280. return normalized in DUAL_NOZZLE_MODELS
  281. def supports_nozzle_flow_type(model: str | None) -> bool:
  282. """Return True if the model offers a Standard / High Flow nozzle choice.
  283. A K-profile is filed under a ``nozzle_id`` of the form ``HS00-0.4``
  284. (Standard) or ``HH00-0.4`` (High Flow), so the flow type is part of the
  285. profile's identity on any printer where both exist — and meaningless noise
  286. on one where only a single variant is sold.
  287. The split is NOT the nozzle count: P1S, P2S, X1C and H2S are single-nozzle
  288. and all offer both flows. BambuStudio/OrcaSlicer derive the same capability
  289. from the machine preset — ``support_nozzle_volume()`` is
  290. ``len(nozzle_volume) // len(nozzle_diameter) > 1`` — and every bundled
  291. Bambu profile evaluated against that formula puts only the A-series on the
  292. "one variant" side (A1 and A1 Mini at 1, A2L at 1; everything from P1P
  293. upward at 2 or more per extruder).
  294. Defaults to True for unknown models: offering the choice on a printer that
  295. turns out to have one flow type costs the user a redundant dropdown, while
  296. hiding it on one that has two makes half its calibration table
  297. unreachable.
  298. """
  299. if not model:
  300. return True
  301. normalized = model.strip().upper().replace(" ", "").replace("-", "")
  302. return normalized not in SINGLE_NOZZLE_FLOW_MODELS
  303. def get_rod_type(model: str | None) -> str | None:
  304. """Return the rod/rail type for a printer model.
  305. Returns:
  306. "carbon" for X1/P1 series (carbon fiber rods),
  307. "steel_rod" for P2S/X2D series (hardened steel rods),
  308. "linear_rail" for A1/H2 series (linear rails),
  309. None for unknown models.
  310. """
  311. if not model:
  312. return None
  313. normalized = model.strip().upper().replace(" ", "").replace("-", "")
  314. if normalized in CARBON_ROD_MODELS:
  315. return "carbon"
  316. if normalized in STEEL_ROD_MODELS:
  317. return "steel_rod"
  318. if normalized in LINEAR_RAIL_MODELS:
  319. return "linear_rail"
  320. return None
  321. # G-code interchange families (#2578). A sliced 3MF may target a different
  322. # model ONLY within its family: same kinematics, build volume and G-code
  323. # dialect. X1/P1 series are the one proven-interchangeable group (256mm
  324. # CoreXY, single nozzle — mixed farms intentionally run X1-sliced jobs on
  325. # P1S/P1P). Everything else is exact-match only; extend deliberately, never
  326. # by assumption — a wrong entry here dispatches G-code onto hardware it was
  327. # not sliced for.
  328. # Short display names only (uppercase, no spaces) — is_gcode_compatible()
  329. # resolves internal codes (C11, O1D, ...) to short names before lookup.
  330. GCODE_COMPAT_FAMILIES = (frozenset(["X1", "X1C", "X1E", "P1P", "P1S"]),)
  331. def is_gcode_compatible(sliced_for_model: str | None, target_model: str | None) -> bool:
  332. """Return True when G-code sliced for one model may be dispatched to the other.
  333. Unknown/missing metadata on either side returns True — we can only
  334. validate what the 3MF declares, and legacy files without
  335. ``sliced_for_model`` must keep working.
  336. """
  337. if not sliced_for_model or not target_model:
  338. return True
  339. def _norm(model: str) -> str:
  340. # Internal codes (e.g. "C11") → short names first, so "C11" vs "X1C"
  341. # compares equal instead of leaning on family membership.
  342. resolved = PRINTER_MODEL_ID_MAP.get(model.strip(), model)
  343. return resolved.strip().upper().replace(" ", "").replace("-", "")
  344. a = _norm(sliced_for_model)
  345. b = _norm(target_model)
  346. if a == b:
  347. return True
  348. return any(a in family and b in family for family in GCODE_COMPAT_FAMILIES)
  349. def normalize_printer_model_id(model_id: str | None) -> str | None:
  350. """Convert printer_model_id (internal code) to normalized short name.
  351. Args:
  352. model_id: The printer_model_id from slice_info.config (e.g., "C11", "O1D")
  353. Returns:
  354. Normalized short name (e.g., "X1C", "H2D") or the original ID if unknown.
  355. """
  356. if not model_id:
  357. return None
  358. # Check known mappings
  359. if model_id in PRINTER_MODEL_ID_MAP:
  360. return PRINTER_MODEL_ID_MAP[model_id]
  361. # Return original if unknown (might already be a short name)
  362. return model_id
  363. def normalize_printer_model(raw_model: str | None) -> str | None:
  364. """Convert 3MF printer_model to normalized short name.
  365. Args:
  366. raw_model: The printer_model string from 3MF metadata
  367. (e.g., "Bambu Lab X1 Carbon")
  368. Returns:
  369. Normalized short name (e.g., "X1C") or None if input is empty.
  370. Unknown models have "Bambu Lab " prefix stripped.
  371. """
  372. if not raw_model:
  373. return None
  374. # Check known mappings first
  375. if raw_model in PRINTER_MODEL_MAP:
  376. return PRINTER_MODEL_MAP[raw_model]
  377. # Strip "Bambu Lab " prefix for unknown models
  378. stripped = raw_model.replace("Bambu Lab ", "").strip()
  379. return stripped or None