v3.0: content-addressed mint tooling, succession runtime, W1 hardening

Implements phase v3.0 of filament_id_plan_v3.md - tooling and client
prerequisites. No filament_id values change in this commit.

Tooling (scripts/assign_filament_ids.py; 106 unit tests):
- The mint key becomes "filament_product/<filament_vendor>/<filament_type>/
  <family_name>", resolved loader-faithfully from the declarer's flattened
  config - bundle-independent and content-addressed; identity fixes re-id
  by design and are made safe by the succession ledger. --mint now takes
  the triple.
- The snapshot gains "triples" and "triple_exceptions" sections, folded
  into the equality gate: any vendor/type/name change surfaces as a
  reviewable snapshot diff. Check 3 rewritten to triple-mint conformance
  with (id, triple) grandfathering; check 5 generalized from OFL generics
  to every OFL-riding vendor preset; new check 8 (triple integrity) and
  check 9 (succession integrity).
- The retired ledger moves to resources/profiles/retired_filament_ids.json
  so it ships with the app; schema {claims, successor} plus cross-island
  "hints". The 14 v1 entries are migrated with mode-rule successors.
  Vanished ids in a foreign island's space (GF*/QD_*) are RELEASED with a
  hint instead of retired: the island catalog owns them and may
  legitimately (re)ship them, which check 4 must never block.
- New maintenance modes: --remint VENDOR, --drop-redundant-ids VENDOR,
  --add-hint "OLD=NEW", and --update-snapshot --forget-never-shipped FILE
  (never-shipped ids drop from lineage; chains splice through them).

Runtime (C++):
- Succession helpers in libslic3r/Preset (pure chain-follow + lazy ledger
  load from resources), consulted only on resolution miss in
  get_filament_by_filament_id, both AMS sync predicates,
  add_ams_filaments, setting_id_to_type, and the calibration-history
  lookup; behavior is byte-identical while the ledger has no matching
  entry. Catch2 coverage in tests/libslic3r.
- W1 hardening: check_ams_filament_valid no longer remote-wipes trays or
  rewrites temps for P-shaped ids that a system preset carries (ten such
  system ids ship today); the size==8 && [0]=='P' assert is relaxed; the
  unguarded filament_list find deref in the temp-equation check returns
  non-destructively on a miss.
- MoonrakerPrinterAgent: the 23 hardcoded OFL generic ids are replaced by
  a runtime lookup of "Generic <family> @System" (id-equivalent on
  today's shipped profiles, follows future re-mints automatically);
  scripts/test_moonraker_lane_data.py derives its expectations from the
  shipped profiles and gains --check-ofl-map.

Profile data (W3 - type is now a key component, so type bugs are fixed
before any re-mint):
- 17 same-name-different-type groups corrected across 50 files (OFL
  Generic PETG-CF/PE-CF/PP-CF; Flashforge ASA Basic/ASA-CF/ABS-CF/HIPS/
  PAHT-CF/PLA Silk; FusRock PAHT; Creality Generic PA6-CF; InfiMech PETG;
  Anycubic TPU 95A / TPU for ACE; Prusa Generic PA-CF/PLA-CF) - every
  value validated against MaterialType::all(); 3 Snapmaker U1 roots gain
  their missing filament_vendor. Flattened-config equivalence vs the
  previous commit: exactly the 54 intended diffs, zero BBL.
- doc/developer-reference/filament_id.md rewritten for the v3 rule.

Ambiguous type divergences (Prusa Generic TPU/TPU HF FLEX-vs-TPU, FusRock
S-Multi/S-PAHT) and all same-type vendor-tag divergences are deferred to
the v3.2 vendor worksheets, catalogued with analysis.
This commit is contained in:
SoftFever
2026-07-04 16:28:35 +08:00
parent 67406b24ba
commit 2c0867619c
67 changed files with 13311 additions and 330 deletions

View File

@@ -69,9 +69,12 @@ the `Generic X` base name, write no id key).
`Generic X @System`, keeps the `Generic X` base name (that alias is what hides the library `Generic X @System`, keeps the `Generic X` base name (that alias is what hides the library
preset on your printers), sets a non-empty `compatible_printers`, and writes no id key. preset on your printers), sets a non-empty `compatible_printers`, and writes no id key.
A vendor-*branded* filament never rides a generic family id. A vendor-*branded* filament never rides a generic family id.
5. **Ids are immutable once shipped.** Renaming a family does not change its id — use 5. **Ids follow the product identity.** The id is a pure function of the product triple
`renamed_from`. A shipped id is never recycled for a different material: old ids live on in `(filament_vendor, filament_type, family name)` — correcting any of them re-mints the id
user presets and 3mf files and a recycled id would silently match the wrong material. **by design**, and `--update-snapshot` records the old id in the shipped succession ledger
with its successor so device trays, calibration records, and user presets keep resolving
(`renamed_from` still gates preset-*name* compatibility as before). A shipped id is never
recycled for a different material: retired ids are blocked forever.
## Minting — nobody invents ids ## Minting — nobody invents ids
@@ -81,15 +84,20 @@ New ids are deterministic, computed exactly like the `setting_id` precedent
```text ```text
FILAMENT_ID_NAMESPACE = uuid5(setting-id NAMESPACE, "filament_id") FILAMENT_ID_NAMESPACE = uuid5(setting-id NAMESPACE, "filament_id")
= c4d3ff49-4c32-5534-a3e3-00894157ab97 = c4d3ff49-4c32-5534-a3e3-00894157ab97
filament_id = "OF" + base62_6( uuid5(FILAMENT_ID_NAMESPACE, "filament_family/<vendor>/<family>") ) filament_id = "OF" + base62_6( uuid5(FILAMENT_ID_NAMESPACE,
"filament_product/<filament_vendor>/<filament_type>/<family_name>") )
``` ```
`base62_6` is the low 6 base62 digits (alphabet `0-9A-Za-z`) of the UUID taken as a big-endian `base62_6` is the low 6 base62 digits (alphabet `0-9A-Za-z`) of the UUID taken as a big-endian
integer, most-significant digit first — 8 chars total, within the AMS length limit. `<vendor>` integer, most-significant digit first — 8 chars total, within the AMS length limit. The triple
is the vendor bundle name (the stem of the vendor index json, e.g. `Qidi`, comes from the family root's *flattened* config: `<filament_vendor>` is the filament
`OrcaFilamentLibrary`); `<family>` is the family base name. On the rare collision with any **manufacturer** (`"Polymaker"`, or `"Generic"` for generics — never the printer brand),
existing or retired id, the minter salts the input (`…/1`, `…/2`, …) until free and the result `<filament_type>` the material type, `<family_name>` the root's base name; the two config
is frozen in the file. Example: family `MyBrand PLA` in vendor `Orca 3D` mints `OFvCEY5V`. values are inheritable list options and the first element counts. The key contains no bundle
name, so the same product mints the same id in every bundle — hoisting a family into
OrcaFilamentLibrary never changes its id. On the rare collision with any existing or retired
id, the minter salts the input (`…/1`, `…/2`, …) until free and the result is frozen in the
file. Example: `Polymaker/PLA/PolyLite PLA` mints `OF5CgdDq`.
Workflow for a new family: Workflow for a new family:
@@ -101,8 +109,12 @@ python scripts/assign_filament_ids.py --check # 4. verify — the sam
# 5. Commit the profile edits together with scripts/filament_id_snapshot.json. # 5. Commit the profile edits together with scripts/filament_id_snapshot.json.
``` ```
`--mint "Vendor/Family"` prints the id for one family without touching anything. The default `--mint "filament_vendor/filament_type/family_name"` prints the id a triple would mint without
run is idempotent and never rewrites a valid existing id. touching anything. The default run is idempotent and never rewrites a valid existing id.
Maintenance modes (normally only used by id migrations): `--remint VENDOR` re-derives a
vendor's declared ids from their triples, `--drop-redundant-ids VENDOR` deletes declarations
that merely re-declare an inherited OFL id, and `--add-hint "OLD=NEW"` records a cross-island
succession hint.
If you skip the tooling, CI fails and prints the remedy: the expected id for your family, and If you skip the tooling, CI fails and prints the remedy: the expected id for your family, and
the instruction to run `python scripts/assign_filament_ids.py --update-snapshot` and commit the instruction to run `python scripts/assign_filament_ids.py --update-snapshot` and commit
@@ -116,7 +128,20 @@ the resulting diff.
| `QD_*` | Qidi device protocol | frozen device contract; Qidi vendor only | | `QD_*` | Qidi device protocol | frozen device contract; Qidi vendor only |
| `P` + 7 hex chars (case-insensitive), `"null"` | user-created custom filaments (`CreatePresetsDialog.cpp`) | never appears in system profiles | | `P` + 7 hex chars (case-insensitive), `"null"` | user-created custom filaments (`CreatePresetsDialog.cpp`) | never appears in system profiles |
| every already-shipped id | grandfather snapshot | frozen as-is; new claims need maintainer sign-off | | every already-shipped id | grandfather snapshot | frozen as-is; new claims need maintainer sign-off |
| every retired id | `scripts/retired_filament_ids.json` | never used again, for anything | | every retired id | `resources/profiles/retired_filament_ids.json` | never used again, for anything |
## The succession ledger
`resources/profiles/retired_filament_ids.json` ships with the app. Each retired id maps to
`{"claims": [...], "successor": <id|null>}` — successor chains are followed to the live end —
and a `hints` map carries the same forwarding for ids Orca cannot retire because another
island owns them (e.g. a `GF*` id whose material also exists as an OFL family). The client
consults the ledger **only on resolution miss** (AMS tray sync, tray-id type lookup,
calibration history, filament-id preset lookup): a live preset always wins first, so BBL
installs resolve `GF*` natively and behavior is unchanged wherever the raw id still exists.
This is what makes identity-driven re-mints (structure rule 5) safe: the old id keeps
resolving to the family's current preset instead of degrading to a `Generic <type>` fallback.
The file is append-only and maintained exclusively by `--update-snapshot` / `--add-hint`.
## How CI enforces this ## How CI enforces this
@@ -131,14 +156,22 @@ deterministically (running it twice changes nothing).
The checks, in brief: The checks, in brief:
- **Format** — every id is either in the snapshot, `OF` + 6 base62 chars, BBL's, or Qidi `QD_*`. - **Format** — every id is either in the snapshot, `OF` + 6 base62 chars, BBL's, or Qidi `QD_*`.
- **Snapshot equality** — tree claims == snapshot claims, both directions. - **Snapshot equality** — tree claims == snapshot claims **and** tree triples == snapshot
- **Mint conformance** — a non-grandfathered `OF*` claim must equal the mint (or a salt triples, both directions: any `filament_vendor`/`filament_type`/family-name change surfaces
iteration) of its `(vendor, family)`; the error prints the expected id. as a snapshot diff.
- **Retired reuse** — any tree id present in `scripts/retired_filament_ids.json` is an error. - **Mint conformance** — a non-grandfathered `OF*` id must equal the mint (or a salt
iteration) of its declarer's product triple; the error prints the expected id.
- **Retired reuse** — any tree id present in `resources/profiles/retired_filament_ids.json` is an error.
Ids that fully vanish from the tree are appended there by `--update-snapshot`; the file is Ids that fully vanish from the tree are appended there by `--update-snapshot`; the file is
**append-only**. **append-only**.
- **Alias hygiene** — a tuned generic must keep the library preset's base name and a non-empty - **Alias hygiene** — any vendor preset riding an OFL family id must keep the library
`compatible_printers` (structure rule 4); the error names the rename as the cause. preset's base name, a non-empty `compatible_printers`, and no own id key (structure rule 4);
the error names the rename as the cause.
- **Triple integrity** — every declarer outside the BBL/`QD_*` islands 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.
- **Succession integrity** — retired successor chains terminate at a live id (or null) with
no cycles; `hints` keys are live, island-owned ids.
- **Reserved namespaces** — `GF*` outside BBL, `QD_*` outside Qidi, `P<7-hex>` or `null` - **Reserved namespaces** — `GF*` outside BBL, `QD_*` outside Qidi, `P<7-hex>` or `null`
anywhere, unless that exact claim is grandfathered in the snapshot. anywhere, unless that exact claim is grandfathered in the snapshot.
- **Structure** — no `filament_id` key on instantiated presets; no declared-vs-inherited id - **Structure** — no `filament_id` key on instantiated presets; no declared-vs-inherited id
@@ -161,7 +194,10 @@ check instead.
inherit the family's root, write no id key. inherit the family's root, write no id key.
- **A tuned generic ("our profile for Generic PLA")?** Inherit `Generic PLA @System`, keep the - **A tuned generic ("our profile for Generic PLA")?** Inherit `Generic PLA @System`, keep the
`Generic PLA` base name, set `compatible_printers`, write no id key. `Generic PLA` base name, set `compatible_printers`, write no id key.
- **I need to rename a family.** Keep the id, add `renamed_from`. Ids never change. - **I need to rename a family (or fix its `filament_vendor`/`filament_type`).** Add
`renamed_from` for the name, run `--remint <Vendor>` then `--update-snapshot`: the id
re-derives from the corrected identity and the old id lands in the succession ledger
pointing at the new one. Commit the profile, snapshot, and ledger diffs together.
- **CI says my family needs an id.** Run `python scripts/assign_filament_ids.py`, then - **CI says my family needs an id.** Run `python scripts/assign_filament_ids.py`, then
`--update-snapshot`, and commit both diffs. Do not type an id by hand. `--update-snapshot`, and commit both diffs. Do not type an id by hand.

