From 25e020f5c927350a7491aef21c318604d00d1abc Mon Sep 17 00:00:00 2001 From: SoftFever Date: Fri, 4 Sep 2026 13:31:19 +0800 Subject: [PATCH] Add the generated map from Orca filament ids to Bambu catalog ids scripts/update_bambu_filament_ids.py derives resources/printers/bambu_filament_ids.json from BambuStudio's own shipped BBL bundle (cloned from upstream, or read from a local checkout via --bambustudio-dir), pairing each Bambu catalog id with the Orca filament_id we already ship for that product where we ship one, else a freshly generated one. Nothing reads the map yet; later work uses it to translate ids at the Bambu printer boundary. --- resources/printers/bambu_filament_ids.json | 607 +++++++++++++++++++++ scripts/tests/test_filament_id.py | 133 +++++ scripts/update_bambu_filament_ids.py | 291 ++++++++++ 3 files changed, 1031 insertions(+) create mode 100644 resources/printers/bambu_filament_ids.json create mode 100644 scripts/update_bambu_filament_ids.py diff --git a/resources/printers/bambu_filament_ids.json b/resources/printers/bambu_filament_ids.json new file mode 100644 index 0000000000..b21946f1a3 --- /dev/null +++ b/resources/printers/bambu_filament_ids.json @@ -0,0 +1,607 @@ +{ + "bambustudio_commit": "66e405477", + "filaments": { + "OF0UJcb6": { + "bambu_id": "GFG60", + "name": "PolyLite PETG", + "type": "PETG", + "vendor": "Polymaker" + }, + "OF0wBGNx": { + "bambu_id": "GFB51", + "name": "Bambu ASA-CF", + "type": "ASA-CF", + "vendor": "Bambu Lab" + }, + "OF1UNk9P": { + "bambu_id": "GFP97", + "name": "Generic PP", + "type": "PP", + "vendor": "Generic" + }, + "OF2GCW1l": { + "bambu_id": "GFSNL08", + "name": "SUNLU PETG", + "type": "PETG", + "vendor": "SUNLU" + }, + "OF2VFe4J": { + "bambu_id": "GFN04", + "name": "Bambu PAHT-CF", + "type": "PA-CF", + "vendor": "Bambu Lab" + }, + "OF342jVN": { + "bambu_id": "GFA10", + "name": "Bambu PLA Tough+", + "type": "PLA", + "vendor": "Bambu Lab" + }, + "OF4RvVuU": { + "bambu_id": "GFT02", + "name": "Bambu PPS-CF", + "type": "PPS-CF", + "vendor": "Bambu Lab" + }, + "OF54B0S0": { + "bambu_id": "GFN06", + "name": "Bambu PPA-CF", + "type": "PPA-CF", + "vendor": "Bambu Lab" + }, + "OF5CgdDq": { + "bambu_id": "GFL00", + "name": "PolyLite PLA", + "type": "PLA", + "vendor": "Polymaker" + }, + "OF6rdQ6M": { + "bambu_id": "GFT98", + "name": "Generic PPS-CF", + "type": "PPS-CF", + "vendor": "Generic" + }, + "OF74KeQR": { + "bambu_id": "GFR98", + "name": "Generic PHA", + "type": "PHA", + "vendor": "Generic" + }, + "OF7lOgYF": { + "bambu_id": "GFG96", + "name": "Generic PETG HF", + "type": "PETG", + "vendor": "Generic" + }, + "OF8Tlg47": { + "bambu_id": "GFN96", + "name": "Generic PPA-GF", + "type": "PPA-GF", + "vendor": "Generic" + }, + "OF8tPByX": { + "bambu_id": "GFN97", + "name": "Generic PPA-CF", + "type": "PPA-CF", + "vendor": "Generic" + }, + "OF9OlTWH": { + "bambu_id": "GFA07", + "name": "Bambu PLA Marble", + "type": "PLA", + "vendor": "Bambu Lab" + }, + "OFAnyRUI": { + "bambu_id": "GFS02", + "name": "Bambu Support For PLA", + "type": "PLA", + "vendor": "Bambu Lab" + }, + "OFBSW57R": { + "bambu_id": "GFA11", + "name": "Bambu PLA Aero", + "type": "PLA-AERO", + "vendor": "Bambu Lab" + }, + "OFBw6eEG": { + "bambu_id": "GFL05", + "name": "Overture Matte PLA", + "type": "PLA", + "vendor": "Overture" + }, + "OFCD8qiU": { + "bambu_id": "GFR99", + "name": "Generic EVA", + "type": "EVA", + "vendor": "Generic" + }, + "OFDGigEI": { + "bambu_id": "GFA13", + "name": "Bambu PLA Dynamic", + "type": "PLA", + "vendor": "Bambu Lab" + }, + "OFDSrzZ8": { + "bambu_id": "GFL99", + "name": "Generic PLA", + "type": "PLA", + "vendor": "Generic" + }, + "OFDvXujf": { + "bambu_id": "GFS99", + "name": "Generic PVA", + "type": "PVA", + "vendor": "Generic" + }, + "OFDxfPgH": { + "bambu_id": "GFA08", + "name": "Bambu PLA Sparkle", + "type": "PLA", + "vendor": "Bambu Lab" + }, + "OFEGNJD4": { + "bambu_id": "GFA05", + "name": "Bambu PLA Silk", + "type": "PLA", + "vendor": "Bambu Lab" + }, + "OFEkPBwx": { + "bambu_id": "GFA12", + "name": "Bambu PLA Glow", + "type": "PLA", + "vendor": "Bambu Lab" + }, + "OFEswT5W": { + "bambu_id": "GFA50", + "name": "Bambu PLA-CF", + "type": "PLA-CF", + "vendor": "Bambu Lab" + }, + "OFFNYwWR": { + "bambu_id": "GFN03", + "name": "Bambu PA-CF", + "type": "PA-CF", + "vendor": "Bambu Lab" + }, + "OFFbSnCD": { + "bambu_id": "GFA17", + "name": "Bambu PLA Translucent", + "type": "PLA", + "vendor": "Bambu Lab" + }, + "OFFvzqcd": { + "bambu_id": "GFG00", + "name": "Bambu PETG Basic", + "type": "PETG", + "vendor": "Bambu Lab" + }, + "OFHWSM21": { + "bambu_id": "GFL54", + "name": "Fiberon PET-CF", + "type": "PET-CF", + "vendor": "Polymaker" + }, + "OFHa48An": { + "bambu_id": "GFT01", + "name": "Bambu PET-CF", + "type": "PET-CF", + "vendor": "Bambu Lab" + }, + "OFHmPYRy": { + "bambu_id": "GFSNL04", + "name": "SUNLU PLA+ 2.0", + "type": "PLA", + "vendor": "SUNLU" + }, + "OFIBbYO5": { + "bambu_id": "GFU02", + "name": "Bambu TPU for AMS", + "type": "TPU-AMS", + "vendor": "Bambu Lab" + }, + "OFIfnzxC": { + "bambu_id": "GFS05", + "name": "Bambu Support For PLA/PETG", + "type": "PLA", + "vendor": "Bambu Lab" + }, + "OFKhMPeX": { + "bambu_id": "GFC00", + "name": "Bambu PC", + "type": "PC", + "vendor": "Bambu Lab" + }, + "OFLJ8S6I": { + "bambu_id": "GFSNL07", + "name": "SUNLU Wood PLA", + "type": "PLA", + "vendor": "SUNLU" + }, + "OFLPAxz3": { + "bambu_id": "GFB98", + "name": "Generic ASA", + "type": "ASA", + "vendor": "Generic" + }, + "OFLakOUI": { + "bambu_id": "GFC99", + "name": "Generic PC", + "type": "PC", + "vendor": "Generic" + }, + "OFMTK0UC": { + "bambu_id": "GFS03", + "name": "Bambu Support For PA/PET", + "type": "PA", + "vendor": "Bambu Lab" + }, + "OFMUWNkp": { + "bambu_id": "GFSNL03", + "name": "SUNLU PLA+", + "type": "PLA", + "vendor": "SUNLU" + }, + "OFMjtqTC": { + "bambu_id": "GFA16", + "name": "Bambu PLA Wood", + "type": "PLA", + "vendor": "Bambu Lab" + }, + "OFNk8bxk": { + "bambu_id": "GFN08", + "name": "Bambu PA6-GF", + "type": "PA-GF", + "vendor": "Bambu Lab" + }, + "OFOvv91M": { + "bambu_id": "GFB60", + "name": "PolyLite ABS", + "type": "ABS", + "vendor": "Polymaker" + }, + "OFPklMI1": { + "bambu_id": "GFP99", + "name": "Generic PE", + "type": "PE", + "vendor": "Generic" + }, + "OFQ3e56w": { + "bambu_id": "GFA15", + "name": "Bambu PLA Galaxy", + "type": "PLA", + "vendor": "Bambu Lab" + }, + "OFQHiNJs": { + "bambu_id": "GFS01", + "name": "Bambu Support G", + "type": "PA", + "vendor": "Bambu Lab" + }, + "OFQLcbps": { + "bambu_id": "GFN98", + "name": "Generic PA-CF", + "type": "PA-CF", + "vendor": "Generic" + }, + "OFS2tn6G": { + "bambu_id": "GFL53", + "name": "Fiberon PA612-CF", + "type": "PA", + "vendor": "Polymaker" + }, + "OFTHDCqA": { + "bambu_id": "GFA19", + "name": "Bambu PLA Pure", + "type": "PLA", + "vendor": "Bambu Lab" + }, + "OFTRZ8Y4": { + "bambu_id": "GFG50", + "name": "Bambu PETG-CF", + "type": "PETG-CF", + "vendor": "Bambu Lab" + }, + "OFUanySo": { + "bambu_id": "GFA06", + "name": "Bambu PLA Silk+", + "type": "PLA", + "vendor": "Bambu Lab" + }, + "OFVCkX5w": { + "bambu_id": "GFU01", + "name": "Bambu TPU 95A", + "type": "TPU", + "vendor": "Bambu Lab" + }, + "OFW29a9R": { + "bambu_id": "GFL01", + "name": "PolyTerra PLA", + "type": "PLA", + "vendor": "Polymaker" + }, + "OFWbdGsC": { + "bambu_id": "GFL98", + "name": "Generic PLA-CF", + "type": "PLA-CF", + "vendor": "Generic" + }, + "OFX0ycRQ": { + "bambu_id": "GFL52", + "name": "Fiberon PA12-CF", + "type": "PA-CF", + "vendor": "Polymaker" + }, + "OFXIbw5D": { + "bambu_id": "GFA01", + "name": "Bambu PLA Matte", + "type": "PLA", + "vendor": "Bambu Lab" + }, + "OFXkm8q1": { + "bambu_id": "GFP96", + "name": "Generic PP-CF", + "type": "PP-CF", + "vendor": "Generic" + }, + "OFXzQ4yL": { + "bambu_id": "GFB02", + "name": "Bambu ASA-Aero", + "type": "ASA-AERO", + "vendor": "Bambu Lab" + }, + "OFY9muEs": { + "bambu_id": "GFB99", + "name": "Generic ABS", + "type": "ABS", + "vendor": "Generic" + }, + "OFYPdQJh": { + "bambu_id": "GFG99", + "name": "Generic PETG", + "type": "PETG", + "vendor": "Generic" + }, + "OFaQMgRH": { + "bambu_id": "GFA09", + "name": "Bambu PLA Tough", + "type": "PLA", + "vendor": "Bambu Lab" + }, + "OFc3xdm9": { + "bambu_id": "GFL04", + "name": "Overture PLA", + "type": "PLA", + "vendor": "Overture" + }, + "OFcytyoA": { + "bambu_id": "GFL06", + "name": "Fiberon PETG-ESD", + "type": "PETG", + "vendor": "Polymaker" + }, + "OFd0Fv0k": { + "bambu_id": "GFP98", + "name": "Generic PE-CF", + "type": "PE-CF", + "vendor": "Generic" + }, + "OFdyfQvU": { + "bambu_id": "GFG03", + "name": "Bambu PETG Matte", + "type": "PETG", + "vendor": "Bambu Lab" + }, + "OFesA6rF": { + "bambu_id": "GFL96", + "name": "Generic PLA Silk", + "type": "PLA", + "vendor": "Generic" + }, + "OFf6mfQO": { + "bambu_id": "GFS06", + "name": "Bambu Support for ABS", + "type": "ABS", + "vendor": "Bambu Lab" + }, + "OFfBpSRI": { + "bambu_id": "GFB01", + "name": "Bambu ASA", + "type": "ASA", + "vendor": "Bambu Lab" + }, + "OFg57Nmc": { + "bambu_id": "GFC01", + "name": "Bambu PC FR", + "type": "PC", + "vendor": "Bambu Lab" + }, + "OFg8ndtj": { + "bambu_id": "GFN99", + "name": "Generic PA", + "type": "PA", + "vendor": "Generic" + }, + "OFgbpcy9": { + "bambu_id": "GFU99", + "name": "Generic TPU", + "type": "TPU", + "vendor": "Generic" + }, + "OFhuaUQB": { + "bambu_id": "GFB00", + "name": "Bambu ABS", + "type": "ABS", + "vendor": "Bambu Lab" + }, + "OFk8t9mz": { + "bambu_id": "GFL55", + "name": "Fiberon PETG-rCF", + "type": "PETG-CF", + "vendor": "Polymaker" + }, + "OFkOviHk": { + "bambu_id": "GFL50", + "name": "Fiberon PA6-CF", + "type": "PA6-CF", + "vendor": "Polymaker" + }, + "OFknl9Iz": { + "bambu_id": "GFB50", + "name": "Bambu ABS-GF", + "type": "ABS-GF", + "vendor": "Bambu Lab" + }, + "OFlfuj2k": { + "bambu_id": "GFU04", + "name": "Bambu TPU 85A", + "type": "TPU", + "vendor": "Bambu Lab" + }, + "OFmN2lvw": { + "bambu_id": "GFU98", + "name": "Generic TPU for AMS", + "type": "TPU-AMS", + "vendor": "Generic" + }, + "OFmpMwxS": { + "bambu_id": "GFL95", + "name": "Generic PLA High Speed", + "type": "PLA", + "vendor": "Generic" + }, + "OFnfxTvi": { + "bambu_id": "GFA18", + "name": "Bambu PLA Lite", + "type": "PLA", + "vendor": "Bambu Lab" + }, + "OFniMuTN": { + "bambu_id": "GFN05", + "name": "Bambu PA6-CF", + "type": "PA6-CF", + "vendor": "Bambu Lab" + }, + "OFo2UF2C": { + "bambu_id": "GFS97", + "name": "Generic BVOH", + "type": "BVOH", + "vendor": "Generic" + }, + "OFoYSJKi": { + "bambu_id": "GFG98", + "name": "Generic PETG-CF", + "type": "PETG-CF", + "vendor": "Generic" + }, + "OFoiVqVM": { + "bambu_id": "GFA00", + "name": "Bambu PLA Basic", + "type": "PLA", + "vendor": "Bambu Lab" + }, + "OFovEIbw": { + "bambu_id": "GFG02", + "name": "Bambu PETG HF", + "type": "PETG", + "vendor": "Bambu Lab" + }, + "OFpPGSKG": { + "bambu_id": "GFSNL05", + "name": "SUNLU Silk PLA+", + "type": "PLA", + "vendor": "SUNLU" + }, + "OFpW4gdi": { + "bambu_id": "GFSNL02", + "name": "SUNLU PLA Matte", + "type": "PLA", + "vendor": "SUNLU" + }, + "OFq9svOz": { + "bambu_id": "GFT97", + "name": "Generic PPS", + "type": "PPS", + "vendor": "Generic" + }, + "OFqINlYj": { + "bambu_id": "GFL03", + "name": "eSUN PLA+", + "type": "PLA", + "vendor": "eSUN" + }, + "OFrKLeE3": { + "bambu_id": "GFA02", + "name": "Bambu PLA Metal", + "type": "PLA", + "vendor": "Bambu Lab" + }, + "OFsFon5l": { + "bambu_id": "GFS98", + "name": "Generic HIPS", + "type": "HIPS", + "vendor": "Generic" + }, + "OFsHSVZc": { + "bambu_id": "GFS00", + "name": "Bambu Support W", + "type": "PLA", + "vendor": "Bambu Lab" + }, + "OFsijjtH": { + "bambu_id": "GFP95", + "name": "Generic PP-GF", + "type": "PP-GF", + "vendor": "Generic" + }, + "OFtkLO6q": { + "bambu_id": "GFU03", + "name": "Bambu TPU 90A", + "type": "TPU", + "vendor": "Bambu Lab" + }, + "OFu1evlr": { + "bambu_id": "GFG97", + "name": "Generic PCTG", + "type": "PCTG", + "vendor": "Generic" + }, + "OFvKUnLh": { + "bambu_id": "GFU00", + "name": "Bambu TPU 95A HF", + "type": "TPU", + "vendor": "Bambu Lab" + }, + "OFvrXuV7": { + "bambu_id": "GFB61", + "name": "PolyLite ASA", + "type": "ASA", + "vendor": "Polymaker" + }, + "OFwPrlCM": { + "bambu_id": "GFG01", + "name": "Bambu PETG Translucent", + "type": "PETG", + "vendor": "Bambu Lab" + }, + "OFwaYjL6": { + "bambu_id": "GFL51", + "name": "Fiberon PA6-GF", + "type": "PA-GF", + "vendor": "Polymaker" + }, + "OFxUwUeW": { + "bambu_id": "GFSNL06", + "name": "SUNLU PLA Marble", + "type": "PLA", + "vendor": "SUNLU" + }, + "OFzyIxba": { + "bambu_id": "GFS04", + "name": "Bambu PVA", + "type": "PVA", + "vendor": "Bambu Lab" + } + }, + "generated": "2026-09-04", + "source": "https://github.com/bambulab/BambuStudio" +} diff --git a/scripts/tests/test_filament_id.py b/scripts/tests/test_filament_id.py index ef61992784..2ff0e42a7c 100644 --- a/scripts/tests/test_filament_id.py +++ b/scripts/tests/test_filament_id.py @@ -18,6 +18,7 @@ import uuid sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) import assign_filament_ids as afi # noqa: E402 +import update_bambu_filament_ids as ubfi # noqa: E402 REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) REAL_PROFILES = os.path.join(REPO_ROOT, "resources", "profiles") @@ -1081,5 +1082,137 @@ class TestReviewFixes(SyntheticTreeCase): self.assertIn(afi.generate_filament_id("OV", "PLA", "Orphan PLA"), out) +# --------------------------------------------------------------------------- +# scripts/update_bambu_filament_ids.py: the generated Bambu catalog id map +# --------------------------------------------------------------------------- + +class TestBambuMap(unittest.TestCase): + def _bs_tree(self, families): + # families: list of (family, bambu_id, vendor, type); builds a minimal BBL bundle + # with an @base per family carrying the id and one instantiated child. + t = SyntheticTree() + self.addCleanup(t.cleanup) + presets = [] + for family, bambu_id, vendor, ftype in families: + presets.append(preset(f"{family} @base", filament_id=bambu_id, + instantiation=False, filament_vendor=vendor, + filament_type=ftype)) + presets.append(preset(f"{family} @P1", inherits=f"{family} @base", + compatible_printers=["P1 0.4 nozzle"])) + t.add_vendor("BBL", presets) + filaments, errors = afi.load_vendor_filaments(t.profiles, "BBL") + self.assertEqual(errors, []) + return filaments + + def test_one_row_per_family(self): + rows = ubfi.derive_rows(self._bs_tree([("Bambu ABS", "GFB00", "Bambu Lab", "ABS")]), {}) + self.assertEqual(rows, {afi.generate_filament_id("Bambu Lab", "ABS", "Bambu ABS"): + {"bambu_id": "GFB00", "vendor": "Bambu Lab", "type": "ABS", "name": "Bambu ABS"}}) + + def test_shared_bambu_id_is_an_error(self): + with self.assertRaises(SystemExit): + ubfi.derive_rows(self._bs_tree([("A", "GFX00", "V", "PLA"), ("B", "GFX00", "V", "PLA")]), {}) + + def test_reuses_the_id_we_ship_for_that_triple(self): + rows = ubfi.derive_rows(self._bs_tree([("Bambu ABS", "GFB00", "Bambu Lab", "ABS")]), + {("Bambu Lab", "ABS", "Bambu ABS"): "OFsalted1"}) + self.assertIn("OFsalted1", rows) + + @unittest.skipUnless(os.path.isdir("/Users/lijiang/codes/BambuStudio/resources/profiles"), "no local clone") + def test_local_clone_yields_the_catalog(self): + rows = ubfi.derive_rows(afi.load_vendor_filaments("/Users/lijiang/codes/BambuStudio/resources/profiles", "BBL")[0], {}) + self.assertEqual(len(rows), 100) + self.assertEqual(len({r["bambu_id"] for r in rows.values()}), 100) + + +class TestOrcaTriplesFromAnalysis(unittest.TestCase): + def _tree(self): + t = SyntheticTree() + self.addCleanup(t.cleanup) + t.add_vendor("VendorA", [ + # Same triple declared under two different ids on purpose: mirrors + # a real, sanctioned case (Cubicon's xCeler line keeps its own id + # per printer instead of inheriting the family's @base id). + # Ambiguous, but must stay harmless unless something needs it. + preset("Ambig PLA @base", filament_id="OF111111", instantiation=False, + filament_vendor="V", filament_type="PLA"), + preset("Ambig PLA @P1", filament_id="OF222222", inherits="Ambig PLA @base", + compatible_printers=["P1"]), + # An unambiguous family elsewhere in the same tree. + preset("Needed PLA @base", filament_id="OF333333", instantiation=False, + filament_vendor="V2", filament_type="PLA"), + preset("Needed PLA @P1", inherits="Needed PLA @base", + compatible_printers=["P1"]), + ]) + return afi.analyze_tree(t.profiles) + + def test_ambiguity_outside_needed_triples_is_ignored(self): + analysis = self._tree() + result = ubfi.orca_triples_from_analysis(analysis, {("V2", "PLA", "Needed PLA")}) + self.assertEqual(result, {("V2", "PLA", "Needed PLA"): "OF333333"}) + + def test_ambiguity_inside_needed_triples_is_an_error(self): + analysis = self._tree() + with self.assertRaises(SystemExit): + ubfi.orca_triples_from_analysis(analysis, {("V", "PLA", "Ambig PLA")}) + + +class TestWriteMap(unittest.TestCase): + def test_format(self): + d = tempfile.mkdtemp(prefix="bambu_map_test_") + self.addCleanup(shutil.rmtree, d, ignore_errors=True) + # nested, not-yet-existing directory: write_map must create it + path = os.path.join(d, "out", "bambu_filament_ids.json") + rows = {"OFabc123": {"bambu_id": "GFB00", "vendor": "Bambu Lab", "type": "ABS", "name": "Bambu ABS"}} + + ubfi.write_map(path, rows, "66e405477", "2026-09-04") + + with open(path, "rb") as f: + raw = f.read() + self.assertTrue(raw.endswith(b"\n")) + self.assertNotIn(b"\r", raw) + text = raw.decode("utf-8") + self.assertEqual(json.loads(text), { + "source": "https://github.com/bambulab/BambuStudio", + "bambustudio_commit": "66e405477", + "generated": "2026-09-04", + "filaments": rows, + }) + # sorted top-level keys + self.assertLess(text.index('"bambustudio_commit"'), text.index('"filaments"')) + self.assertLess(text.index('"filaments"'), text.index('"generated"')) + self.assertLess(text.index('"generated"'), text.index('"source"')) + + +class TestDriftReport(unittest.TestCase): + def test_reports_both_directions(self): + t = SyntheticTree() + self.addCleanup(t.cleanup) + t.add_vendor("BBL", [ + preset("Match PLA @base", filament_id="GFX01", instantiation=False, + filament_vendor="V", filament_type="PLA"), + preset("Match PLA @P1", inherits="Match PLA @base", + compatible_printers=["P1"]), + preset("Orphan PLA @base", filament_id="GFX03", instantiation=False, + filament_vendor="V", filament_type="PLA"), + preset("Orphan PLA @P1", inherits="Orphan PLA @base", + compatible_printers=["P1"]), + ]) + rows = { + # matches the BBL bundle's "Match PLA" family: no drift either way + "OFmatch01": {"bambu_id": "GFX01", "vendor": "V", "type": "PLA", "name": "Match PLA"}, + # no family of this identity in the BBL bundle above + "OFghost01": {"bambu_id": "GFX02", "vendor": "V", "type": "PLA", "name": "Upstream Only PLA"}, + } + orca_analysis = afi.analyze_tree(t.profiles) + + lines = ubfi.drift_report(rows, orca_analysis) + + report = "\n".join(lines) + self.assertIn("Upstream Only PLA", report) + self.assertIn("Orca BBL families with no row: 1", report) + self.assertNotIn("Match PLA", report) # the matched family is not drift + + if __name__ == "__main__": unittest.main() diff --git a/scripts/update_bambu_filament_ids.py b/scripts/update_bambu_filament_ids.py new file mode 100644 index 0000000000..744fcc0401 --- /dev/null +++ b/scripts/update_bambu_filament_ids.py @@ -0,0 +1,291 @@ +#!/usr/bin/env python3 +""" +Generate resources/printers/bambu_filament_ids.json: the map from Orca's +content-addressed filament_id ("OF" + 6 base62 chars, see assign_filament_ids.py) +to Bambu Lab's own AMS/RFID catalog id ("GF..." etc.) for the subset of filament +products Bambu ships. + +The map is generated from BambuStudio's OWN shipped BBL bundle, never from +Orca's: Orca's BBL bundle is a fork of Bambu's, tuned and extended +independently, so it is not the source of truth for Bambu's catalog ids. +Nothing in OrcaSlicer consumes this map yet; it exists so a later change can +translate an id only where it crosses to or from a Bambu printer, without +hand-maintaining the correspondence. + +One row per BambuStudio filament PRODUCT: one commercial line = one +"@base"-declared filament_id, shared by every per-printer/per-nozzle +instantiation of it (BambuStudio follows the same one-product-one-id shape +Orca's own filament_id policy does). A row's key reuses whatever OF id Orca +already ships for that same (filament_vendor, filament_type, family) triple; +a triple Orca does not ship anywhere yet gets a freshly generated one. + +Map format: +{ + "source": "https://github.com/bambulab/BambuStudio", + "bambustudio_commit": "66e405477", + "generated": "2026-09-04", + "filaments": { + "OFhuaUQB": {"bambu_id": "GFB00", "vendor": "Bambu Lab", "type": "ABS", "name": "Bambu ABS"} + } +} + +Run from anywhere: python3 scripts/update_bambu_filament_ids.py + (default) shallow-clone BambuStudio's master branch (sparse: just + the BBL filament bundle) and regenerate the map + --bambustudio-dir DIR + read DIR (a BambuStudio resources/profiles checkout) + instead of cloning + --ref REF clone this BambuStudio ref instead of master + --output PATH write here instead of resources/printers/bambu_filament_ids.json + +After writing, an informational drift report is printed: BambuStudio families +Orca ships nothing with the same identity for, and a count of Orca's own BBL +families that matched no BambuStudio row. Neither blocks the write; both are +for a human to read. +""" + +import argparse +import json +import os +import shutil +import subprocess +import sys +import tempfile +import datetime + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from assign_filament_ids import ( # noqa: E402 + OFL, + OF_ID_RE, + PROFILES_DIR, + analyze_tree, + base_name, + generate_filament_id, + load_vendor_filaments, + print_error, + print_info, + print_success, + resolve_filament_id, + resolve_triple, +) + +SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__)) +BAMBU_MAP_PATH = os.path.normpath( + os.path.join(SCRIPTS_DIR, "..", "resources", "printers", "bambu_filament_ids.json")) + +BAMBUSTUDIO_REPO = "https://github.com/bambulab/BambuStudio" + + +# --------------------------------------------------------------------------- +# Row derivation +# --------------------------------------------------------------------------- + +def derive_rows(bs_filaments, orca_triples): + """orca_triples: {(vendor, type, family): orca_id} built from analyze_tree()["triples"].""" + by_family = {} # family -> (bambu_id, triple) + for rec in bs_filaments.values(): + if not rec["instantiation"]: + continue + bambu_id, _src, _entry = resolve_filament_id(rec["name"], bs_filaments, {}) + if not bambu_id: + continue + family = base_name(rec["name"]) + triple = resolve_triple(rec["name"], bs_filaments, {}) + prev = by_family.setdefault(family, (bambu_id, triple)) + if prev != (bambu_id, triple): + raise SystemExit(f"BambuStudio family {family!r} is not one product: {prev} vs {(bambu_id, triple)}") + rows, seen = {}, {} + for family, (bambu_id, triple) in sorted(by_family.items()): + if bambu_id in seen: + raise SystemExit(f"Bambu id {bambu_id} is shared by {seen[bambu_id]!r} and {family!r}") + seen[bambu_id] = family + orca_id = orca_triples.get(triple) or generate_filament_id(*triple) # reuse a salted id if we ship one + rows[orca_id] = {"bambu_id": bambu_id, "vendor": triple[0], "type": triple[1], "name": triple[2]} + return rows + + +def orca_triples_from_analysis(orca_analysis, needed_triples): + """{(vendor, type, family): orca_id}, inverted from analyze_tree()["triples"] + (id -> [[vendor, type, family], ...]) and restricted to `needed_triples` + (the triples BambuStudio's own bundle ships, i.e. the only ones derive_rows + will ever look up). + + Kept to ids matching the OF format: our own BBL bundle still declares + Bambu's GF ids today (analyze_tree already excludes those as BBL-island + declarations), but filtering here too means a future triple shipped under + a non-OF scheme can never key a map row, which must always be keyed by + the OF id the BBL bundle receives once it is re-minted onto the OF space. + + A needed triple owned by more than one OF id would make derive_rows's + reuse step a guess, so that is a hard error rather than a silent pick. + Restricting the scan to needed_triples matters for that check: elsewhere + in the tree one triple legitimately resolves two different ids on purpose + (e.g. Cubicon's xCeler line declares its own id per printer instead of + inheriting its family's, sanctioned in scripts/filament_id_snapshot.json) + and that pre-existing, Bambu-unrelated divergence must not block a map + that never reads it. + """ + needed_triples = set(needed_triples) + result = {} + for fid, triple_list in orca_analysis["triples"].items(): + if not OF_ID_RE.match(fid): + continue + for triple in triple_list: + triple = tuple(triple) + if triple not in needed_triples: + continue + owner = result.get(triple) + if owner and owner != fid: + raise SystemExit( + f"Orca triple {triple} resolves to more than one filament_id: " + f"{owner!r} and {fid!r}") + result[triple] = fid + return result + + +# --------------------------------------------------------------------------- +# Drift report (informational only) +# --------------------------------------------------------------------------- + +def drift_report(rows, orca_analysis): + """Two triple-identity comparisons between the map just derived and Orca's + own BBL bundle (never id-based: the two bundles assign filament_id + independently, so only the (vendor, type, name) identity is comparable). + + Returns print-ready lines: one per BambuStudio row triple with no + same-triple family in Orca's BBL bundle (upstream ships it, we ship + nothing with that identity there — sometimes a genuinely missing product, + sometimes a renamed one), then a count of Orca's own BBL families that + matched no BambuStudio row (Orca-only products, e.g. a name-drifted + duplicate of one already counted in the first list). + """ + row_triples = {(r["vendor"], r["type"], r["name"]) for r in rows.values()} + + bbl_filaments = orca_analysis["vendors"].get("BBL", {}) + ofl_filaments = orca_analysis["vendors"].get(OFL, {}) + orca_families = {} + for rec in bbl_filaments.values(): + if not rec["instantiation"]: + continue + triple = resolve_triple(rec["name"], bbl_filaments, ofl_filaments) + orca_families.setdefault(base_name(rec["name"]), triple) + + lines = [] + for triple in sorted(row_triples - set(orca_families.values())): + lines.append(f'upstream ships {triple[2]!r} ({triple[0]}/{triple[1]}), ' + "we ship nothing with that identity") + orca_only = [family for family, triple in orca_families.items() if triple not in row_triples] + lines.append(f"Orca BBL families with no row: {len(orca_only)} Orca-only product(s)") + return lines + + +# --------------------------------------------------------------------------- +# Map IO +# --------------------------------------------------------------------------- + +def write_map(path, rows, commit, date): + """Write the map: sorted keys, indent 2, LF, trailing newline.""" + payload = { + "source": BAMBUSTUDIO_REPO, + "bambustudio_commit": commit, + "generated": date, + "filaments": rows, + } + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w", encoding="utf-8", newline="\n") as f: + json.dump(payload, f, indent=2, ensure_ascii=False, sort_keys=True) + f.write("\n") + + +# --------------------------------------------------------------------------- +# BambuStudio fetch +# --------------------------------------------------------------------------- + +def run(command, cwd=None): + """Run a command, streaming its output; raise SystemExit on failure.""" + print("+ " + " ".join(command)) + try: + subprocess.run(command, cwd=cwd, check=True) + except subprocess.CalledProcessError as e: + raise SystemExit(f"command failed ({e.returncode}): {' '.join(command)}") from e + + +def run_out(command, cwd=None): + """Run a command and return its stripped stdout; raise SystemExit on failure.""" + try: + result = subprocess.run(command, cwd=cwd, check=True, capture_output=True, text=True) + except subprocess.CalledProcessError as e: + raise SystemExit( + f"command failed ({e.returncode}): {' '.join(command)}\n{e.stderr}") from e + return result.stdout.strip() + + +def fetch_bambustudio(ref, workdir): + run(["git", "clone", "--depth=1", "--filter=blob:none", "--sparse", "--branch", ref, + BAMBUSTUDIO_REPO + ".git", workdir]) + run(["git", "-C", workdir, "sparse-checkout", "set", "--no-cone", + "resources/profiles/BBL.json", "resources/profiles/BBL/filament"]) + return os.path.join(workdir, "resources", "profiles"), run_out(["git", "-C", workdir, "rev-parse", "--short", "HEAD"]) + + +def local_dir_commit(dir_path): + """git rev-parse --short HEAD of dir_path, or "local" when it is not a repo.""" + result = subprocess.run(["git", "-C", dir_path, "rev-parse", "--short", "HEAD"], + capture_output=True, text=True) + return result.stdout.strip() if result.returncode == 0 else "local" + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + +def main(argv=None): + parser = argparse.ArgumentParser( + description="Regenerate resources/printers/bambu_filament_ids.json from " + "BambuStudio's shipped BBL bundle.") + source = parser.add_mutually_exclusive_group() + source.add_argument("--bambustudio-dir", metavar="DIR", + help="a BambuStudio resources/profiles directory to read " + "instead of cloning") + source.add_argument("--ref", default="master", + help='BambuStudio git ref to shallow-clone when ' + '--bambustudio-dir is not given (default: "master")') + parser.add_argument("--output", default=BAMBU_MAP_PATH, + help="output path (default: resources/printers/bambu_filament_ids.json)") + args = parser.parse_args(argv) + + workdir = None + if args.bambustudio_dir: + profiles_dir = args.bambustudio_dir + commit = local_dir_commit(profiles_dir) + else: + workdir = tempfile.mkdtemp(prefix="bambustudio_") + profiles_dir, commit = fetch_bambustudio(args.ref, workdir) + + try: + bs_filaments, errors = load_vendor_filaments(profiles_dir, "BBL") + if errors: + for e in errors: + print_error(e) + raise SystemExit("unreadable BambuStudio filament profile(s)") + + orca_analysis = analyze_tree(PROFILES_DIR) + needed_triples = {resolve_triple(rec["name"], bs_filaments, {}) + for rec in bs_filaments.values() if rec["instantiation"]} + orca_triples = orca_triples_from_analysis(orca_analysis, needed_triples) + + rows = derive_rows(bs_filaments, orca_triples) + write_map(args.output, rows, commit, datetime.date.today().isoformat()) + print_success(f"wrote {len(rows)} row(s) to {args.output} (BambuStudio @ {commit})") + + for line in drift_report(rows, orca_analysis): + print_info(line) + finally: + if workdir: + shutil.rmtree(workdir, ignore_errors=True) + + return 0 + + +if __name__ == "__main__": + sys.exit(main())