Files
OrcaSlicer/scripts/update_bambu_filament_ids.py
T
SoftFever ade9e77b6b Run every profile maintenance job from one tool (#15726)
* Run every profile maintenance job from one tool

orca_id_tool.py becomes orca_profile_tool.py, and orca_extra_profile_check.py
and orca_filament_lib.py fold into it as subcommands: check, generate-id, fix,
trim, update-index and update-snapshot. The three scripts already overlapped --
the checker imported half of its rules from the id tool, which in turn kept a
copy-pasted set of output helpers to avoid the resulting import cycle -- while
disagreeing on how a vendor is enumerated, how a JSON file is read and what the
exit code means. One file settles all three.

check, normalize, trim and update-index reproduce their predecessors exactly; normalize and
update-index were diffed byte-for-byte against the old scripts over a copy of
the whole tree. Deliberate changes: the compatible-printers check no longer
switches itself off when --check-materials is passed, an error exits 1 rather
than -1, update-index honours --profile-type and reports a profile it cannot
place instead of dropping it from the index, fix and update-index gained
--dry-run, trim keeps an unindexed file some surviving profile still inherits
from, and vendors are enumerated as directories with an index -- which is why
blacklist.json, a data file that an unscoped index rebuild once wrote four empty
list sections into, loses them here and will not collect them again. The dead
rename_filament_system() helper is gone.

The suite under scripts/tests now covers the maintenance commands too, and CI
runs it; nothing there ran in CI before. No shipped profile data changes apart
from those four keys.

* update vendor index files with "python3 ./scripts/orca_profile_tool.py update-index" and "python3 ./scripts/orca_profile_tool.py normalize"
2026-09-16 19:53:23 +08:00

247 lines
10 KiB
Python

#!/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_profile_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_profile_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())