View File

@@ -84,7 +84,12 @@ holding OFL-only materials, on-device PA-calibration records, user-root preset c
followed to the live end). A retired id may never be minted again (check 4 unchanged). followed to the live end). A retired id may never be minted again (check 4 unchanged).
Cross-island *hint* entries are permitted for ids Orca cannot retire because another Cross-island *hint* entries are permitted for ids Orca cannot retire because another
island owns them (e.g. `GFL99 → <OFL Generic PLA id>`): consulted only when no live island owns them (e.g. `GFL99 → <OFL Generic PLA id>`): consulted only when no live
preset matches, so BBL installs still resolve `GFL99` natively first. preset matches, so BBL installs still resolve `GFL99` natively first. By the same
principle, an id in a foreign island's space (`GF*`/`QD_*`) that vanishes from the tree
is **released with a hint, never retired** — the island's catalog owns it and may
legitimately (re)ship it later, which check 4 must never block. (Implementation
amendment, v3.0: `--update-snapshot` routes such ids to `hints` automatically; hint
keys may be absent from the tree.)
- **Shipped and consulted at runtime.** The ledger ships in `resources/`; a small helper - **Shipped and consulted at runtime.** The ledger ships in `resources/`; a small helper
(`resolve_filament_id_succession(id)`) follows the chain and is consulted **only on (`resolve_filament_id_succession(id)`) follows the chain and is consulted **only on
resolution miss**, before the `Generic <type>` name fallback, in: resolution miss**, before the `Generic <type>` name fallback, in:
@@ -134,8 +139,9 @@ those families twice.
**v3.1 — OFL re-mint.** Every OFL-declared id re-derives from its triple (mirrors like **v3.1 — OFL re-mint.** Every OFL-declared id re-derives from its triple (mirrors like
`OGFA00`, generics like `OGFL99`, blocks like `OEPLAB00` — all of it); each old shipped id `OGFA00`, generics like `OGFL99`, blocks like `OEPLAB00` — all of it); each old shipped id
gains a succession entry pointing at its replacement; the 26 BBL×OFL byte-shared ids gains a succession entry pointing at its replacement; the 26 GF-shaped ids OFL declares
dissolve (BBL keeps its id; the OFL family gets its own — two-island purity). Snapshot (measured at v3.0: only one of them, `GFOT001`, is also declared by BBL) are released to
the BBL island space with hints at the re-minted families — two-island purity. Snapshot
regenerated; full gate battery + fixture overlays (this phase touches preset-visibility regenerated; full gate battery + fixture overlays (this phase touches preset-visibility
machinery only via ids, but the fixtures are cheap insurance). machinery only via ids, but the fixtures are cheap insurance).

View File

