#!/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 orca_id_tool.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. src/slic3r/Utils/BBLPrinterAgent.cpp loads it at runtime and translates an id only where it crosses to or from a Bambu printer, so the correspondence never has to be hand-maintained. See docs/HLSD/filament_id.md. One row per BambuStudio filament PRODUCT: one named spool product = 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 is the OF id that product's (filament_vendor, filament_type, filament) triple mints — the id Orca carries for it wherever it ships it, since the id is a function of the triple alone. 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 filaments Orca ships nothing with the same identity for, and a count of Orca's own BBL filaments 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 orca_id_tool import ( # noqa: E402 BAMBU_MAP_PATH, OFL, PROFILES_DIR, analyze_tree, base_name, generate_filament_id, load_vendor_filaments, print_error, print_info, print_success, resolve_filament_id, resolve_triple, ) BAMBUSTUDIO_REPO = "https://github.com/bambulab/BambuStudio" # --------------------------------------------------------------------------- # Row derivation # --------------------------------------------------------------------------- def derive_rows(bs_filaments): """One row per BambuStudio product, keyed by the OF id its triple mints.""" by_filament = {} # filament_name -> (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 filament_name = base_name(rec["name"]) triple = resolve_triple(rec["name"], bs_filaments, {}) prev = by_filament.setdefault(filament_name, (bambu_id, triple)) if prev != (bambu_id, triple): raise SystemExit(f"BambuStudio filament {filament_name!r} is not one " f"product: {prev} vs {(bambu_id, triple)}") rows, seen = {}, {} for filament_name, (bambu_id, triple) in sorted(by_filament.items()): if bambu_id in seen: raise SystemExit(f"Bambu id {bambu_id} is shared by " f"{seen[bambu_id]!r} and {filament_name!r}") seen[bambu_id] = filament_name rows[generate_filament_id(*triple)] = { "bambu_id": bambu_id, "vendor": triple[0], "type": triple[1], "name": triple[2]} return rows # --------------------------------------------------------------------------- # 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 filament 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 filaments 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_filaments = {} for rec in bbl_filaments.values(): if not rec["instantiation"]: continue triple = resolve_triple(rec["name"], bbl_filaments, ofl_filaments) orca_filaments.setdefault(base_name(rec["name"]), triple) lines = [] for triple in sorted(row_triples - set(orca_filaments.values())): lines.append(f'upstream ships {triple[2]!r} ({triple[0]}/{triple[1]}), ' "we ship nothing with that identity") orca_only = [name for name, triple in orca_filaments.items() if triple not in row_triples] lines.append(f"Orca BBL filaments 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)") rows = derive_rows(bs_filaments) 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, analyze_tree(PROFILES_DIR)): print_info(line) finally: if workdir: shutil.rmtree(workdir, ignore_errors=True) return 0 if __name__ == "__main__": sys.exit(main())