From 4aa0e1d60b6f32ea84a4cfebbf1cfa93f87988e5 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Fri, 4 Sep 2026 22:10:48 +0800 Subject: [PATCH] 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. --- .github/workflows/check_profiles.yml | 4 + docs/HLSD/filament_id.md | 223 +++++++++++++++--- scripts/assign_filament_ids.py | 19 +- scripts/tests/test_filament_id.py | 55 +++++ scripts/update_bambu_filament_ids.py | 6 +- src/OrcaSlicer.cpp | 22 ++ src/slic3r/GUI/AMSDryControl.cpp | 15 +- src/slic3r/GUI/AMSMaterialsSetting.cpp | 10 +- src/slic3r/GUI/ColorDecomposeSupport.cpp | 44 +++- .../GUI/DeviceCore/DevFilaBlackList.cpp | 7 +- src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp | 15 +- src/slic3r/GUI/DeviceManager.cpp | 11 +- src/slic3r/GUI/Plater.cpp | 9 + src/slic3r/GUI/PresetComboBoxes.cpp | 9 +- src/slic3r/GUI/SelectMachine.cpp | 16 +- src/slic3r/Utils/BBLPrinterAgent.cpp | 117 ++++++++- src/slic3r/Utils/BBLPrinterAgent.hpp | 14 ++ src/slic3r/Utils/CalibUtils.cpp | 26 +- src/slic3r/Utils/IPrinterAgent.hpp | 10 + src/slic3r/Utils/NetworkAgent.cpp | 14 ++ src/slic3r/Utils/NetworkAgent.hpp | 2 + tests/slic3rutils/CMakeLists.txt | 1 + tests/slic3rutils/test_bambu_filament_ids.cpp | 81 +++++++ 23 files changed, 652 insertions(+), 78 deletions(-) create mode 100644 tests/slic3rutils/test_bambu_filament_ids.cpp diff --git a/.github/workflows/check_profiles.yml b/.github/workflows/check_profiles.yml index 0c0750c610..b855d09da7 100644 --- a/.github/workflows/check_profiles.yml +++ b/.github/workflows/check_profiles.yml @@ -9,6 +9,10 @@ on: - release/* paths: - 'resources/profiles/**' + # The extra JSON check also validates resources/printers/bambu_filament_ids.json, + # and lives in scripts/, so a PR touching only those must still run this workflow. + - 'resources/printers/**' + - 'scripts/**' - ".github/workflows/check_profiles.yml" workflow_dispatch: diff --git a/docs/HLSD/filament_id.md b/docs/HLSD/filament_id.md index 980d830d7d..4c1c1f7443 100644 --- a/docs/HLSD/filament_id.md +++ b/docs/HLSD/filament_id.md @@ -64,14 +64,15 @@ failing that, any compatible system preset, else the slot is skipped — every f selection surfaces a user-visible notice. Today only the Bambu AMS integration follows this pattern end to end — the device itself -reports the id, and the pipeline does all the matching. The other device integrations still -synthesize a preset id client-side in their agents (by type, brand, or color lookups against -the loaded presets) before the pipeline runs; they are intended to converge on the same -pattern, with the device-reported tray material id flowing through the shared matcher. +reports the id, `BBLPrinterAgent` translates it out of Bambu's catalog into ours, and the +pipeline does all the matching. The other device integrations still synthesize a preset id +client-side in their agents (by type, brand, or color lookups against the loaded presets) +before the pipeline runs; they are intended to converge on the same pattern, with the +device-reported tray material id flowing through the shared matcher. | Ecosystem | Where the tray id comes from today | | --- | --- | -| Bambu AMS | the device itself (RFID / user tray setting) — the `GF*` catalog | +| Bambu AMS | the device itself (RFID / user tray setting), in Bambu's own `GF*` catalog; `BBLPrinterAgent` rewrites it into our id before the matcher sees it (see [The Bambu catalog map](#the-bambu-catalog-map)) | | Qidi box | composed at runtime as `QD___` — vendor and type indices from the device's per-slot saved variables, the series digit inferred client-side from the printer model/name. No preset carries a `QD_*` value, so the slot currently resolves by filament type; mapping the composed id onto the family's minted id belongs in the agent | | Creality CFS | runtime brand/type scoring returns the winning preset's id | | Klipper (AFC / Happy Hare) | runtime lookup by filament type | @@ -205,20 +206,23 @@ python scripts/assign_filament_ids.py --check # 4. verify — the sam # 5. Commit the profile edits together with scripts/filament_id_snapshot.json. ``` -The default run is idempotent and never rewrites a valid existing id; it edits profile files -byte-preservingly (indentation, BOM, and line endings intact) and re-parses them to fail -loudly. `--mint "filament_vendor/filament_type/family_name"` prints the id a **new** mint of -that triple would get, without touching anything — note that for a triple whose id already -exists it prints the next *free* salt iteration, not the live id (asking for +The default run mints ids for id-less families and replaces any declaration that is not in +`OF` format; it never rewrites a valid `OF` id, so it is idempotent and a no-op once every +family has one. It edits profile files byte-preservingly (indentation, BOM, and line endings +intact) and re-parses them to fail loudly. +`--mint "filament_vendor/filament_type/family_name"` prints the id a **new** mint of that +triple would get, without touching anything — note that for a triple whose id already exists +it prints the next *free* salt iteration, not the live id (asking for `Polymaker/PLA/PolyLite PLA` today prints the salt-1 id, because `OF5CgdDq` is taken). Maintenance modes (`--remint` is also the step for identity fixes — see the FAQ; the rest are normally only used by id migrations): -- `--remint VENDOR` re-derives a vendor's declared ids from their triples. A declaration - already equal to a salt iteration of its own triple is conformant and left alone, so - deliberate salt splits survive; convergence onto an id another bundle already uses for the - *same* triple is legal by design — that is the point. +- `--remint VENDOR` re-derives a vendor's declared ids from their triples — any vendor, BBL + included, since no bundle is exempt from the mint rule. A declaration already equal to a + salt iteration of its own triple is conformant and left alone, so deliberate salt splits + survive; convergence onto an id another bundle already uses for the *same* triple is legal + by design — that is the point. - `--drop-redundant-ids VENDOR` deletes declarations that merely re-declare an inherited OrcaFilamentLibrary id. - `--profiles DIR` points the tooling at a different profile tree (default @@ -231,22 +235,26 @@ diff. ## Reserved namespaces — never mint or hand-write into -An **island** is a frozen id namespace exempt from the mint rule because an external catalog -or device contract owns it. +A **reserved namespace** is an id space no system profile may declare, because an external +catalog or a device protocol owns the values. None of them has an owning vendor: there is no +bundle — not even the one whose printers use the catalog — that may write one into a profile. | Space | Status | Rule | | --- | --- | --- | -| `GF*` | Bambu AMS/RFID catalog — the one remaining *island* | BBL vendor only; a claim by anyone else is refused unless the snapshot sanctions that exact claim (via `--allow-shared-catalog` — see the CI section) | +| `GF*` | Bambu AMS/RFID catalog | declarable by **nobody**, BBL included: Bambu's own ids live in the generated catalog map, never in a profile | | `QD_*` | Qidi device protocol | declarable by **nobody**, Qidi included: the box composes these ids at runtime and they are not preset ids | | `P` + 7 hex chars (case-insensitive), `"null"` | user-created custom filaments (`CreatePresetsDialog.cpp`) | never appears in system profiles | | every already-shipped id | frozen in the snapshot (grandfathered) | frozen as-is; new claims need maintainer sign-off | The two device namespaces, in detail: -- **BBL (`GF*`).** Bambu's device/RFID/cloud catalog is external and opaque, so BBL - declarations are frozen as-is and never re-minted. BBL also carries several dozen - grandfathered legacy codes that are neither `GF*` nor `OF*` (the `BETA` family's `B*` ids, - plus `Generic SBS`'s legacy `BFLSBS99`) — frozen the same way, via the snapshot. +- **Bambu (`GF*`).** Bambu's device/RFID/cloud catalog is external and opaque, which is a + reason to keep it out of the profiles rather than to let one bundle own it. Every BBL family + mints an `OF` id from its triple like every other vendor's, and the correspondence to Bambu's + catalog ids lives in one generated file the app applies at the printer boundary — the next + section. Nothing under `resources/profiles/**` carries a `GF*` id today and the snapshot + grandfathers none, so a `GF*` id appearing anywhere in the tree is a mistake, whoever wrote + it. - **Qidi (`QD_*`).** `QD_*` is a device-*protocol* namespace, not a preset id space: the Qidi box path composes `QD___` ids at runtime (slot vendor and type indices reported by the device, the series digit inferred client-side from the printer @@ -256,6 +264,142 @@ The two device namespaces, in detail: protocol ids as preset ids — would put one product under five ids (`QIDI PLA Rapido` would be `QD_0_1_1` through `QD_4_1_1`), exactly the fragmentation the mint rule removes. +## The Bambu catalog map + +Bambu's printers, its AMS and its cloud know only Bambu's own catalog ids. Our profiles carry +minted `OF` ids like every other vendor's, so one generated file records the correspondence and +the app applies it **only where an id crosses to or from a Bambu printer**. + +**The file** is `resources/printers/bambu_filament_ids.json` — a header plus one row per +catalogued product, keyed by our id: + +```json +{ + "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" } + } +} +``` + +It ships in `resources/printers/`, next to `filaments_blacklist.json` — deliberately not in +`resources/profiles/`, where the loader reads every top-level `.json` as a vendor index. It +holds 100 rows today, one per product BambuStudio ships, and the correspondence is +one-to-one in both directions. + +**It is generated, never hand-edited.** `python scripts/update_bambu_filament_ids.py` rebuilds +it from **BambuStudio's own shipped BBL bundle** — a sparse shallow clone of upstream `master`, +or `--bambustudio-dir `. Our BBL bundle is a fork of +Bambu's, tuned and extended independently, so it is not the source of truth for Bambu's ids. +A row's key is whatever id our tree already mints for that same +`(filament_vendor, filament_type, family)` triple; a product we do not ship gets a freshly +generated key and the row sits inert until some bundle claims that triple — `OFdyfQvU` / +`GFG03`, "Bambu PETG Matte", is such a row today. + +**Regenerate it in the same commit as every BBL profile sync**, and read the drift report it +prints. Two lines, both informational, neither blocking the write: + +```text +upstream ships 'Bambu PETG Matte' (Bambu Lab/PETG), we ship nothing with that identity +Orca BBL families with no row: 135 Orca-only product(s) +``` + +The first names each upstream product our BBL bundle has no same-identity family for — +sometimes a genuinely missing product, sometimes a name drift a follow-up rename would +converge. The second counts our own BBL families that matched no row: 135 of 234 today, of +which 109 send an `OF` id on the wire and 26 already rode `OF` ids inherited from the +OrcaFilamentLibrary. **135 is the number to expect at every regeneration** — 109 was the +one-off size of the transition and stopped being computable from the tree once the BBL bundle +was re-minted, so do not "fix" the report to print it. + +**Check 8** lives in `check_filament_ids`, so profile CI runs it alongside the other seven. It +holds the file to its contract: it parses, carries `source` / `bambustudio_commit` / +`generated`, keys only `OF`-format ids, maps each Bambu id at most once, and — for every row +whose key the tree actually claims — agrees with the tree on that id's `(vendor, type, name)` +triple. A row for a product we do not ship is skipped, not an error. The remedy it prints is +always the same: regenerate the map and commit the diff for review. + +### The runtime rule: swap on hit + +Outbound, our id with a row becomes Bambu's; inbound, Bambu's id with a row becomes ours. +Everything else is forwarded untouched — an `OF` id with no row, a Bambu id for a product we do +not ship, a `P`-hex user id, `"null"`, an empty string. Translation is confined to the +boundary: nothing between the boundaries ever holds a Bambu id. + +Translating one value is a capability of the printer agent: `IPrinterAgent` declares +`to_orca_filament_id` and `from_orca_filament_id` returning their argument, and `BBLPrinterAgent` +overrides them with Bambu's map, so an agent whose printers already speak our ids inherits the +identity default and translates nothing. `NetworkAgent` forwards both to the live agent, so the +comparison sites below reach them through `wxGetApp().getAgent()` and leave an id untranslated +while no agent is live. Whole documents are Bambu's business alone: +`BBLPrinterAgent::to_orca_payload` and `from_orca_payload` rewrite every string under +`tray_info_idx`, `filament_id` or `filamentId` at any depth; text that does not parse, or carries +none of those keys, comes back unchanged. The map is loaded once, lazily; a missing or malformed +file degrades to identity with a log line rather than failing. + +| Boundary | Where it translates | +| --- | --- | +| Everything the agent sends | `BBLPrinterAgent::send_message` and `send_message_to_printer`, plus `PrintParams::ams_mapping_info` in `dispatch_start` — the funnel all five `start_*` calls share | +| Everything the agent receives | `set_on_message_fn` and `set_on_local_message_fn` wrap their callback, so `MachineObject::parse_json` and everything downstream see our ids only | +| 3mf export | `Plater::export_3mf` writes Bambu's ids into `slice_info.config`, gated on `preset_bundle.is_bbl_vendor()` — the printer reads that file and knows only its own catalog, and no other vendor's export is affected. The CLI has its own writer in `OrcaSlicer.cpp`; it does the same, gated on the `printer_model` prefix that already decides `Print::is_BBL_printer()` for that run | +| Project ingest | `Plater::priv::load_files` reverse-maps the project's `filament_ids` before the bundle ingests them, so a project saved by an older Orca or by BambuStudio still resolves the same presets | +| Prints from the printer's SD card | `SelectMachineDialog::update_print_required_data` reverse-maps each plate's slice-info ids as it adopts the plates, so the AMS mapping dialog pairs them with trays | +| Bambu-specific comparisons | `CalibUtils.cpp`, `DeviceManager.cpp`, `DeviceCore/DevFilaSystem.cpp`, `DeviceCore/DevFilaBlackList.cpp`, `SelectMachine.cpp`, `AMSDryControl.cpp`, `AMSMaterialsSetting.cpp`, `PresetComboBoxes.cpp`, `ColorDecomposeSupport.cpp` | + +That last row is the rule to follow when a new Bambu-specific behaviour is added: **translate +the value you are about to compare, never the table you compare it against.** The shipped data +those sites read is Bambu's and stays verbatim — `white_fila_ids` in +`resources/printers/filaments_blacklist.json`, the calibration id lists in +`resources/printers/.json`, `fila_id` in +`resources/profiles/BBL/filament/filaments_color_codes.json`. + +`tests/slic3rutils/test_bambu_filament_ids.cpp` covers the lookups, the payload rewrite and the +Bambu-specific rules. `orcaslicer_discover_tests` registers a Catch2 tag as a CTest **label**, +not as part of the test name, so `-R` matches nothing here and the filter is `-L`: + +```bash +ctest --test-dir /tests/slic3rutils -L BambuFilamentIds +``` + +### Three places the map deliberately does not reach + +The map and its lookups live in the GUI library, which libslic3r cannot link against and which a +GUI-less build does not link at all. Three consequences are known and documented; none is worth +pulling the map down into libslic3r for. + +- **The support display type in `PrintConfig.cpp`.** `DynamicPrintConfig::get_filament_type` + picks `PLA-S` / `Sup.PLA` and `PA-S` / `Sup.PA` for a support filament by testing + `filament_id` against `GFS00` and `GFS01`, and otherwise falls back on `filament_type` — a + fallback that returns those same two pairs for `"PLA"` and `"PA"`. Bambu Support W inherits + `fdm_filament_pla` and Bambu Support G inherits `fdm_filament_pa`, so with their `OF` ids the + fallback produces exactly what the id branches produced. (The only config that ever carries a + singular `filament_id` key is the AMS tray config built in `Plater.cpp`, and that one never + reaches this function.) These two lines are the only mention of a Bambu id anywhere in + libslic3r, and they need no change. +- **Config imports.** `PresetBundle::import_presets` (File ▸ Import ▸ Import Configs, for + `.json` / `.zip` / `.orca_filament` / `.orca_printer` / `.orca_bundle`) and + `PresetBundle::load_config_file` (the CLI's `--load-settings` of a G-code file with an + embedded config) both parse inside libslic3r, out of the GUI's reach, so a Bambu id carried + in such a file lands on the imported preset untranslated. The effect is bounded: that preset + does not auto-match an AMS tray while the stale id is live, and the id does not survive + being saved — `Preset::save` writes a `filament_id` key only for a preset whose `inherits` is + empty, and on the next load an inheriting preset takes its parent's id. A known gap, and not + a regression: nothing forwarded a stale id before either. +- **A build configured without the GUI.** `target_link_libraries(OrcaSlicer libslic3r_gui)` sits + inside `if (SLIC3R_GUI)` in `src/CMakeLists.txt`, so the lookups are not linkable when the GUI + is off. The CLI's 3mf writer in `src/OrcaSlicer.cpp` therefore guards its translation with + `#ifdef SLIC3R_GUI`, and a 3mf that such a build slices for a Bambu printer carries our `OF` + ids in `slice_info.config` rather than Bambu's. Every shipped build enables the GUI, so this + reaches only a purpose-built GUI-less binary. + +One more thing worth recording before it is rediscovered: +`SyncAmsInfoDialog::update_print_required_data` is a structural twin of the SD-card function +above and carries no translation. It has no callers today and its plate list is only ever read +for `printer_model_id`, so it is not a live gap — but wiring it up without adding the reverse +map would silently reproduce the bug. + ## How CI enforces this Profile CI (`check_profiles.yml`) runs `check_filament_ids()` tree-wide via @@ -271,7 +415,8 @@ checks ratchet all new profiles to the clean rules. The checks, in brief: -- **Format** — every id is either in the snapshot, `OF` + 6 base62 chars, or BBL's. +- **Format** — every id occurring in the tree is `OF` + 6 base62 chars. No exceptions: not a + grandfathered snapshot entry, not BBL. - **Snapshot equality** — tree claims == snapshot claims **and** tree triples == snapshot triples, both directions: any `filament_vendor`/`filament_type`/family-name change surfaces as a snapshot diff. @@ -282,22 +427,34 @@ The checks, in brief: base name (a rename re-exposes the library preset, since alias shadowing is name-based), a non-empty `compatible_printers` (an empty one shadows nothing), and no own id key (structure rule 4). -- **Triple integrity** — every declarer outside the BBL island must resolve a non-empty - `filament_vendor` and `filament_type` (generics use `"Generic"`), and all declarers of one - family within a bundle must agree on the triple. -- **Reserved namespaces** — `GF*` outside BBL, `P<7-hex>` or `"null"` anywhere, unless that - exact claim is grandfathered in the snapshot; `QD_*` anywhere, with no exception. +- **Triple integrity** — every declarer must resolve a non-empty `filament_vendor` and + `filament_type` (generics use `"Generic"`), and all declarers of one family within a + bundle must agree on the triple. +- **Reserved namespaces** — `GF*`, `QD_*`, `P<7-hex>` or `"null"` claimed by any vendor, + BBL and Qidi included, unless that exact claim is grandfathered in the snapshot (none is + today). - **Structure** — no `filament_id` key on newly instantiated presets; no new declared-vs-inherited id drift; every instantiated system filament must resolve an effective id through its `inherits` chain (recall: an id-less one is a hard load error in C++ that discards the whole vendor bundle). +- **Bambu catalog map** — `resources/printers/bambu_filament_ids.json` parses, carries its + `source` / `bambustudio_commit` / `generated` header, keys only `OF`-format ids, maps each + Bambu id at most once, and agrees with the tree on the triple of every row whose key the + tree claims. See [The Bambu catalog map](#the-bambu-catalog-map); the remedy is always to + regenerate, never to hand-edit. -Sharing a **reserved-catalog** id with a new family or vendor (e.g. shipping a Bambu-cataloged -product under another vendor with its authentic `GF*` id) is refused by `--update-snapshot` -unless you pass `--allow-shared-catalog` — and it still lands in the snapshot diff for -maintainer review. Any other new sharing via a *declared* id is caught by the mint-conformance -check; sharing through inheritance carries no declaration to check and surfaces only as a new -claim in the snapshot diff — which is exactly why that diff is the gate. +Any new claim on a **reserved namespace** — a profile that declares a `GF*`, `QD_*` or +`P<7-hex>` id, whatever its vendor — is refused by `--update-snapshot` unless you pass +`--allow-shared-catalog`, and even then it lands in the snapshot diff for maintainer review. +There is no such claim in the tree today and adding one should be a last resort: for a +Bambu-cataloged product, the catalog map is where the correspondence belongs. Any other new +sharing via a *declared* id is caught by the mint-conformance check; sharing through +inheritance carries no declaration to check and surfaces only as a new claim in the snapshot +diff — which is exactly why that diff is the gate. + +`orca_extra_profile_check.py` separately holds every declared id to the AMS 8-character limit, +tree-wide and for every vendor alike, scoped to the presets a vendor's index actually +references (a file the index never loads cannot break AMS matching). Complementing the Python checks, CI also runs the C++ profile validator with `-f` (`check_filament_subtypes`): it loads the bundle exactly as the app does and flags any printer diff --git a/scripts/assign_filament_ids.py b/scripts/assign_filament_ids.py index 7ce907f816..d410242277 100755 --- a/scripts/assign_filament_ids.py +++ b/scripts/assign_filament_ids.py @@ -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}') diff --git a/scripts/tests/test_filament_id.py b/scripts/tests/test_filament_id.py index 3d668d4fc3..b9291e9433 100644 --- a/scripts/tests/test_filament_id.py +++ b/scripts/tests/test_filament_id.py @@ -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 diff --git a/scripts/update_bambu_filament_ids.py b/scripts/update_bambu_filament_ids.py index cd5e05e3bc..fc195a3f9c 100644 --- a/scripts/update_bambu_filament_ids.py +++ b/scripts/update_bambu_filament_ids.py @@ -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 diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index 33669abcf2..2e39f802cf 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -100,6 +100,10 @@ using namespace nlohmann; #ifdef SLIC3R_GUI #include "slic3r/GUI/GUI_Init.hpp" + // BBLPrinterAgent::from_orca_filament_id(); the map and its lookups live in libslic3r_gui, + // which only a SLIC3R_GUI build links (see target_link_libraries(OrcaSlicer libslic3r_gui) + // in CMakeLists). + #include "slic3r/Utils/BBLPrinterAgent.hpp" #endif /* SLIC3R_GUI */ using namespace Slic3r; @@ -6504,6 +6508,20 @@ int CLI::run(int argc, char **argv) std::string nozzle_diameter_str; if (nozzle_diameter_option) nozzle_diameter_str = nozzle_diameter_option->serialize(); +#ifdef SLIC3R_GUI + // A Bambu printer reads slice_info.config and knows only its own catalog ids. The GUI + // gates the same translation on PresetBundle::is_bbl_vendor(); the CLI has no + // PresetBundle, so reuse the printer_model prefix that already decides + // Print::is_BBL_printer() for this same run. + auto* printer_model_option = dynamic_cast(m_print_config.option("printer_model")); + const bool is_bbl_printer = printer_model_option && printer_model_option->value.compare(0, 9, "Bambu Lab") == 0; + // No wxApp on the CLI path, so there is no live agent to ask; the translator is stateless + // over a lazily loaded map, so one instance serves every plate and filament below. + // ORCA TODO: this assumes Bambu's is the only agent with a catalog of its own. Once another + // agent carries one, resolve the agent from the selected printer the way + // GUI_App::resolve_printer_agent_id does, rather than hard-coding BBLPrinterAgent here. + const BBLPrinterAgent bbl_agent; +#endif /* SLIC3R_GUI */ for (int i = 0; i < plate_data_list.size(); i++) { PlateData *plate_data = plate_data_list[i]; @@ -6520,6 +6538,10 @@ int CLI::run(int argc, char **argv) it->type = m_print_config.get_filament_type(display_filament_type, it->id); it->color = filament_color ? filament_color->get_at(it->id) : "#FFFFFF"; it->filament_id = filament_id?filament_id->get_at(it->id):""; +#ifdef SLIC3R_GUI + if (is_bbl_printer) + it->filament_id = bbl_agent.from_orca_filament_id(it->filament_id); +#endif /* SLIC3R_GUI */ } if (!plate_data->plate_thumbnail.is_valid()) { diff --git a/src/slic3r/GUI/AMSDryControl.cpp b/src/slic3r/GUI/AMSDryControl.cpp index a668a33c76..c191e24eac 100644 --- a/src/slic3r/GUI/AMSDryControl.cpp +++ b/src/slic3r/GUI/AMSDryControl.cpp @@ -1511,6 +1511,10 @@ void AMSDryCtrWin::update_filament_guide_info(DevAms* dev_ams) m_temperature_input->GetValue().ToLong(&input_temp); bool can_start = true; + // "GFA00" is Bambu's PLA id; GetFilamentDryingPreset is keyed by our OF ids. + auto* agent = wxGetApp().getAgent(); + const std::string pla_filament_id = agent ? agent->to_orca_filament_id("GFA00") : std::string("GFA00"); + int slot_count = 0, empty_count = 0; for (auto& tray_pair : dev_ams->GetTrays()) { if (!tray_pair.second) { @@ -1526,13 +1530,15 @@ void AMSDryCtrWin::update_filament_guide_info(DevAms* dev_ams) wxString filament_type = tray_pair.second->get_display_filament_type(); DevFilamentDryingPreset preset; if (filament_type.IsEmpty()) { - auto fallback_preset = DevUtilBackend::GetFilamentDryingPreset("GFA00"); + auto fallback_preset = DevUtilBackend::GetFilamentDryingPreset(pla_filament_id); + if (!fallback_preset) continue; // no PLA preset (e.g. the id map is missing): skip, don't throw preset = fallback_preset.value(); filament_type = "?"; } else if (preset_opt.has_value()) { preset = preset_opt.value(); } else { - auto fallback_preset = DevUtilBackend::GetFilamentDryingPreset("GFA00"); + auto fallback_preset = DevUtilBackend::GetFilamentDryingPreset(pla_filament_id); + if (!fallback_preset) continue; preset = fallback_preset.value(); } std::string icon_path = "dev_ams_dry_ctr_enable"; @@ -1683,9 +1689,10 @@ int AMSDryCtrWin::update_filament_list(DevAms* dev_ams, MachineObject* obj) // Select recommended drying temperature and default filament float min_dry_temp = std::numeric_limits::max(); - std::string default_filament_id = "GFA00"; + auto* agent = wxGetApp().getAgent(); + std::string default_filament_id = agent ? agent->to_orca_filament_id("GFA00") : std::string("GFA00"); // compared against m_tray_ids[i].filament_id (our OF ids) below bool has_ready = false; - const auto fallback_preset = DevUtilBackend::GetFilamentDryingPreset("GFA00"); + const auto fallback_preset = DevUtilBackend::GetFilamentDryingPreset(default_filament_id); for (const auto& tray_pair : dev_ams->GetTrays()) { if (!tray_pair.second || !tray_pair.second->is_tray_info_ready()) continue; has_ready = true; diff --git a/src/slic3r/GUI/AMSMaterialsSetting.cpp b/src/slic3r/GUI/AMSMaterialsSetting.cpp index ed532e91a8..1748617789 100644 --- a/src/slic3r/GUI/AMSMaterialsSetting.cpp +++ b/src/slic3r/GUI/AMSMaterialsSetting.cpp @@ -815,7 +815,10 @@ void AMSMaterialsSetting::set_color(wxColour color) fila_color.m_colors.insert(color); fila_color.EndSet(m_clr_picker->ctype); auto clr_query = GUI::wxGetApp().get_filament_color_code_query(); - m_clr_name->SetLabelText(clr_query->GetFilaColorName(ams_filament_id, fila_color)); + // ams_filament_id is our OF id; GetFilaColorName looks up filaments_color_codes.json, + // downloaded from Bambu and keyed by the printer's own ids, so translate for this lookup only. + auto* agent = GUI::wxGetApp().getAgent(); + m_clr_name->SetLabelText(clr_query->GetFilaColorName(agent ? agent->from_orca_filament_id(ams_filament_id) : ams_filament_id, fila_color)); } void AMSMaterialsSetting::set_empty_color(wxColour color) @@ -836,7 +839,10 @@ void AMSMaterialsSetting::set_colors(std::vector colors) for (const auto& clr : colors) { fila_color.m_colors.insert(clr); } fila_color.EndSet(m_clr_picker->ctype); auto clr_query = GUI::wxGetApp().get_filament_color_code_query(); - m_clr_name->SetLabelText(clr_query->GetFilaColorName(ams_filament_id, fila_color)); + // ams_filament_id is our OF id; GetFilaColorName looks up filaments_color_codes.json, + // downloaded from Bambu and keyed by the printer's own ids, so translate for this lookup only. + auto* agent = GUI::wxGetApp().getAgent(); + m_clr_name->SetLabelText(clr_query->GetFilaColorName(agent ? agent->from_orca_filament_id(ams_filament_id) : ams_filament_id, fila_color)); } } diff --git a/src/slic3r/GUI/ColorDecomposeSupport.cpp b/src/slic3r/GUI/ColorDecomposeSupport.cpp index e8fb4c9082..6621b97059 100644 --- a/src/slic3r/GUI/ColorDecomposeSupport.cpp +++ b/src/slic3r/GUI/ColorDecomposeSupport.cpp @@ -62,10 +62,16 @@ std::string decompose_basic_type_from_source(size_t source_config_idx, auto& project_config = wxGetApp().preset_bundle->project_config; if (auto* filament_id_opt = project_config.option("filament_id")) { if (source_config_idx < filament_id_opt->values.size()) { - const std::string& filament_id = filament_id_opt->values[source_config_idx]; - if (filament_id == kDecomposePetgFilamentId) + // Dead in practice: "filament_id" is not in PresetBundle's s_project_options, so this + // option() lookup (create=false) always returns null and the block never runs. Kept as + // found, with the translation the values would need: they would be our OF ids, and the + // two constants are the printer's own ids. + auto* agent = wxGetApp().getAgent(); + const std::string& orca_filament_id = filament_id_opt->values[source_config_idx]; + const std::string printer_filament_id = agent ? agent->from_orca_filament_id(orca_filament_id) : orca_filament_id; + if (printer_filament_id == kDecomposePetgFilamentId) return kDecomposePetgBasicType; - if (filament_id == kDecomposePlaFilamentId) + if (printer_filament_id == kDecomposePlaFilamentId) return kDecomposePlaBasicType; } } @@ -82,9 +88,14 @@ std::string decompose_basic_type_from_source(size_t source_config_idx, std::string decompose_basic_filament_id(const std::string& basic_type) { - if (basic_type == kDecomposePetgBasicType) - return kDecomposePetgFilamentId; - return kDecomposePlaFilamentId; + // The result becomes DecomposeOfficialComponent::filament_id, which the rest of this file + // reads as one of our OF ids (translating back before it compares against the printer's + // ids), so translate on the way out; kDecompose*FilamentId itself stays the printer-side + // literal. The only place that would carry it further, project_config's "filament_id", is + // dead code: that key is not in PresetBundle's s_project_options. + const std::string printer_filament_id = basic_type == kDecomposePetgBasicType ? kDecomposePetgFilamentId : kDecomposePlaFilamentId; + auto* agent = wxGetApp().getAgent(); + return agent ? agent->to_orca_filament_id(printer_filament_id) : printer_filament_id; } void set_created_standard_component_metadata(size_t config_idx, const DecomposeOfficialComponent& component) @@ -98,8 +109,11 @@ void set_created_standard_component_metadata(size_t config_idx, const DecomposeO } } - const std::string type = component.filament_id == kDecomposePetgFilamentId ? kDecomposePetgShortType : - component.filament_id == kDecomposePlaFilamentId ? kDecomposePlaShortType : ""; + // component.filament_id is our OF id; the two constants are the printer's own ids. + auto* agent = wxGetApp().getAgent(); + const std::string printer_filament_id = agent ? agent->from_orca_filament_id(component.filament_id) : component.filament_id; + const std::string type = printer_filament_id == kDecomposePetgFilamentId ? kDecomposePetgShortType : + printer_filament_id == kDecomposePlaFilamentId ? kDecomposePlaShortType : ""; if (!type.empty()) { if (auto* type_opt = project_config.option("filament_type")) { while (type_opt->values.size() <= config_idx) @@ -151,7 +165,12 @@ DecomposeOfficialComponent lookup_decompose_official_component( continue; if (item.contains("fila_color") && item["fila_color"].is_array() && !item["fila_color"].empty()) result.color_hex = decompose_normalize_color_hex(item["fila_color"][0].get()); - result.filament_id = item.value("fila_id", result.filament_id); + // fila_id from this shipped, Bambu-keyed color table is a printer-side id; translate it so + // result.filament_id stays an OF id like the rest of this struct (the fallback default, + // result.filament_id, is already OF and passes through unchanged). + const std::string fila_id = item.value("fila_id", result.filament_id); + auto* agent = wxGetApp().getAgent(); + result.filament_id = agent ? agent->to_orca_filament_id(fila_id) : fila_id; return result; } } @@ -211,8 +230,11 @@ int find_existing_decompose_component( auto* type_opt = project_config.option("filament_type"); const PresetBundle& preset_bundle = *wxGetApp().preset_bundle; const size_t num_physical = physical_colors.size(); - const std::string expected_basic_type = component.filament_id == kDecomposePetgFilamentId ? kDecomposePetgBasicType : - component.filament_id == kDecomposePlaFilamentId ? kDecomposePlaBasicType : ""; + // component.filament_id is our OF id; the two constants are the printer's own ids. + auto* agent = wxGetApp().getAgent(); + const std::string printer_filament_id = agent ? agent->from_orca_filament_id(component.filament_id) : component.filament_id; + const std::string expected_basic_type = printer_filament_id == kDecomposePetgFilamentId ? kDecomposePetgBasicType : + printer_filament_id == kDecomposePlaFilamentId ? kDecomposePlaBasicType : ""; const std::string expected_short_type = expected_basic_type == kDecomposePetgBasicType ? kDecomposePetgShortType : expected_basic_type == kDecomposePlaBasicType ? kDecomposePlaShortType : ""; const std::string expected_preset_part = expected_basic_type.empty() ? "" : std::string(kDecomposeBambuPresetPrefix) + expected_basic_type; diff --git a/src/slic3r/GUI/DeviceCore/DevFilaBlackList.cpp b/src/slic3r/GUI/DeviceCore/DevFilaBlackList.cpp index e75622720c..b59499d9d9 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaBlackList.cpp +++ b/src/slic3r/GUI/DeviceCore/DevFilaBlackList.cpp @@ -241,8 +241,11 @@ void check_filaments(const DevFilaBlacklist::CheckFilamentInfo& check_info, DevF std::set white_fila_ids = filament_item.contains("white_fila_ids") ? filament_item["white_fila_ids"].get>() : std::set(); if (!white_fila_ids.empty() && !check_info.fila_id.empty()) { - auto it = std::find_if(white_fila_ids.begin(), white_fila_ids.end(), [&check_info](const std::string& white_fila_id) { - return white_fila_id == check_info.fila_id; + // check_info.fila_id is our OF id; white_fila_ids in filaments_blacklist.json holds the printer's own. + auto* agent = Slic3r::GUI::wxGetApp().getAgent(); + const std::string printer_filament_id = agent ? agent->from_orca_filament_id(check_info.fila_id) : check_info.fila_id; + auto it = std::find_if(white_fila_ids.begin(), white_fila_ids.end(), [&printer_filament_id](const std::string& white_fila_id) { + return white_fila_id == printer_filament_id; }); if (it != white_fila_ids.end()) { continue; } } diff --git a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp index 806af9d5f0..e0a230969b 100644 --- a/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp +++ b/src/slic3r/GUI/DeviceCore/DevFilaSystem.cpp @@ -5,6 +5,7 @@ // TODO: remove this include #include "slic3r/GUI/DeviceManager.hpp" #include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/GUI_App.hpp" #include "DevUtil.h" #include "DevUtilBackend.h" @@ -95,7 +96,12 @@ std::string DevAmsTray::get_filament_type() if (m_fila_type == "Sup.ABS") { return "ABS-S"; } if (m_fila_type == "Support W") { return "PLA-S"; } if (m_fila_type == "Support G") { return "PA-S"; } - if (m_fila_type == "Support") { if (setting_id == "GFS00") { m_fila_type = "PLA-S"; } else if (setting_id == "GFS01") { m_fila_type = "PA-S"; } else { return "PLA-S"; } } + // setting_id is our OF id; GFS00/GFS01 are the printer's own support-filament ids. + if (m_fila_type == "Support") { + auto* agent = GUI::wxGetApp().getAgent(); + const std::string printer_filament_id = agent ? agent->from_orca_filament_id(setting_id) : setting_id; + if (printer_filament_id == "GFS00") { m_fila_type = "PLA-S"; } else if (printer_filament_id == "GFS01") { m_fila_type = "PA-S"; } else { return "PLA-S"; } + } return m_fila_type; } @@ -654,11 +660,14 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS curr_tray->setting_id = (*tray_it)["tray_info_idx"].get(); //std::string type = (*tray_it)["tray_type"].get(); std::string type = MachineObject::setting_id_to_type(curr_tray->setting_id, (*tray_it)["tray_type"].get()); - if (curr_tray->setting_id == "GFS00") + // curr_tray->setting_id is our OF id; GFS00/GFS01 are the printer's own support-filament ids. + auto* agent = GUI::wxGetApp().getAgent(); + const std::string printer_filament_id = agent ? agent->from_orca_filament_id(curr_tray->setting_id) : curr_tray->setting_id; + if (printer_filament_id == "GFS00") { curr_tray->m_fila_type = "PLA-S"; } - else if (curr_tray->setting_id == "GFS01") + else if (printer_filament_id == "GFS01") { curr_tray->m_fila_type = "PA-S"; } diff --git a/src/slic3r/GUI/DeviceManager.cpp b/src/slic3r/GUI/DeviceManager.cpp index b28e410557..782574c220 100644 --- a/src/slic3r/GUI/DeviceManager.cpp +++ b/src/slic3r/GUI/DeviceManager.cpp @@ -110,7 +110,9 @@ bool Slic3r::is_stringing_prone_filament(const std::string& filament_id, float n if (filament_id.empty()) return false; const auto* set = pick_stringing_set(nozzle_diameter); if (!set) return false; - return set->count(filament_id) > 0; + // filament_id is one of our content-addressed OF ids; the table above is keyed by the printer's own. + auto* agent = Slic3r::GUI::wxGetApp().getAgent(); + return set->count(agent ? agent->from_orca_filament_id(filament_id) : filament_id) > 0; } wxString Slic3r::get_stage_string(int stage) @@ -5048,10 +5050,13 @@ DevAmsTray MachineObject::parse_vt_tray(json vtray) vt_tray.setting_id = vtray["tray_info_idx"].get(); //std::string type = vtray["tray_type"].get(); std::string type = setting_id_to_type(vt_tray.setting_id, vtray["tray_type"].get()); - if (vt_tray.setting_id == "GFS00") { + // vt_tray.setting_id is our OF id (translated on the way in); the two support ids below are the printer's own. + auto* agent = GUI::wxGetApp().getAgent(); + const std::string printer_filament_id = agent ? agent->from_orca_filament_id(vt_tray.setting_id) : vt_tray.setting_id; + if (printer_filament_id == "GFS00") { vt_tray.m_fila_type = "PLA-S"; } - else if (vt_tray.setting_id == "GFS01") { + else if (printer_filament_id == "GFS01") { vt_tray.m_fila_type = "PA-S"; } else { diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 0347803dbc..ff10b26b6d 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -8823,6 +8823,11 @@ std::vector Plater::priv::load_files(const std::vector& input_ if (wipe_tower_y_opt) file_wipe_tower_y = *wipe_tower_y_opt; + if (auto* agent = wxGetApp().getAgent()) { + if (auto* ids = config.opt("filament_ids")) + for (std::string& id : ids->values) + id = agent->to_orca_filament_id(id); + } preset_bundle->load_config_model(filename.string(), std::move(config), file_version); ConfigOption* bed_type_opt = preset_bundle->project_config.option("curr_bed_type"); @@ -18669,6 +18674,8 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy nozzle_diameter_str = nozzle_diameter_option->serialize(); std::string printer_model_id = preset_bundle.printers.get_edited_preset().get_printer_type(&preset_bundle); + // The printer reads slice_info.config and knows only its own catalog ids. + auto* id_agent = preset_bundle.is_bbl_vendor() ? wxGetApp().getAgent() : nullptr; for (int i = 0; i < plate_data_list.size(); i++) { PlateData *plate_data = plate_data_list[i]; @@ -18678,6 +18685,8 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy std::string display_filament_type; it->type = cfg.get_filament_type(display_filament_type, it->id); it->filament_id = filament_id_opt ? filament_id_opt->get_at(it->id) : ""; + if (id_agent) + it->filament_id = id_agent->from_orca_filament_id(it->filament_id); it->color = filament_color ? filament_color->get_at(it->id) : "#FFFFFF"; // save filament info used in curr plate int index = p->partplate_list.get_curr_plate_index(); diff --git a/src/slic3r/GUI/PresetComboBoxes.cpp b/src/slic3r/GUI/PresetComboBoxes.cpp index 837f4ebe0c..ac419073d7 100644 --- a/src/slic3r/GUI/PresetComboBoxes.cpp +++ b/src/slic3r/GUI/PresetComboBoxes.cpp @@ -873,10 +873,15 @@ PlaterPresetComboBox::PlaterPresetComboBox(wxWindow *parent, Preset::Type preset auto fila_type = Preset::remove_suffix_modified(GetValue().ToUTF8().data()); bool is_official = boost::algorithm::starts_with(fila_type, "Bambu"); if (is_official) { - // Get filament_id from filament_presets + // Get filament_id from filament_presets. FilamentPickerDialog looks up + // filaments_color_codes.json, which is downloaded from Bambu and keyed by the + // printer's own ids, so translate our OF id (the "GFA00" fallback is already one). const std::string& preset_name = m_preset_bundle->filament_presets[m_filament_idx]; const Preset* selected_preset = m_collection->find_preset(preset_name); - wxString fila_id = selected_preset ? wxString::FromUTF8(selected_preset->filament_id) : "GFA00"; + auto* agent = wxGetApp().getAgent(); + wxString fila_id = "GFA00"; + if (selected_preset) + fila_id = wxString::FromUTF8(agent ? agent->from_orca_filament_id(selected_preset->filament_id) : selected_preset->filament_id); FilamentColor fila_color = get_cur_color_info(); // Show filament picker dialog diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index 199de9236e..e70ba84225 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -3845,8 +3845,12 @@ int SelectMachineDialog::update_print_required_data(Slic3r::DynamicPrintConfig c m_required_data_config = config; m_required_data_model = model; //m_required_data_plate_data_list = plate_data_list; + auto* agent = wxGetApp().getAgent(); for (auto i = 0; i < plate_data_list.size(); i++) { if (!plate_data_list[i]->gcode_file.empty()) { + if (agent) + for (auto& info : plate_data_list[i]->slice_filaments_info) + info.filament_id = agent->to_orca_filament_id(info.filament_id); m_required_data_plate_data_list.push_back(plate_data_list[i]); } } @@ -5051,8 +5055,11 @@ void SelectMachineDialog::update_show_status(MachineObject* obj_) const auto& warning_tpu_filaments = DevPrinterConfigUtil::get_value_from_config>(obj_->printer_type, "auto_on_cali_warning_tpu_filaments"); if (!warning_tpu_filaments.empty()) { + auto* agent = wxGetApp().getAgent(); for (const auto& fila : m_ams_mapping_result) { - if (std::find(warning_tpu_filaments.begin(), warning_tpu_filaments.end(), fila.filament_id) != warning_tpu_filaments.end()) { + // fila.filament_id is our OF id; the printer config list holds the printer's own. + const std::string printer_filament_id = agent ? agent->from_orca_filament_id(fila.filament_id) : fila.filament_id; + if (std::find(warning_tpu_filaments.begin(), warning_tpu_filaments.end(), printer_filament_id) != warning_tpu_filaments.end()) { show_status(PrintDialogStatus::PrintStatusTPUUnsuggestCali, { _L("If 'Dynamic Flow Calibration' is set to Auto/On, the system will use the manual calibration value or the default value and skip the flow calibration process. You can perform a manual flow calibration for TPU filament on the 'Calibration' page.") }); break; @@ -5208,9 +5215,12 @@ bool SelectMachineDialog::can_support_pa_auto_cali() std::vector unsupport_auto_cali_filaments = DevPrinterConfigUtil::get_unsupport_auto_cali_filaments(obj->printer_type); if (!unsupport_auto_cali_filaments.empty()) { + auto* agent = wxGetApp().getAgent(); auto iter = std::find_if(m_filaments.begin(), m_filaments.end(), - [&unsupport_auto_cali_filaments](const FilamentInfo &item) { - auto iter = std::find(unsupport_auto_cali_filaments.begin(), unsupport_auto_cali_filaments.end(), item.filament_id); + [&unsupport_auto_cali_filaments, agent](const FilamentInfo &item) { + // item.filament_id is our OF id; the printer config list holds the printer's own. + const std::string printer_filament_id = agent ? agent->from_orca_filament_id(item.filament_id) : item.filament_id; + auto iter = std::find(unsupport_auto_cali_filaments.begin(), unsupport_auto_cali_filaments.end(), printer_filament_id); return iter != unsupport_auto_cali_filaments.end(); }); diff --git a/src/slic3r/Utils/BBLPrinterAgent.cpp b/src/slic3r/Utils/BBLPrinterAgent.cpp index ef85e0a1ff..5e73edf84c 100644 --- a/src/slic3r/Utils/BBLPrinterAgent.cpp +++ b/src/slic3r/Utils/BBLPrinterAgent.cpp @@ -1,11 +1,121 @@ #include "BBLPrinterAgent.hpp" #include "BBLNetworkPlugin.hpp" #include "NetworkAgentFactory.hpp" +#include "libslic3r/Utils.hpp" #include +#include +#include +using json = nlohmann::json; + +#include namespace Slic3r { +namespace { + +// Bambu's own catalog ids for every filament this app ships, Bambu's own included, since Orca +// content-addresses those too. Keyed both ways so each of the four translation entry points +// below is a single lookup. Loaded once per process, on first use. A missing or malformed file +// logs once and leaves both maps empty, so every translation degrades to identity. Same shape +// as DevFilaBlacklist::load_filaments_blacklist_config. +struct BambuFilamentIdMap { std::unordered_map to_bambu, to_orca; }; + +const BambuFilamentIdMap& bambu_filament_id_map() +{ + static const BambuFilamentIdMap map = [] { + BambuFilamentIdMap m; + const std::string path = resources_dir() + "/printers/bambu_filament_ids.json"; + try { + boost::nowide::ifstream file(path); + if (!file.is_open()) { + BOOST_LOG_TRIVIAL(warning) << "Bambu filament id map not found, ids pass through untranslated: " << path; + return m; + } + json doc; + file >> doc; + for (const auto& [orca_filament_id, row] : doc.at("filaments").items()) { + const std::string bambu_id = row.at("bambu_id").get(); + m.to_bambu.emplace(orca_filament_id, bambu_id); + m.to_orca.emplace(bambu_id, orca_filament_id); + } + } catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << "Bambu filament id map unreadable, ids pass through untranslated: " << e.what(); + m = {}; + } + return m; + }(); + return map; +} + +// Rewrites every string under "tray_info_idx", "filament_id" or "filamentId", at any depth, in place. +void rewrite_filament_ids(json& j, const std::unordered_map& map) +{ + if (j.is_object()) { + for (auto& [key, value] : j.items()) { + if (value.is_string() && (key == "tray_info_idx" || key == "filament_id" || key == "filamentId")) { + auto it = map.find(value.get_ref()); + if (it != map.end()) + value = it->second; + } else + rewrite_filament_ids(value, map); + } + } else if (j.is_array()) + for (auto& element : j) + rewrite_filament_ids(element, map); +} + +// Text that does not parse as JSON, or that mentions none of the id keys, comes back byte-identical. +std::string rewrite_filament_ids(std::string text, const std::unordered_map& map) +{ + if (map.empty() || (text.find("tray_info_idx") == std::string::npos && text.find("filament_id") == std::string::npos && + text.find("filamentId") == std::string::npos)) + return text; // nothing to map, skip the parse (moved, not copied) + try { + json j = json::parse(text); + rewrite_filament_ids(j, map); + return j.dump(); + } catch (const std::exception&) { + return text; // not JSON: forward as received + } +} + +// Wraps an inbound message callback so every Bambu id it delivers arrives already translated. +// A null fn is a deregistration (see GUI_App.cpp's shutdown phase 1 and NetworkAgent::apply_printer_callbacks +// clearing callbacks with {}), and must stay null rather than become a live wrapper around an empty target. +OnMessageFn to_orca_messages(OnMessageFn fn) +{ + if (!fn) + return fn; + return [fn = std::move(fn)](std::string dev_id, std::string msg) { fn(std::move(dev_id), BBLPrinterAgent::to_orca_payload(std::move(msg))); }; +} + +} // namespace + +std::string BBLPrinterAgent::to_orca_filament_id(const std::string& printer_filament_id) const +{ + const auto& map = bambu_filament_id_map().to_orca; + auto it = map.find(printer_filament_id); + return it != map.end() ? it->second : printer_filament_id; +} + +std::string BBLPrinterAgent::from_orca_filament_id(const std::string& orca_filament_id) const +{ + const auto& map = bambu_filament_id_map().to_bambu; + auto it = map.find(orca_filament_id); + return it != map.end() ? it->second : orca_filament_id; +} + +std::string BBLPrinterAgent::to_orca_payload(std::string json_text) +{ + return rewrite_filament_ids(std::move(json_text), bambu_filament_id_map().to_orca); +} + +std::string BBLPrinterAgent::from_orca_payload(std::string json_text) +{ + return rewrite_filament_ids(std::move(json_text), bambu_filament_id_map().to_bambu); +} + BBLPrinterAgent::BBLPrinterAgent() = default; BBLPrinterAgent::~BBLPrinterAgent() = default; @@ -22,6 +132,7 @@ void BBLPrinterAgent::set_cloud_agent(std::shared_ptr cloud) int BBLPrinterAgent::send_message(std::string dev_id, std::string json_str, int qos, int flag) { + json_str = from_orca_payload(std::move(json_str)); auto& plugin = BBLNetworkPlugin::instance(); auto agent = plugin.get_agent(); auto func = plugin.get_send_message(); @@ -67,6 +178,7 @@ int BBLPrinterAgent::disconnect_printer() int BBLPrinterAgent::send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) { + json_str = from_orca_payload(std::move(json_str)); auto& plugin = BBLNetworkPlugin::instance(); auto agent = plugin.get_agent(); auto func = plugin.get_send_message_to_printer(); @@ -321,6 +433,7 @@ int dispatch_start(CurrentFn func, PrintParams& params, const CallbackFns&... ca auto agent = plugin.get_agent(); if (!func || !agent) return -1; + params.ams_mapping_info = BBLPrinterAgent::from_orca_payload(std::move(params.ams_mapping_info)); switch (plugin.network_abi()) { case NetworkAbi::Legacy: return reinterpret_cast(func)(agent, BBLNetworkPlugin::as_legacy(params), callbacks...); @@ -408,7 +521,7 @@ int BBLPrinterAgent::set_on_message_fn(OnMessageFn fn) auto agent = plugin.get_agent(); auto func = plugin.get_set_on_message_fn(); if (func && agent) { - return func(agent, fn); + return func(agent, to_orca_messages(std::move(fn))); } return -1; } @@ -441,7 +554,7 @@ int BBLPrinterAgent::set_on_local_message_fn(OnMessageFn fn) auto agent = plugin.get_agent(); auto func = plugin.get_set_on_local_message_fn(); if (func && agent) { - return func(agent, fn); + return func(agent, to_orca_messages(std::move(fn))); } return -1; } diff --git a/src/slic3r/Utils/BBLPrinterAgent.hpp b/src/slic3r/Utils/BBLPrinterAgent.hpp index a8880bf6bf..83e21bb6d8 100644 --- a/src/slic3r/Utils/BBLPrinterAgent.hpp +++ b/src/slic3r/Utils/BBLPrinterAgent.hpp @@ -84,6 +84,20 @@ public: int set_queue_on_main_fn(QueueOnMainFn fn) override; FilamentSyncMode get_filament_sync_mode() const override; + // Bambu's own catalog ids. Orca content-addresses every system filament, Bambu's included; + // the printer, the AMS and Bambu's cloud know only Bambu's ids, so this agent translates at + // the boundary through resources/printers/bambu_filament_ids.json (generated by + // scripts/update_bambu_filament_ids.py). An id without a map row is returned as it is. + std::string to_orca_filament_id(const std::string& printer_filament_id) const override; + std::string from_orca_filament_id(const std::string& orca_filament_id) const override; + + // Rewrite every string under "tray_info_idx", "filament_id" or "filamentId", at any depth, + // in a JSON document (an MQTT payload or PrintParams::ams_mapping_info). Text that does not + // parse, or contains none of the keys, is returned unchanged. + // Taken by value: most outbound traffic carries no filament id and is moved straight back out. + static std::string to_orca_payload(std::string json_text); + static std::string from_orca_payload(std::string json_text); + private: std::shared_ptr m_cloud_agent; }; diff --git a/src/slic3r/Utils/CalibUtils.cpp b/src/slic3r/Utils/CalibUtils.cpp index 473f62337f..499228d13c 100644 --- a/src/slic3r/Utils/CalibUtils.cpp +++ b/src/slic3r/Utils/CalibUtils.cpp @@ -62,22 +62,29 @@ std::vector not_support_auto_pa_cali_filaments = { void get_default_k_n_value(const std::string &filament_id, float &k, float &n) { - if (filament_id.compare("GFU01") == 0) { + // filament_id is our OF id; the literals below are the printer's own. An id the agent has + // no mapping for (e.g. a caller still on the old id) passes through unchanged. + auto* agent = wxGetApp().getAgent(); + const std::string printer_filament_id = agent ? agent->from_orca_filament_id(filament_id) : filament_id; + if (printer_filament_id.compare("GFU01") == 0) { /* TPU 95A */ k = 0.25; n = 1.0; - } else if (filament_id.compare("GFU03") == 0) { + } else if (printer_filament_id.compare("GFU03") == 0) { /* TPU 90A */ k = 0.35; n = 1.0; - } else if (filament_id.compare("GFU04") == 0) { + } else if (printer_filament_id.compare("GFU04") == 0) { /* TPU 85A */ k = 0.65; n = 1.0; - } else if (filament_id.compare("GFG00") == 0 || filament_id.compare("GFG01") == 0 || filament_id.compare("GFG60") == 0 || filament_id.compare("GFL06") == 0 || - filament_id.compare("GFL55") == 0 || filament_id.compare("GFG99") == 0 || filament_id.compare("GFG98") == 0 || filament_id.compare("GFG97") == 0 || - filament_id.compare("GFG50") == 0 || filament_id.compare("GFU02") == 0 || filament_id.compare("GFU98") == 0 || filament_id.compare("GFS00") == 0 || - filament_id.compare("GFS02") == 0) { + } else if (printer_filament_id.compare("GFG00") == 0 || printer_filament_id.compare("GFG01") == 0 || + printer_filament_id.compare("GFG60") == 0 || printer_filament_id.compare("GFL06") == 0 || + printer_filament_id.compare("GFL55") == 0 || printer_filament_id.compare("GFG99") == 0 || + printer_filament_id.compare("GFG98") == 0 || printer_filament_id.compare("GFG97") == 0 || + printer_filament_id.compare("GFG50") == 0 || printer_filament_id.compare("GFU02") == 0 || + printer_filament_id.compare("GFU98") == 0 || printer_filament_id.compare("GFS00") == 0 || + printer_filament_id.compare("GFS02") == 0) { /* 0.04 filaments */ k = 0.04; n = 1.0; @@ -1393,7 +1400,10 @@ void CalibUtils::calib_retraction(const CalibInfo &calib_info, wxString &error_m bool CalibUtils::is_support_auto_pa_cali(std::string filament_id) { - auto iter = std::find(not_support_auto_pa_cali_filaments.begin(), not_support_auto_pa_cali_filaments.end(), filament_id); + // filament_id is our OF id; not_support_auto_pa_cali_filaments holds the printer's own ids. + auto* agent = wxGetApp().getAgent(); + const std::string printer_filament_id = agent ? agent->from_orca_filament_id(filament_id) : filament_id; + auto iter = std::find(not_support_auto_pa_cali_filaments.begin(), not_support_auto_pa_cali_filaments.end(), printer_filament_id); if (iter != not_support_auto_pa_cali_filaments.end()) { return false; } diff --git a/src/slic3r/Utils/IPrinterAgent.hpp b/src/slic3r/Utils/IPrinterAgent.hpp index 0fa3616344..1f0d45dc1e 100644 --- a/src/slic3r/Utils/IPrinterAgent.hpp +++ b/src/slic3r/Utils/IPrinterAgent.hpp @@ -290,6 +290,16 @@ public: * Populates the MachineObject's DevFilaSystem with fetched filament data. */ virtual bool fetch_filament_info(std::string dev_id) { return false; } + + /** + * Translate one filament id across the printer boundary. + * + * Orca content-addresses every system filament; a printer, its AMS and its vendor cloud + * know only that vendor's own catalog ids. An agent whose printers already speak Orca's + * ids leaves them alone, and so does an id with no mapping. + */ + virtual std::string to_orca_filament_id(const std::string& printer_filament_id) const { return printer_filament_id; } + virtual std::string from_orca_filament_id(const std::string& orca_filament_id) const { return orca_filament_id; } }; } // namespace Slic3r diff --git a/src/slic3r/Utils/NetworkAgent.cpp b/src/slic3r/Utils/NetworkAgent.cpp index 0d77e5e660..b15e10c591 100644 --- a/src/slic3r/Utils/NetworkAgent.cpp +++ b/src/slic3r/Utils/NetworkAgent.cpp @@ -929,6 +929,20 @@ bool NetworkAgent::fetch_filament_info(std::string dev_id) return false; } +std::string NetworkAgent::to_orca_filament_id(const std::string& printer_filament_id) const +{ + if (m_printer_agent) + return m_printer_agent->to_orca_filament_id(printer_filament_id); + return printer_filament_id; +} + +std::string NetworkAgent::from_orca_filament_id(const std::string& orca_filament_id) const +{ + if (m_printer_agent) + return m_printer_agent->from_orca_filament_id(orca_filament_id); + return orca_filament_id; +} + int NetworkAgent::request_bind_ticket(std::string* ticket) { if (m_printer_agent) diff --git a/src/slic3r/Utils/NetworkAgent.hpp b/src/slic3r/Utils/NetworkAgent.hpp index d7032b7a20..76128073a4 100644 --- a/src/slic3r/Utils/NetworkAgent.hpp +++ b/src/slic3r/Utils/NetworkAgent.hpp @@ -165,6 +165,8 @@ public: int start_sdcard_print(PrintParams params, OnUpdateStatusFn update_fn, WasCancelledFn cancel_fn); FilamentSyncMode get_filament_sync_mode() const; bool fetch_filament_info(std::string dev_id); + std::string to_orca_filament_id(const std::string& printer_filament_id) const; + std::string from_orca_filament_id(const std::string& orca_filament_id) const; int request_bind_ticket(std::string* ticket); int get_hms_snapshot(std::string dev_id, std::string file_name, std::function callback); diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index eca1af2b86..2ab78f56de 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -1,6 +1,7 @@ get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp + test_bambu_filament_ids.cpp test_creality_cfs_match.cpp test_dev_mapping.cpp test_filament_bitmap_utils.cpp diff --git a/tests/slic3rutils/test_bambu_filament_ids.cpp b/tests/slic3rutils/test_bambu_filament_ids.cpp new file mode 100644 index 0000000000..5f6215d7ca --- /dev/null +++ b/tests/slic3rutils/test_bambu_filament_ids.cpp @@ -0,0 +1,81 @@ +#include +#include +#include +#include + +#include "libslic3r/Utils.hpp" +#include "slic3r/Utils/BBLPrinterAgent.hpp" +#include "slic3r/Utils/OrcaPrinterAgent.hpp" + +using json = nlohmann::json; +using namespace Slic3r; + +namespace { + +// Point resources_dir() at the repo's own tree for the lifetime of a test and restore it +// afterwards, mirroring ScopedResourcesDir (which only ever makes a throwaway directory). +// PROFILES_DIR is /resources/profiles; the map lives in /resources/printers. +struct ScopedRepoResourcesDir +{ + std::string previous{resources_dir()}; + + ScopedRepoResourcesDir() { set_resources_dir(boost::filesystem::path(PROFILES_DIR).parent_path().string()); } + ~ScopedRepoResourcesDir() { set_resources_dir(previous); } + + ScopedRepoResourcesDir(const ScopedRepoResourcesDir&) = delete; + ScopedRepoResourcesDir& operator=(const ScopedRepoResourcesDir&) = delete; +}; + +std::string orca_id_of(const std::string& bambu_id) +{ + boost::nowide::ifstream file(resources_dir() + "/printers/bambu_filament_ids.json"); + json doc; + file >> doc; + for (const auto& [orca_id, row] : doc["filaments"].items()) + if (row["bambu_id"] == bambu_id) + return orca_id; + FAIL("no map row for " << bambu_id); + return {}; +} + +} // namespace + +TEST_CASE("Bambu filament id map is one-to-one and leaves unmapped ids alone", "[BambuFilamentIds]") +{ + const ScopedRepoResourcesDir repo_resources; + const BBLPrinterAgent bbl; + const std::string abs = orca_id_of("GFB00"); // Bambu ABS + REQUIRE(abs.rfind("OF", 0) == 0); + CHECK(bbl.from_orca_filament_id(abs) == "GFB00"); + CHECK(bbl.to_orca_filament_id("GFB00") == abs); + CHECK(bbl.from_orca_filament_id("OFnotarow") == "OFnotarow"); + CHECK(bbl.to_orca_filament_id("GFZZ99") == "GFZZ99"); // Bambu id we do not ship + CHECK(bbl.to_orca_filament_id("P1234567") == "P1234567"); // user root + CHECK(bbl.from_orca_filament_id("") == ""); + + // An agent whose printers already speak our ids inherits IPrinterAgent's identity default. + const OrcaPrinterAgent other{""}; + CHECK(other.from_orca_filament_id(abs) == abs); + CHECK(other.to_orca_filament_id("GFB00") == "GFB00"); +} + +TEST_CASE("Payload rewrite covers nested trays, calibration lists and mapping info", "[BambuFilamentIds]") +{ + const ScopedRepoResourcesDir repo_resources; + const std::string abs = orca_id_of("GFB00"), pla = orca_id_of("GFA00"); + const std::string status = + R"({"print":{"ams":{"ams":[{"tray":[{"tray_info_idx":"GFB00"},{"tray_info_idx":"GFZZ99"}]}]},)" + R"("vt_tray":{"tray_info_idx":"GFA00"},"filaments":[{"filament_id":"GFA00","setting_id":"GFB00"}]}})"; + json inbound = json::parse(BBLPrinterAgent::to_orca_payload(status)); + CHECK(inbound["print"]["ams"]["ams"][0]["tray"][0]["tray_info_idx"] == abs); + CHECK(inbound["print"]["ams"]["ams"][0]["tray"][1]["tray_info_idx"] == "GFZZ99"); // no row: untouched + CHECK(inbound["print"]["vt_tray"]["tray_info_idx"] == pla); + CHECK(inbound["print"]["filaments"][0]["filament_id"] == pla); + CHECK(inbound["print"]["filaments"][0]["setting_id"] == "GFB00"); // not an id key: has a map row but must stay put + CHECK(json::parse(BBLPrinterAgent::from_orca_payload(inbound.dump())) == json::parse(status)); // round trip + + const std::string mapping = R"([{"ams":0,"filamentId":")" + abs + R"(","filamentType":"ABS"}])"; + CHECK(BBLPrinterAgent::from_orca_payload(mapping) == R"([{"ams":0,"filamentId":"GFB00","filamentType":"ABS"}])"); + CHECK(BBLPrinterAgent::from_orca_payload("not json") == "not json"); + CHECK(BBLPrinterAgent::from_orca_payload(R"({"print":{"command":"pushall"}})") == R"({"print":{"command":"pushall"}})"); +}