@@ -13,7 +13,7 @@
"Anycubic" "Anycubic"
], ],
"filament_type": [ "filament_type": [
"PEBA" "TPU"
], ],
"compatible_printers": [ "compatible_printers": [
"Anycubic Kobra S1 Max 0.6 nozzle" "Anycubic Kobra S1 Max 0.6 nozzle"

View File

@@ -13,7 +13,7 @@
"Anycubic" "Anycubic"
], ],
"filament_type": [ "filament_type": [
"TPU for ACE" "TPU"
], ],
"compatible_printers": [ "compatible_printers": [
"Anycubic Kobra S1 Max 0.4 nozzle" "Anycubic Kobra S1 Max 0.4 nozzle"

View File

@@ -13,7 +13,7 @@
"Anycubic" "Anycubic"
], ],
"filament_type": [ "filament_type": [
"PEBA" "TPU"
], ],
"compatible_printers": [ "compatible_printers": [
"Anycubic Kobra S1 Max 0.6 nozzle" "Anycubic Kobra S1 Max 0.6 nozzle"

View File

@@ -87,7 +87,7 @@
"0" "0"
], ],
"filament_type": [ "filament_type": [
"PA-CF" "PA6-CF"
], ],
"filament_vendor": [ "filament_vendor": [
"Generic" "Generic"

View File

@@ -87,7 +87,7 @@
"0" "0"
], ],
"filament_type": [ "filament_type": [
"PA-CF" "PA6-CF"
], ],
"filament_vendor": [ "filament_vendor": [
"Generic" "Generic"

View File

@@ -87,7 +87,7 @@
"0" "0"
], ],
"filament_type": [ "filament_type": [
"PA-CF" "PA6-CF"
], ],
"filament_vendor": [ "filament_vendor": [
"Generic" "Generic"

View File

@@ -87,7 +87,7 @@
"0" "0"
], ],
"filament_type": [ "filament_type": [
"PA-CF" "PA6-CF"
], ],
"filament_vendor": [ "filament_vendor": [
"Generic" "Generic"

View File

@@ -84,7 +84,7 @@
"0" "0"
], ],
"filament_type": [ "filament_type": [
"PA-CF" "PA6-CF"
], ],
"filament_vendor": [ "filament_vendor": [
"Generic" "Generic"

View File

@@ -84,7 +84,7 @@
"0" "0"
], ],
"filament_type": [ "filament_type": [
"PA-CF" "PA6-CF"
], ],
"filament_vendor": [ "filament_vendor": [
"Generic" "Generic"

View File

@@ -39,6 +39,9 @@
"filament_start_gcode": [ "filament_start_gcode": [
"; filament start gcode\n;right_extruder_material: ABS-CF\n" "; filament start gcode\n;right_extruder_material: ABS-CF\n"
], ],
"filament_type": [
"ABS-CF"
],
"compatible_printers": [ "compatible_printers": [
"Flashforge Guider4 0.4 nozzle" "Flashforge Guider4 0.4 nozzle"
], ],

View File

@@ -39,6 +39,9 @@
"filament_start_gcode": [ "filament_start_gcode": [
"; filament start gcode\n;right_extruder_material: ABS-CF\n" "; filament start gcode\n;right_extruder_material: ABS-CF\n"
], ],
"filament_type": [
"ABS-CF"
],
"compatible_printers": [ "compatible_printers": [
"Flashforge Guider4 Pro 0.4 nozzle" "Flashforge Guider4 Pro 0.4 nozzle"
], ],

View File

@@ -27,6 +27,9 @@
"filament_settings_id": [ "filament_settings_id": [
"Flashforge ASA Basic @FF AD5X 0.6 nozzle" "Flashforge ASA Basic @FF AD5X 0.6 nozzle"
], ],
"filament_type": [
"ASA"
],
"compatible_printers": [ "compatible_printers": [
"Flashforge AD5X 0.6 nozzle" "Flashforge AD5X 0.6 nozzle"
], ],

View File

@@ -27,6 +27,9 @@
"filament_settings_id": [ "filament_settings_id": [
"Flashforge ASA Basic @FF AD5X 0.8 nozzle" "Flashforge ASA Basic @FF AD5X 0.8 nozzle"
], ],
"filament_type": [
"ASA"
],
"compatible_printers": [ "compatible_printers": [
"Flashforge AD5X 0.8 nozzle" "Flashforge AD5X 0.8 nozzle"
], ],

View File

@@ -27,6 +27,9 @@
"filament_settings_id": [ "filament_settings_id": [
"Flashforge ASA Basic @FF AD5X" "Flashforge ASA Basic @FF AD5X"
], ],
"filament_type": [
"ASA"
],
"compatible_printers": [ "compatible_printers": [
"Flashforge AD5X 0.4 nozzle" "Flashforge AD5X 0.4 nozzle"
], ],

View File

@@ -37,7 +37,7 @@
"; filament start gcode\n;right_extruder_material: ASA-CF\n" "; filament start gcode\n;right_extruder_material: ASA-CF\n"
], ],
"filament_type": [ "filament_type": [
"ASA" "ASA-CF"
], ],
"compatible_printers": [ "compatible_printers": [
"Flashforge Guider4 0.4 nozzle" "Flashforge Guider4 0.4 nozzle"

View File

@@ -37,7 +37,7 @@
"; filament start gcode\n;right_extruder_material: ASA-CF\n" "; filament start gcode\n;right_extruder_material: ASA-CF\n"
], ],
"filament_type": [ "filament_type": [
"ASA" "ASA-CF"
], ],
"compatible_printers": [ "compatible_printers": [
"Flashforge Guider4 Pro 0.4 nozzle" "Flashforge Guider4 Pro 0.4 nozzle"

View File

@@ -16,7 +16,7 @@
"Flashforge ASA-CF" "Flashforge ASA-CF"
], ],
"filament_type": [ "filament_type": [
"ASA" "ASA-CF"
], ],
"hot_plate_temp": [ "hot_plate_temp": [
"100" "100"

View File

@@ -63,6 +63,9 @@
"filament_start_gcode": [ "filament_start_gcode": [
"; filament start gcode\n;right_extruder_material:HIPS" "; filament start gcode\n;right_extruder_material:HIPS"
], ],
"filament_type": [
"HIPS"
],
"compatible_printers": [ "compatible_printers": [
"Flashforge Guider4 0.6 HF nozzle" "Flashforge Guider4 0.6 HF nozzle"
], ],

View File

@@ -63,6 +63,9 @@
"filament_start_gcode": [ "filament_start_gcode": [
"; filament start gcode\n;right_extruder_material:HIPS" "; filament start gcode\n;right_extruder_material:HIPS"
], ],
"filament_type": [
"HIPS"
],
"compatible_printers": [ "compatible_printers": [
"Flashforge Guider4 0.6 nozzle" "Flashforge Guider4 0.6 nozzle"
], ],

View File

@@ -63,6 +63,9 @@
"filament_start_gcode": [ "filament_start_gcode": [
"; filament start gcode\n;right_extruder_material:HIPS" "; filament start gcode\n;right_extruder_material:HIPS"
], ],
"filament_type": [
"HIPS"
],
"compatible_printers": [ "compatible_printers": [
"Flashforge Guider4 0.8 HF nozzle" "Flashforge Guider4 0.8 HF nozzle"
], ],

View File

@@ -63,6 +63,9 @@
"filament_start_gcode": [ "filament_start_gcode": [
"; filament start gcode\n;right_extruder_material:HIPS" "; filament start gcode\n;right_extruder_material:HIPS"
], ],
"filament_type": [
"HIPS"
],
"compatible_printers": [ "compatible_printers": [
"Flashforge Guider4 Pro 0.6 HF nozzle" "Flashforge Guider4 Pro 0.6 HF nozzle"
], ],

View File

@@ -63,6 +63,9 @@
"filament_start_gcode": [ "filament_start_gcode": [
"; filament start gcode\n;right_extruder_material:HIPS" "; filament start gcode\n;right_extruder_material:HIPS"
], ],
"filament_type": [
"HIPS"
],
"compatible_printers": [ "compatible_printers": [
"Flashforge Guider4 Pro 0.6 nozzle" "Flashforge Guider4 Pro 0.6 nozzle"
], ],

View File

@@ -63,6 +63,9 @@
"filament_start_gcode": [ "filament_start_gcode": [
"; filament start gcode\n;right_extruder_material:HIPS" "; filament start gcode\n;right_extruder_material:HIPS"
], ],
"filament_type": [
"HIPS"
],
"compatible_printers": [ "compatible_printers": [
"Flashforge Guider4 Pro 0.8 HF nozzle" "Flashforge Guider4 Pro 0.8 HF nozzle"
], ],

View File

@@ -61,7 +61,7 @@
"; filament start gcode\n;right_extruder_material:PA-CF\n" "; filament start gcode\n;right_extruder_material:PA-CF\n"
], ],
"filament_type": [ "filament_type": [
"PA-CF" "PAHT-CF"
], ],
"compatible_printers": [ "compatible_printers": [
"Flashforge Guider4 0.4 nozzle" "Flashforge Guider4 0.4 nozzle"

View File

@@ -61,7 +61,7 @@
"; filament start gcode\n;right_extruder_material:PA-CF\n" "; filament start gcode\n;right_extruder_material:PA-CF\n"
], ],
"filament_type": [ "filament_type": [
"PA-CF" "PAHT-CF"
], ],
"compatible_printers": [ "compatible_printers": [
"Flashforge Guider4 Pro 0.4 nozzle" "Flashforge Guider4 Pro 0.4 nozzle"

View File

@@ -31,7 +31,7 @@
"Flashforge PLA Silk @FF AD5X 0.25 nozzle" "Flashforge PLA Silk @FF AD5X 0.25 nozzle"
], ],
"filament_type": [ "filament_type": [
"SILK" "PLA"
], ],
"compatible_printers": [ "compatible_printers": [
"Flashforge AD5X 0.25 nozzle" "Flashforge AD5X 0.25 nozzle"

View File

@@ -22,7 +22,7 @@
"Flashforge PLA Silk @FF AD5X 0.6 nozzle" "Flashforge PLA Silk @FF AD5X 0.6 nozzle"
], ],
"filament_type": [ "filament_type": [
"SILK" "PLA"
], ],
"compatible_printers": [ "compatible_printers": [
"Flashforge AD5X 0.6 nozzle" "Flashforge AD5X 0.6 nozzle"

View File

@@ -22,7 +22,7 @@
"Flashforge PLA Silk @FF AD5X 0.8 nozzle" "Flashforge PLA Silk @FF AD5X 0.8 nozzle"
], ],
"filament_type": [ "filament_type": [
"SILK" "PLA"
], ],
"compatible_printers": [ "compatible_printers": [
"Flashforge AD5X 0.8 nozzle" "Flashforge AD5X 0.8 nozzle"

View File

@@ -22,7 +22,7 @@
"Flashforge PLA Silk @FF AD5X" "Flashforge PLA Silk @FF AD5X"
], ],
"filament_type": [ "filament_type": [
"SILK" "PLA"
], ],
"hot_plate_temp": [ "hot_plate_temp": [
"60" "60"

View File

@@ -54,7 +54,7 @@
"; filament start gcode" "; filament start gcode"
], ],
"filament_type": [ "filament_type": [
"SILK" "PLA"
], ],
"hot_plate_temp": [ "hot_plate_temp": [
"65" "65"

View File

@@ -57,7 +57,7 @@
"; filament start gcode" "; filament start gcode"
], ],
"filament_type": [ "filament_type": [
"SILK" "PLA"
], ],
"nozzle_temperature": [ "nozzle_temperature": [
"230" "230"

View File

@@ -36,6 +36,9 @@
"filament_start_gcode": [ "filament_start_gcode": [
"; filament start gcode\n;right_extruder_material:PA" "; filament start gcode\n;right_extruder_material:PA"
], ],
"filament_type": [
"PAHT"
],
"compatible_printers": [ "compatible_printers": [
"Flashforge Guider4 0.6 HF nozzle" "Flashforge Guider4 0.6 HF nozzle"
], ],

View File

@@ -36,6 +36,9 @@
"filament_start_gcode": [ "filament_start_gcode": [
"; filament start gcode\n;right_extruder_material:PA" "; filament start gcode\n;right_extruder_material:PA"
], ],
"filament_type": [
"PAHT"
],
"compatible_printers": [ "compatible_printers": [
"Flashforge Guider4 0.8 HF nozzle" "Flashforge Guider4 0.8 HF nozzle"
], ],

View File

@@ -36,6 +36,9 @@
"filament_start_gcode": [ "filament_start_gcode": [
"; filament start gcode\n;right_extruder_material:PA" "; filament start gcode\n;right_extruder_material:PA"
], ],
"filament_type": [
"PAHT"
],
"compatible_printers": [ "compatible_printers": [
"Flashforge Guider4 Pro 0.6 HF nozzle" "Flashforge Guider4 Pro 0.6 HF nozzle"
], ],

View File

@@ -36,6 +36,9 @@
"filament_start_gcode": [ "filament_start_gcode": [
"; filament start gcode\n;right_extruder_material:PA" "; filament start gcode\n;right_extruder_material:PA"
], ],
"filament_type": [
"PAHT"
],
"compatible_printers": [ "compatible_printers": [
"Flashforge Guider4 Pro 0.8 HF nozzle" "Flashforge Guider4 Pro 0.8 HF nozzle"
], ],

View File

@@ -55,7 +55,7 @@
"; filament start gcode\n;right_extruder_material:PA-CF\n" "; filament start gcode\n;right_extruder_material:PA-CF\n"
], ],
"filament_type": [ "filament_type": [
"PA-CF" "PAHT-CF"
], ],
"compatible_printers": [ "compatible_printers": [
"Flashforge Guider4 0.4 nozzle" "Flashforge Guider4 0.4 nozzle"

View File

@@ -55,7 +55,7 @@
"; filament start gcode\n;right_extruder_material:PA-CF\n" "; filament start gcode\n;right_extruder_material:PA-CF\n"
], ],
"filament_type": [ "filament_type": [
"PA-CF" "PAHT-CF"
], ],
"compatible_printers": [ "compatible_printers": [
"Flashforge Guider4 Pro 0.4 nozzle" "Flashforge Guider4 Pro 0.4 nozzle"

View File

@@ -168,7 +168,7 @@
"0" "0"
], ],
"filament_type": [ "filament_type": [
"PA-CF" "PAHT-CF"
], ],
"filament_unload_time": [ "filament_unload_time": [
"0" "0"

View File

@@ -53,7 +53,7 @@
"; filament start gcode" "; filament start gcode"
], ],
"filament_type": [ "filament_type": [
"SILK" "PLA"
], ],
"hot_plate_temp": [ "hot_plate_temp": [
"65" "65"

View File

@@ -56,7 +56,7 @@
"; filament start gcode" "; filament start gcode"
], ],
"filament_type": [ "filament_type": [
"SILK" "PLA"
], ],
"nozzle_temperature": [ "nozzle_temperature": [
"230" "230"

View File

@@ -44,7 +44,7 @@
"12" "12"
], ],
"filament_type": [ "filament_type": [
"PETG Basic" "PETG"
], ],
"filament_density": [ "filament_density": [
"1.27" "1.27"

View File

@@ -44,7 +44,7 @@
"12" "12"
], ],
"filament_type": [ "filament_type": [
"PETG Basic" "PETG"
], ],
"filament_density": [ "filament_density": [
"1.27" "1.27"

View File

@@ -4,5 +4,8 @@
"inherits": "fdm_filament_pe", "inherits": "fdm_filament_pe",
"from": "system", "from": "system",
"filament_id": "OFJfx0Cl", "filament_id": "OFJfx0Cl",
"instantiation": "false" "instantiation": "false",
"filament_type": [
"PE-CF"
]
} }

