mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-08 09:46:55 +00:00
Translate filament ids at the printer boundary
Orca content-addresses every system filament, Bambu's included, but a printer, its AMS and its vendor's cloud know only that vendor's own catalog ids. The printer agent now translates between the two: outbound MQTT and FTP traffic, the AMS mapping sent with a print job, and the ids written into a 3mf bound for the printer all leave in the printer's own ids, while status messages, loaded projects and SD-card prints arrive in Orca's. An id with no mapping passes through unchanged, and an agent whose printers already speak Orca's ids translates nothing at all. Bambu's map is generated from BambuStudio's own shipped bundle; a missing or unreadable file leaves every lookup an identity rather than taking the app down. The profile check validates the map's shape, and profile CI now runs on the paths that can change it. docs/HLSD/filament_id.md records the places the map deliberately does not reach.
This commit is contained in:
@@ -729,6 +729,8 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
|
||||
# -- 8. Bambu catalog map --------------------------------------------------
|
||||
try:
|
||||
bambu_map = load_json(map_path)
|
||||
if not isinstance(bambu_map, dict):
|
||||
raise ValueError("top level is not a JSON object")
|
||||
except (OSError, ValueError) as e:
|
||||
print_error(f"Bambu catalog map {map_path} does not parse ({e}); {BAMBU_MAP_HINT}")
|
||||
errors += 1
|
||||
@@ -737,7 +739,15 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
|
||||
if not bambu_map.get(key):
|
||||
print_error(f'Bambu catalog map {map_path} is missing "{key}"; {BAMBU_MAP_HINT}')
|
||||
errors += 1
|
||||
rows = bambu_map.get("filaments", {})
|
||||
rows = bambu_map.get("filaments")
|
||||
# An empty or absent section is not a well-formed map: it makes every runtime
|
||||
# translation silently degrade to identity (BBLPrinterAgent logs nothing for it),
|
||||
# and it is what a regeneration against the wrong --bambustudio-dir writes.
|
||||
if not isinstance(rows, dict) or not rows:
|
||||
print_error(f'Bambu catalog map {map_path} declares no "filaments" rows; '
|
||||
f"{BAMBU_MAP_HINT}")
|
||||
errors += 1
|
||||
rows = {}
|
||||
bambu_id_owners = {}
|
||||
for fid, row in sorted(rows.items()):
|
||||
if not OF_ID_RE.match(fid):
|
||||
@@ -745,7 +755,12 @@ def check_filament_ids(profiles_dir=PROFILES_DIR, snapshot_path=SNAPSHOT_PATH,
|
||||
f"{BAMBU_MAP_HINT}")
|
||||
errors += 1
|
||||
bambu_id = row.get("bambu_id")
|
||||
if bambu_id in bambu_id_owners:
|
||||
if not bambu_id:
|
||||
# An empty id would map the empty string to a real filament at runtime.
|
||||
print_error(f'Bambu catalog map row "{fid}" declares no "bambu_id"; '
|
||||
f"{BAMBU_MAP_HINT}")
|
||||
errors += 1
|
||||
elif bambu_id in bambu_id_owners:
|
||||
print_error(
|
||||
f'Bambu catalog map: Bambu id "{bambu_id}" is mapped by both '
|
||||
f'"{bambu_id_owners[bambu_id]}" and "{fid}"; {BAMBU_MAP_HINT}')
|
||||
|
||||
@@ -682,6 +682,13 @@ class TestCheck8(OfCleanTreeCase):
|
||||
ubfi.write_map(path, rows, "testcommit", "2026-09-04")
|
||||
return path
|
||||
|
||||
def _write_raw_map(self, payload):
|
||||
"""Write a map write_map() would never produce (hand-edited or mis-generated)."""
|
||||
path = os.path.join(self.t.dir, "bambu_filament_ids.json")
|
||||
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
||||
json.dump(payload, f, indent=2, ensure_ascii=False, sort_keys=True)
|
||||
return path
|
||||
|
||||
def test_row_triple_must_match_tree(self):
|
||||
fid = afi.generate_filament_id("V", "PLA", "Foo")
|
||||
self.t.write_preset("VendorA", preset("Foo @base", filament_id=fid,
|
||||
@@ -712,6 +719,54 @@ class TestCheck8(OfCleanTreeCase):
|
||||
errors, out = self.t.check(map_path)
|
||||
self.assertEqual(errors, 0, out)
|
||||
|
||||
def test_non_object_top_level_is_a_clean_error(self):
|
||||
# A hand-edited map that is a list (or any non-object) must report the map,
|
||||
# not raise AttributeError out of the check.
|
||||
map_path = self._write_raw_map([{"bambu_id": "GFZ00"}])
|
||||
errors, out = self.t.check(map_path)
|
||||
self.assertGreater(errors, 0)
|
||||
self.assertIn("does not parse", out)
|
||||
self.assertIn("regenerate the map", out)
|
||||
|
||||
def test_empty_filaments_section_is_an_error(self):
|
||||
# What a regeneration against the wrong --bambustudio-dir writes: a well-formed
|
||||
# header with zero rows. At runtime every translation silently becomes identity.
|
||||
map_path = self._write_map({})
|
||||
errors, out = self.t.check(map_path)
|
||||
self.assertGreater(errors, 0)
|
||||
self.assertIn('no "filaments" rows', out)
|
||||
|
||||
def test_absent_filaments_section_is_an_error(self):
|
||||
map_path = self._write_raw_map({
|
||||
"source": "https://github.com/bambulab/BambuStudio",
|
||||
"bambustudio_commit": "testcommit",
|
||||
"generated": "2026-09-04",
|
||||
})
|
||||
errors, out = self.t.check(map_path)
|
||||
self.assertGreater(errors, 0)
|
||||
self.assertIn('no "filaments" rows', out)
|
||||
|
||||
def test_row_without_bambu_id_is_an_error(self):
|
||||
# Two such rows used to collide on None and be reported as a duplicate id.
|
||||
map_path = self._write_map({
|
||||
"OFaaaaaa": {"vendor": "V", "type": "PLA", "name": "Foo"},
|
||||
"OFbbbbbb": {"vendor": "V", "type": "PETG", "name": "Bar"},
|
||||
})
|
||||
errors, out = self.t.check(map_path)
|
||||
self.assertGreater(errors, 0)
|
||||
self.assertIn('"OFaaaaaa" declares no "bambu_id"', out)
|
||||
self.assertIn('"OFbbbbbb" declares no "bambu_id"', out)
|
||||
self.assertNotIn("mapped by both", out)
|
||||
|
||||
def test_empty_bambu_id_is_an_error(self):
|
||||
# "" would land in the runtime map and translate an empty tray id into a filament.
|
||||
map_path = self._write_map({
|
||||
"OFaaaaaa": {"bambu_id": "", "vendor": "V", "type": "PLA", "name": "Foo"},
|
||||
})
|
||||
errors, out = self.t.check(map_path)
|
||||
self.assertGreater(errors, 0)
|
||||
self.assertIn('declares no "bambu_id"', out)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# --update-snapshot
|
||||
|
||||
@@ -8,9 +8,9 @@ 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.
|
||||
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 commercial line = one
|
||||
"@base"-declared filament_id, shared by every per-printer/per-nozzle
|
||||
|
||||
Reference in New Issue
Block a user