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"}})"); +}