View File

@@ -4,5 +4,8 @@
"inherits": "fdm_filament_pet", "inherits": "fdm_filament_pet",
"from": "system", "from": "system",
"filament_id": "OFDCk0cz", "filament_id": "OFDCk0cz",
"instantiation": "false" "instantiation": "false",
"filament_type": [
"PETG-CF"
]
} }

View File

@@ -4,5 +4,8 @@
"inherits": "fdm_filament_pp", "inherits": "fdm_filament_pp",
"from": "system", "from": "system",
"filament_id": "OFRS2Ggt", "filament_id": "OFRS2Ggt",
"instantiation": "false" "instantiation": "false",
"filament_type": [
"PP-CF"
]
} }

View File

@@ -4,5 +4,8 @@
"inherits": "fdm_filament_pa", "inherits": "fdm_filament_pa",
"from": "system", "from": "system",
"filament_id": "OFXfgh61", "filament_id": "OFXfgh61",
"instantiation": "false" "instantiation": "false",
"filament_type": [
"PA-CF"
]
} }

View File

@@ -4,5 +4,8 @@
"inherits": "fdm_filament_pla", "inherits": "fdm_filament_pla",
"from": "system", "from": "system",
"filament_id": "OFv8SRZh", "filament_id": "OFv8SRZh",
"instantiation": "false" "instantiation": "false",
"filament_type": [
"PLA-CF"
]
} }

View File

@@ -46,6 +46,9 @@
"filament_type": [ "filament_type": [
"PLA" "PLA"
], ],
"filament_vendor": [
"Snapmaker"
],
"filament_density": [ "filament_density": [
"1.24" "1.24"
], ],

View File

@@ -46,6 +46,9 @@
"filament_type": [ "filament_type": [
"PLA" "PLA"
], ],
"filament_vendor": [
"Snapmaker"
],
"filament_density": [ "filament_density": [
"1.24" "1.24"
], ],

View File

@@ -46,6 +46,9 @@
"filament_type": [ "filament_type": [
"PLA" "PLA"
], ],
"filament_vendor": [
"Snapmaker"
],
"filament_density": [ "filament_density": [
"1.24" "1.24"
], ],

View File

@@ -0,0 +1,90 @@
{
"hints": {},
"retired": {
"GFABS": {
"claims": [
"Anycubic/Anycubic ABS"
],
"successor": "OFf2b9wu"
},
"GFL92": {
"claims": [
"Anycubic/Anycubic PLA"
],
"successor": "OFgK2Xvv"
},
"GFL93": {
"claims": [
"Anycubic/Anycubic PLA+"
],
"successor": "OFgrGfPy"
},
"GFPLA": {
"claims": [
"Anycubic/Anycubic PLA"
],
"successor": "OFgK2Xvv"
},
"GFPLA High Speed": {
"claims": [
"Anycubic/Anycubic PLA High Speed"
],
"successor": "OFgSK1hL"
},
"GFPLA Matte": {
"claims": [
"Anycubic/Anycubic PLA Matte"
],
"successor": "OFvYfHPB"
},
"GFPLA Silk": {
"claims": [
"Anycubic/Anycubic PLA Silk"
],
"successor": "OFjdTQd6"
},
"GFPLA+": {
"claims": [
"Anycubic/Anycubic PLA+"
],
"successor": "OFgrGfPy"
},
"P11851ba": {
"claims": [
"Artillery/Artillery PET"
],
"successor": "OFw0qp7w"
},
"P284941e": {
"claims": [
"Artillery/Artillery PETG",
"Artillery/Artillery PETG Basic"
],
"successor": "OFEYKrjb"
},
"P2fbf0c0": {
"claims": [
"Artillery/Artillery TPU"
],
"successor": "OFTbsWVg"
},
"P39db358": {
"claims": [
"Artillery/Artillery ABS"
],
"successor": "OFw1S8lL"
},
"P8163162": {
"claims": [
"Artillery/Artillery PVA"
],
"successor": "OFc2SnEp"
},
"P87436f6": {
"claims": [
"Artillery/Artillery ASA"
],
"successor": "OFtmT4Nd"
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,47 +0,0 @@
{
"retired": {
"GFABS": [
"Anycubic/Anycubic ABS"
],
"GFL92": [
"Anycubic/Anycubic PLA"
],
"GFL93": [
"Anycubic/Anycubic PLA+"
],
"GFPLA": [
"Anycubic/Anycubic PLA"
],
"GFPLA High Speed": [
"Anycubic/Anycubic PLA High Speed"
],
"GFPLA Matte": [
"Anycubic/Anycubic PLA Matte"
],
"GFPLA Silk": [
"Anycubic/Anycubic PLA Silk"
],
"GFPLA+": [
"Anycubic/Anycubic PLA+"
],
"P11851ba": [
"Artillery/Artillery PET"
],
"P284941e": [
"Artillery/Artillery PETG",
"Artillery/Artillery PETG Basic"
],
"P2fbf0c0": [
"Artillery/Artillery TPU"
],
"P39db358": [
"Artillery/Artillery ABS"
],
"P8163162": [
"Artillery/Artillery PVA"
],
"P87436f6": [
"Artillery/Artillery ASA"
]
}
}

View File

@@ -5,10 +5,15 @@ Inserts/deletes/modifies random lane data in Moonraker database,
then reads back and displays with colored output. then reads back and displays with colored output.
""" """
import requests try:
import requests
except ImportError:
requests = None # only needed for live-printer operations, not --check-ofl-map
import random import random
import argparse import argparse
import json import json
import os
import re
import time import time
import sys import sys
@@ -16,6 +21,11 @@ import sys
DEFAULT_HOST = "192.168.88.9" DEFAULT_HOST = "192.168.88.9"
DEFAULT_PORT = 7125 DEFAULT_PORT = 7125
NAMESPACE = "lane_data" NAMESPACE = "lane_data"
# Repo-relative paths for the offline generic-map check
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MOONRAKER_AGENT_CPP = os.path.join(REPO_ROOT, "src", "slic3r", "Utils", "MoonrakerPrinterAgent.cpp")
OFL_FILAMENT_DIR = os.path.join(REPO_ROOT, "resources", "profiles", "OrcaFilamentLibrary", "filament")
LANE_KEYS = [f"lane{i}" for i in range(1, 9)] # lane1-lane8 LANE_KEYS = [f"lane{i}" for i in range(1, 9)] # lane1-lane8
MATERIALS = ["PLA", "ABS", "PETG", "ASA", "ASA Sparkle", "TPU", ""] MATERIALS = ["PLA", "ABS", "PETG", "ASA", "ASA Sparkle", "TPU", ""]
@@ -30,6 +40,95 @@ MATERIAL_TEMPS = {
"": {"nozzle": None, "bed": None}, "": {"nozzle": None, "bed": None},
} }
def parse_cpp_type_map():
"""Extract the normalized-type -> OFL generic family table from MoonrakerPrinterAgent.cpp.
Reads MoonrakerPrinterAgent::map_filament_type_to_generic_id's type_to_ofl_family
initializer so the check tracks the C++ normalization without a duplicated list.
"""
with open(MOONRAKER_AGENT_CPP, encoding="utf-8") as f:
src = f.read()
m = re.search(r"type_to_ofl_family\s*=\s*\{(.*?)\n\s*\};", src, re.DOTALL)
if not m:
raise RuntimeError(f"type_to_ofl_family table not found in {MOONRAKER_AGENT_CPP}")
pairs = re.findall(r'\{\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\}', m.group(1))
if not pairs:
raise RuntimeError("type_to_ofl_family table parsed empty")
return dict(pairs)
def load_ofl_presets():
"""Map preset name -> parsed JSON for every OrcaFilamentLibrary filament profile."""
presets = {}
for root, _dirs, files in os.walk(OFL_FILAMENT_DIR):
for fn in files:
if not fn.endswith(".json"):
continue
try:
with open(os.path.join(root, fn), encoding="utf-8") as f:
data = json.load(f)
except (OSError, ValueError):
continue
name = data.get("name")
if isinstance(name, str) and name:
presets[name] = data
return presets
def resolve_ofl_filament_id(presets, name):
"""Follow the inherits chain (within OFL) until a filament_id is declared."""
seen = set()
while name and name not in seen:
seen.add(name)
preset = presets.get(name)
if preset is None:
return None
fid = preset.get("filament_id")
if fid:
return fid
name = preset.get("inherits")
return None
def check_ofl_generic_map():
"""Assert every material type the C++ normalization handles resolves to a shipped
OrcaFilamentLibrary generic preset carrying a filament_id.
Expectations are derived from the shipped profiles, not pinned id literals, so the
check stays valid across filament_id re-mints.
"""
print("Checking C++ generic-type map against shipped OrcaFilamentLibrary presets...")
try:
type_map = parse_cpp_type_map()
except (OSError, RuntimeError) as e:
print(f" FAIL: {e}")
return False
presets = load_ofl_presets()
if not presets:
print(f" FAIL: no OFL filament profiles found under {OFL_FILAMENT_DIR}")
return False
errors = []
for family in sorted(set(type_map.values())):
preset_name = f"Generic {family} @System"
if preset_name not in presets:
errors.append(f"{preset_name}: no such OFL preset")
continue
if str(presets[preset_name].get("instantiation", "")).lower() != "true":
errors.append(f"{preset_name}: not instantiated — the C++ runtime lookup "
f"only sees presets loaded into the PresetBundle")
continue
fid = resolve_ofl_filament_id(presets, preset_name)
if not fid:
errors.append(f"{preset_name}: no filament_id resolvable through inherits")
continue
aliases = ", ".join(sorted(t for t, fam in type_map.items() if fam == family))
print(f" {fid:10s} {preset_name:32s} <- {aliases}")
if errors:
for e in errors:
print(f" FAIL: {e}")
print(f"OFL generic map check FAILED ({len(errors)} error(s))")
return False
print(f"OFL generic map check passed: {len(type_map)} type aliases, "
f"{len(set(type_map.values()))} OFL generic presets\n")
return True
def test_connection(host, port, api_key=None, verbose=False): def test_connection(host, port, api_key=None, verbose=False):
"""Test basic connectivity to Moonraker.""" """Test basic connectivity to Moonraker."""
url = f"http://{host}:{port}/server/info" url = f"http://{host}:{port}/server/info"
@@ -404,11 +503,25 @@ def main():
help="Only read and display current lane data") help="Only read and display current lane data")
parser.add_argument("--load", metavar="FILE", parser.add_argument("--load", metavar="FILE",
help="Load lane data from JSON file and overwrite printer lanes") help="Load lane data from JSON file and overwrite printer lanes")
parser.add_argument("--check-ofl-map", action="store_true",
help="Only run the offline check that the C++ generic-type map "
"resolves against shipped OrcaFilamentLibrary presets")
parser.add_argument("--verbose", "-v", action="store_true", parser.add_argument("--verbose", "-v", action="store_true",
help="Verbose output for debugging") help="Verbose output for debugging")
args = parser.parse_args() args = parser.parse_args()
# Offline check first: the C++ type normalization must resolve against shipped
# OFL presets (no printer needed).
if not check_ofl_generic_map():
return 1
if args.check_ofl_map:
return 0
if requests is None:
print("The 'requests' module is required for live printer operations (pip install requests).")
return 1
print(f"\nConnecting to Moonraker at {args.host}:{args.port}...") print(f"\nConnecting to Moonraker at {args.host}:{args.port}...")
# First test basic connectivity # First test basic connectivity

File diff suppressed because it is too large Load Diff

View File

@@ -555,6 +555,60 @@ std::string generate_preset_setting_id(const std::string& vendor, const std::str
return std::string(out, 16); return std::string(out, 16);
} }
std::string follow_filament_id_succession(const std::string& id, const std::map<std::string, std::string>& forwards)
{
auto it = forwards.find(id);
if (it == forwards.end())
return std::string();
std::set<std::string> visited { id };
std::string current = id;
while (it != forwards.end() && visited.insert(it->second).second) {
current = it->second;
it = forwards.find(current);
}
// it == end(): current is the live end of the chain; otherwise the ledger is
// cyclic and current is the last id reached before a repeat.
return current;
}
const std::map<std::string, std::string>& filament_id_succession_map()
{
// Loaded once; C++11 magic-static initialization makes this thread-safe.
// Succession is a best-effort miss-path fallback, so a missing or corrupt
// ledger is not an error for the client - it just yields an empty map.
static const std::map<std::string, std::string> forwards = []() {
std::map<std::string, std::string> map;
const std::string path = Slic3r::resources_dir() + "/profiles/retired_filament_ids.json";
boost::nowide::ifstream file(path.c_str());
if (!file.is_open()) {
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": cannot open " << path;
return map;
}
try {
nlohmann::json ledger;
file >> ledger;
if (ledger.contains("retired") && ledger["retired"].is_object())
for (auto& [old_id, entry] : ledger["retired"].items())
if (entry.is_object() && entry.contains("successor") && entry["successor"].is_string())
map[old_id] = entry["successor"].get<std::string>();
if (ledger.contains("hints") && ledger["hints"].is_object())
for (auto& [old_id, target] : ledger["hints"].items())
if (target.is_string())
map[old_id] = target.get<std::string>();
} catch (...) {
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": failed to parse " << path;
map.clear();
}
return map;
}();
return forwards;
}
std::string resolve_filament_id_succession(const std::string& id)
{
return follow_filament_id_succession(id, filament_id_succession_map());
}
std::string Preset::get_iot_type_string(Preset::Type type) std::string Preset::get_iot_type_string(Preset::Type type)
{ {
switch (type) { switch (type) {

View File

@@ -100,6 +100,23 @@ std::string generate_preset_setting_id(const std::string& vendor,
const std::string& type, const std::string& type,
const std::string& name); const std::string& name);
// Succession forwarding for retired/renamed filament_ids, backed by the shipped
// ledger resources/profiles/retired_filament_ids.json. Consulted on a filament_id
// resolution miss only, so live ids always resolve natively first.
//
// Pure chain walk over `forwards` (old id -> successor). Returns the final id of the
// chain, or an empty string when `id` has no entry. Cycle-guarded: on a malformed
// cyclic ledger it stops at the last id reached before any repeat (a self-loop
// returns `id` itself).
std::string follow_filament_id_succession(const std::string& id,
const std::map<std::string, std::string>& forwards);
// Forwarding map loaded once (thread-safe) from the ledger, merging
// "retired" {id -> non-null successor} and "hints" {island-owned id -> live id}.
// A missing or corrupt file yields an empty map.
const std::map<std::string, std::string>& filament_id_succession_map();
// Convenience: follow_filament_id_succession(id, filament_id_succession_map()).
std::string resolve_filament_id_succession(const std::string& id);
enum ConfigFileType enum ConfigFileType
{ {
CONFIG_FILE_TYPE_UNKNOWN, CONFIG_FILE_TYPE_UNKNOWN,

View File

@@ -729,6 +729,12 @@ std::optional<FilamentBaseInfo> PresetBundle::get_filament_by_filament_id(const
} }
} }
} }
// Miss: a retired/renamed id may forward to a live successor through the shipped
// succession ledger. resolve() returns a chain-final id, so the retry below cannot
// recurse further (a malformed cyclic ledger yields a non-final id, rejected here).
const std::string successor = resolve_filament_id_succession(filament_id);
if (!successor.empty() && resolve_filament_id_succession(successor).empty())
return get_filament_by_filament_id(successor, printer_name);
return std::nullopt; return std::nullopt;
} }
@@ -3150,6 +3156,15 @@ void PresetBundle::get_ams_cobox_infos(AMSComboInfo& combox_info)
auto iter = std::find_if(filaments.begin(), filaments.end(), auto iter = std::find_if(filaments.begin(), filaments.end(),
[this, &filament_id](auto &f) { return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; }); [this, &filament_id](auto &f) { return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; });
warn_ambiguous_filament_id_match(filaments, iter, filament_id); warn_ambiguous_filament_id_match(filaments, iter, filament_id);
if (iter == filaments.end()) {
// Retired/renamed ids forward to a live successor through the shipped succession ledger.
const std::string successor = resolve_filament_id_succession(filament_id);
if (!successor.empty()) {
iter = std::find_if(filaments.begin(), filaments.end(),
[this, &successor](auto &f) { return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == successor; });
warn_ambiguous_filament_id_match(filaments, iter, successor);
}
}
if (iter == filaments.end()) { if (iter == filaments.end()) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id; BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id;
auto filament_type = ams.opt_string("filament_type", 0u); auto filament_type = ams.opt_string("filament_type", 0u);
@@ -3253,6 +3268,16 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
has_type |= f.config.opt_string("filament_type", 0u) == filament_type; has_type |= f.config.opt_string("filament_type", 0u) == filament_type;
return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; }); return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == filament_id; });
warn_ambiguous_filament_id_match(filaments, iter, filament_id); warn_ambiguous_filament_id_match(filaments, iter, filament_id);
if (iter == filaments.end()) {
// Retired/renamed ids forward to a live successor through the shipped succession
// ledger (has_type is already complete: the miss above scanned every preset).
const std::string successor = resolve_filament_id_succession(filament_id);
if (!successor.empty()) {
iter = std::find_if(filaments.begin(), filaments.end(), [this, &successor](auto &f) {
return f.is_compatible && filaments.get_preset_base(f) == &f && f.filament_id == successor; });
warn_ambiguous_filament_id_match(filaments, iter, successor);
}
}
if (iter == filaments.end()) { if (iter == filaments.end()) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": filament_id %1% not found or system or compatible") % filament_id;
if (!filament_type.empty()) { if (!filament_type.empty()) {
@@ -3714,7 +3739,11 @@ bool PresetBundle::check_filament_temp_equation_by_printer_type_and_nozzle_for_m
std::map<std::string, std::vector<Preset const *>> filament_list = filaments.get_filament_presets(); std::map<std::string, std::vector<Preset const *>> filament_list = filaments.get_filament_presets();
std::set<std::string> printer_names = get_printer_names_by_printer_type_and_nozzle(printer_type, nozzle_diameter_str); std::set<std::string> printer_names = get_printer_names_by_printer_type_and_nozzle(printer_type, nozzle_diameter_str);
for (const Preset *preset : filament_list.find(setting_id)->second) { auto filament_iter = filament_list.find(setting_id);
if (filament_iter == filament_list.end())
return is_equation;
for (const Preset *preset : filament_iter->second) {
if (tag_uid == "0" || (tag_uid.size() == 16 && tag_uid.substr(12, 2) == "01")) continue; if (tag_uid == "0" || (tag_uid.size() == 16 && tag_uid.substr(12, 2) == "01")) continue;
if (preset && !preset->is_user()) continue; if (preset && !preset->is_user()) continue;
ConfigOption * printer_opt = const_cast<Preset *>(preset)->config.option("compatible_printers"); ConfigOption * printer_opt = const_cast<Preset *>(preset)->config.option("compatible_printers");

View File

@@ -59,7 +59,7 @@ int get_colume_idx(CaliColumnType type, MachineObject* obj)
return type; return type;
} }
static wxString get_preset_name_by_filament_id(std::string filament_id) static wxString get_preset_name_by_filament_id(std::string filament_id, bool follow_succession = true)
{ {
auto preset_bundle = wxGetApp().preset_bundle; auto preset_bundle = wxGetApp().preset_bundle;
auto collection = &preset_bundle->filaments; auto collection = &preset_bundle->filaments;
@@ -93,6 +93,12 @@ static wxString get_preset_name_by_filament_id(std::string filament_id)
} }
} }
} }
if (preset_name.empty() && follow_succession) {
// Retired/renamed ids forward to a live successor through the shipped succession ledger.
const std::string successor = resolve_filament_id_succession(filament_id);
if (!successor.empty())
preset_name = get_preset_name_by_filament_id(successor, false);
}
return preset_name; return preset_name;
} }

View File

@@ -2540,14 +2540,22 @@ std::string MachineObject::setting_id_to_type(std::string setting_id, std::strin
std::string type; std::string type;
PresetBundle* preset_bundle = GUI::wxGetApp().preset_bundle; PresetBundle* preset_bundle = GUI::wxGetApp().preset_bundle;
if (preset_bundle) { if (preset_bundle) {
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) { auto lookup_system_type = [preset_bundle](const std::string &id) {
std::string display_filament_type;
if (it->filament_id.compare(setting_id) == 0 && it->is_system) { for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
std::string display_filament_type; if (it->filament_id.compare(id) == 0 && it->is_system) {
it->config.get_filament_type(display_filament_type); it->config.get_filament_type(display_filament_type);
type = display_filament_type; break;
break; }
} }
return display_filament_type;
};
type = lookup_system_type(setting_id);
if (type.empty()) {
// Retired/renamed ids forward to a live successor through the shipped succession ledger.
const std::string successor = resolve_filament_id_succession(setting_id);
if (!successor.empty())
type = lookup_system_type(successor);
} }
} }
@@ -5249,7 +5257,10 @@ void MachineObject::update_filament_list()
for (auto it = filament_list.begin(); it != filament_list.end(); it++) { for (auto it = filament_list.begin(); it != filament_list.end(); it++) {
if (m_filament_list.find(it->first) != m_filament_list.end()) { if (m_filament_list.find(it->first) != m_filament_list.end()) {
assert(it->first.size() == 8 && it->first[0] == 'P'); // User roots may legitimately carry adopted system-shaped ids (GF*/OF*/P-hex
// system), so a non-'P' id here is expected, not an invariant violation.
if (it->first.size() != 8 || it->first[0] != 'P')
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << ": user-root filament_id is not user-shaped: " << it->first;
if (it->second.first != m_filament_list[it->first].first) { if (it->second.first != m_filament_list[it->first].first) {
BOOST_LOG_TRIVIAL(info) << "old min temp is not equal to new min temp and filament id: " << it->first; BOOST_LOG_TRIVIAL(info) << "old min temp is not equal to new min temp and filament id: " << it->first;
@@ -5311,6 +5322,17 @@ void MachineObject::update_printer_preset_name()
void MachineObject::check_ams_filament_valid() void MachineObject::check_ams_filament_valid()
{ {
PresetBundle * preset_bundle = Slic3r::GUI::wxGetApp().preset_bundle; PresetBundle * preset_bundle = Slic3r::GUI::wxGetApp().preset_bundle;
// A tray id carried by ANY system filament preset is not a dangling user-preset id
// (ten shipped P-hex system ids pass the 'P' shape gates below), so the destructive
// tray-wipe / temp-rewrite handling must never fire for it.
auto is_system_filament_id = [preset_bundle](const std::string &id) {
if (!preset_bundle)
return false;
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++)
if (it->is_system && it->filament_id == id)
return true;
return false;
};
auto printer_model = DevPrinterConfigUtil::get_printer_display_name(this->printer_type); auto printer_model = DevPrinterConfigUtil::get_printer_display_name(this->printer_type);
std::map<std::string, std::set<std::string>> need_checked_filament_id; std::map<std::string, std::set<std::string>> need_checked_filament_id;
for (auto &ams_pair : m_fila_system->GetAmsList()) { for (auto &ams_pair : m_fila_system->GetAmsList()) {
@@ -5332,6 +5354,8 @@ void MachineObject::check_ams_filament_valid()
auto &checked_filament = data.checked_filament; auto &checked_filament = data.checked_filament;
for (const auto &[slot_id, curr_tray] : ams->GetTrays()) { for (const auto &[slot_id, curr_tray] : ams->GetTrays()) {
if (curr_tray->setting_id.size() == 8 && curr_tray->setting_id[0] == 'P' && is_system_filament_id(curr_tray->setting_id))
continue;
if (curr_tray->setting_id.size() == 8 && curr_tray->setting_id[0] == 'P' && filament_list.find(curr_tray->setting_id) == filament_list.end()) { if (curr_tray->setting_id.size() == 8 && curr_tray->setting_id[0] == 'P' && filament_list.find(curr_tray->setting_id) == filament_list.end()) {
if (checked_filament.find(curr_tray->setting_id) != checked_filament.end()) { if (checked_filament.find(curr_tray->setting_id) != checked_filament.end()) {
need_checked_filament_id[nozzle_diameter_str].insert(curr_tray->setting_id); need_checked_filament_id[nozzle_diameter_str].insert(curr_tray->setting_id);
@@ -5392,6 +5416,8 @@ void MachineObject::check_ams_filament_valid()
auto &data = m_nozzle_filament_data[nozzle_diameter_str]; auto &data = m_nozzle_filament_data[nozzle_diameter_str];
auto &checked_filament = data.checked_filament; auto &checked_filament = data.checked_filament;
auto &filament_list = data.filament_list; auto &filament_list = data.filament_list;
if (vt_tray.setting_id.size() == 8 && vt_tray.setting_id[0] == 'P' && is_system_filament_id(vt_tray.setting_id))
continue;
if (vt_tray.setting_id.size() == 8 && vt_tray.setting_id[0] == 'P' && filament_list.find(vt_tray.setting_id) == filament_list.end()) { if (vt_tray.setting_id.size() == 8 && vt_tray.setting_id[0] == 'P' && filament_list.find(vt_tray.setting_id) == filament_list.end()) {
if (checked_filament.find(vt_tray.setting_id) != checked_filament.end()) { if (checked_filament.find(vt_tray.setting_id) != checked_filament.end()) {
need_checked_filament_id[nozzle_diameter_str].insert(vt_tray.setting_id); need_checked_filament_id[nozzle_diameter_str].insert(vt_tray.setting_id);

View File

@@ -537,6 +537,13 @@ bool PresetComboBox::add_ams_filaments(std::string selected, bool alias_name)
} }
auto iter = std::find_if(filaments.begin(), filaments.end(), auto iter = std::find_if(filaments.begin(), filaments.end(),
[&filament_id, this](auto &f) { return f.is_compatible && m_collection->get_preset_base(f) == &f && f.filament_id == filament_id; }); [&filament_id, this](auto &f) { return f.is_compatible && m_collection->get_preset_base(f) == &f && f.filament_id == filament_id; });
if (iter == filaments.end()) {
// Retired/renamed ids forward to a live successor through the shipped succession ledger.
const std::string successor = resolve_filament_id_succession(filament_id);
if (!successor.empty())
iter = std::find_if(filaments.begin(), filaments.end(),
[&successor, this](auto &f) { return f.is_compatible && m_collection->get_preset_base(f) == &f && f.filament_id == successor; });
}
if (iter == filaments.end()) { if (iter == filaments.end()) {
auto filament_type = tray.opt_string("filament_type", 0u); auto filament_type = tray.opt_string("filament_type", 0u);
if (!filament_type.empty()) { if (!filament_type.empty()) {

View File

@@ -19,6 +19,7 @@
#include <chrono> #include <chrono>
#include <cstdint> #include <cstdint>
#include <cctype> #include <cctype>
#include <map>
#include <thread> #include <thread>
namespace { namespace {
@@ -609,51 +610,70 @@ std::string MoonrakerPrinterAgent::map_filament_type_to_generic_id(const std::st
{ {
const std::string upper = trim_and_upper(filament_type); const std::string upper = trim_and_upper(filament_type);
// Map to OrcaFilamentLibrary preset IDs (compatible with all printers) // Normalize reported material names (trimmed, uppercased) to an OrcaFilamentLibrary
// Source: resources/profiles/OrcaFilamentLibrary/filament/ // generic family. The family's filament_id is resolved from the loaded system presets
// below rather than hardcoded, so profile id re-mints never require touching this table.
// scripts/test_moonraker_lane_data.py parses this initializer; keep the {"A", "B"} format.
static const std::map<std::string, std::string> type_to_ofl_family = {
// PLA variants
{"PLA", "PLA"},
{"PLA-CF", "PLA-CF"},
{"PLA SILK", "PLA Silk"},
{"PLA-SILK", "PLA Silk"},
{"PLA HIGH SPEED", "PLA High Speed"},
{"PLA-HS", "PLA High Speed"},
{"PLA HS", "PLA High Speed"},
// PLA variants // ABS/ASA variants
if (upper == "PLA") return "OGFL99"; {"ABS", "ABS"},
if (upper == "PLA-CF") return "OGFL98"; {"ASA", "ASA"},
if (upper == "PLA SILK" || upper == "PLA-SILK") return "OGFL96";
if (upper == "PLA HIGH SPEED" || upper == "PLA-HS" || upper == "PLA HS") return "OGFL95";
// ABS/ASA variants // PETG/PET variants
if (upper == "ABS") return "OGFB99"; {"PETG", "PETG"},
if (upper == "ASA") return "OGFB98"; {"PET", "PETG"},
{"PCTG", "PCTG"},
// PETG/PET variants // PA/Nylon variants
if (upper == "PETG" || upper == "PET") return "OGFG99"; {"PA", "PA"},
if (upper == "PCTG") return "OGFG97"; {"NYLON", "PA"},
{"PA-CF", "PA-CF"},
{"PPA", "PPA-CF"},
{"PPA-CF", "PPA-CF"},
{"PPA-GF", "PPA-GF"},
// PA/Nylon variants // PC variants
if (upper == "PA" || upper == "NYLON") return "OGFN99"; {"PC", "PC"},
if (upper == "PA-CF") return "OGFN98";
if (upper == "PPA" || upper == "PPA-CF") return "OGFN97";
if (upper == "PPA-GF") return "OGFN96";
// PC variants // PP/PE variants
if (upper == "PC") return "OGFC99"; {"PE", "PE"},
{"PP", "PP"},
// PP/PE variants // Support materials
if (upper == "PE") return "OGFP99"; {"PVA", "PVA"},
if (upper == "PP") return "OGFP97"; {"HIPS", "HIPS"},
{"BVOH", "BVOH"},
// Support materials // TPU variants
if (upper == "PVA") return "OGFS99"; {"TPU", "TPU"},
if (upper == "HIPS") return "OGFS98";
if (upper == "BVOH") return "OGFS97";
// TPU variants // Other materials
if (upper == "TPU") return "OGFU99"; {"EVA", "EVA"},
{"PHA", "PHA"},
{"COPE", "CoPE"},
{"SBS", "SBS"},
};
// Other materials auto it = type_to_ofl_family.find(upper);
if (upper == "EVA") return "OGFR99"; if (it == type_to_ofl_family.end())
if (upper == "PHA") return "OGFR98"; return UNKNOWN_FILAMENT_ID;
if (upper == "COPE") return "OGFLC99";
if (upper == "SBS") return "OFLSBS99";
// Unknown material if (auto* bundle = GUI::wxGetApp().preset_bundle) {
const Preset* preset = bundle->filaments.find_preset("Generic " + it->second + " @System");
if (preset != nullptr && preset->is_system && !preset->filament_id.empty())
return preset->filament_id;
}
// Unknown material, or no loaded preset data to resolve against
return UNKNOWN_FILAMENT_ID; return UNKNOWN_FILAMENT_ID;
} }

View File

@@ -14,6 +14,7 @@ add_executable(${_TEST_NAME}_tests
test_config.cpp test_config.cpp
test_preset_bundle_loading.cpp test_preset_bundle_loading.cpp
test_preset_setting_id.cpp test_preset_setting_id.cpp
test_filament_id_succession.cpp
test_elephant_foot_compensation.cpp test_elephant_foot_compensation.cpp
test_geometry.cpp test_geometry.cpp
test_placeholder_parser.cpp test_placeholder_parser.cpp

View File

@@ -0,0 +1,61 @@
#include <catch2/catch_all.hpp>
#include "libslic3r/Preset.hpp"
using namespace Slic3r;
// Pure-function coverage of the succession chain walk (follow_filament_id_succession).
// The ledger-file loader (filament_id_succession_map) is deliberately untested here so
// the test carries no resources-dir dependency.
TEST_CASE("filament_id succession follows forwarding chains", "[Preset][filament_id]") {
SECTION("empty map returns empty") {
const std::map<std::string, std::string> empty;
CHECK(follow_filament_id_succession("OFabc123", empty).empty());
CHECK(follow_filament_id_succession("", empty).empty());
}
const std::map<std::string, std::string> forwards = {
{"OFold001", "OFnew001"},
{"OFchainA", "OFchainB"},
{"OFchainB", "OFchainC"},
{"OFchainC", "OFlive00"},
};
SECTION("absent id returns empty") {
CHECK(follow_filament_id_succession("GFL99", forwards).empty());
}
SECTION("single hop resolves to the successor") {
CHECK(follow_filament_id_succession("OFold001", forwards) == "OFnew001");
}
SECTION("multi-hop chain resolves to the live end") {
CHECK(follow_filament_id_succession("OFchainA", forwards) == "OFlive00");
CHECK(follow_filament_id_succession("OFchainB", forwards) == "OFlive00");
CHECK(follow_filament_id_succession("OFchainC", forwards) == "OFlive00");
}
}
// Cycles are a ledger-validation error (script check 9) and never ship, but the client
// walk must still terminate on corrupt data: it stops at the last id reached before any
// id would repeat.
TEST_CASE("filament_id succession terminates on malformed cycles", "[Preset][filament_id]") {
SECTION("two-node cycle returns the last id before revisiting") {
const std::map<std::string, std::string> cycle = {{"A", "B"}, {"B", "A"}};
CHECK(follow_filament_id_succession("A", cycle) == "B");
CHECK(follow_filament_id_succession("B", cycle) == "A");
}
SECTION("cycle entered through a tail stops before re-entering it") {
// T -> A -> B -> C -> A: stops at C, the last id reached before repeating A.
const std::map<std::string, std::string> cycle = {
{"T", "A"}, {"A", "B"}, {"B", "C"}, {"C", "A"}};
CHECK(follow_filament_id_succession("T", cycle) == "C");
CHECK(follow_filament_id_succession("A", cycle) == "C");
}
SECTION("self-loop returns the id itself") {
const std::map<std::string, std::string> loop = {{"A", "A"}};
CHECK(follow_filament_id_succession("A", loop) == "A");
}
}