From db951de2c220768456f5341236b9a91e38851f01 Mon Sep 17 00:00:00 2001 From: maziggy Date: Thu, 14 May 2026 09:16:50 +0200 Subject: [PATCH] Updated CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3ed05ff5..41856dd6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ All notable changes to Bambuddy will be documented in this file. - **Spoolman weight tracking now uses per-print grams for all spools, matching the internal Filament Inventory** ([#1119](https://github.com/maziggy/bambuddy/issues/1119), reported by @Moskito99) — Spoolman previously had two mutually-exclusive weight paths: AMS remain%×tray_weight auto-sync (default; only worked for Bambu Lab spools with valid RFID tray_weight) and per-print 3MF-grams tracking (only enabled when "Disable AMS Weight Sync" was toggled on). Non-BL spools without RFID fell through both paths — AMS auto-sync had no tray_weight to multiply, and the inventory_remaining fallback was wiped because activating Spoolman deletes the internal `spool_assignment` table — so Spoolman never saw a weight update for them. The internal Filament Inventory has no such gap: it always uses per-print 3MF grams as the primary path with AMS-remain% delta as fallback, and it works for every spool type. Spoolman now does the same: per-print tracking runs whenever Spoolman is enabled and is the only writer of `remaining_weight`. AMS auto-sync continues to maintain spool metadata and slot assignments but no longer touches weight (eliminating the double-count that would otherwise occur for BL spools with both paths active). `store_print_data` ([`spoolman_tracking.py:159`](backend/app/services/spoolman_tracking.py)) had its `disable_weight_sync` early-return removed; the three `sync_ams_tray` callsites (`main.py:1450` auto-sync, `spoolman.py:318` per-printer manual, `spoolman.py:517` sync-all) now hard-code `disable_weight_sync=True`. The `spoolman_disable_weight_sync` setting is now deprecated and a no-op — kept in the DB/UI for backwards compat. Behavioral consequence for existing users on the default flag (False): live AMS-based remaining_weight updates between prints stop happening; weight updates now arrive once per print completion with 3MF gram precision. Regression test in `test_spoolman_tracking.py::test_stores_tracking_when_disable_weight_sync_is_false` proves the early-return is gone. ### Fixed +- **Spoolman auto-create no longer labels Bambu Lab RFID spools with competitor names like "3DXTECH™ Black"** ([#1309](https://github.com/maziggy/bambuddy/issues/1309), [PR #1330](https://github.com/maziggy/bambuddy/pull/1330) by @ojimpo) — When Bambuddy auto-created a Spoolman filament entry for a Bambu Lab RFID spool, the second-stage lookup against Spoolman's external library (`GET /api/v1/external/filament`, served from [SpoolmanDB](https://donkie.github.io/SpoolmanDB/filaments.json)) matched on `material + color_hex` only — there was no `manufacturer` / `vendor` filter. **The catalog is multi-vendor and roughly ID-sorted**: for PLA + `#000000` (black) it contains 64 entries, with the first hit being `3djake_pla_black_1000_175_n` (`3DJAKE`), the third being `3dxtech_pla_carbonxcarbonfiberblack_500_175_p` (`3DXTECH`, name `CarbonX™ Carbon Fiber Black`), and the actual `bambulab_pla_black_1000_175_n` not surfacing until position 15. Bambuddy therefore created the filament under the Bambu Lab vendor but labeled it with a competitor's product name. Real-world observations in production: Bambu Lab ABS Black created as `3DXTECH™ Black`, Bambu Lab PLA Support picked the adjacent / wrong variant instead of `bambulab_pla_supportforpla/petgblack_500_175_n`, and PLA Basic Black created as `PLA` (material, not `PLA Basic`). A **secondary issue compounded this**: `_create_filament_from_external` dropped the external entry's `density` field, so even when the correct entry was eventually picked the density got overwritten by `create_filament`'s built-in PLA-default 1.24 fallback instead of the catalog's actual value (1.26 for PLA Basic, 1.31 for PETG, etc.). **Fix** in [`backend/app/services/spoolman.py::_find_or_create_filament`](backend/app/services/spoolman.py): (1) the external-library loop now filters by `manufacturer == "Bambu Lab"` (case-insensitive, whitespace-trimmed), with a defensive `id.startswith("bambulab_")` fallback that handles entries where the `manufacturer` field is missing or has drifted in a future SpoolmanDB schema. (2) When multiple Bambu Lab candidates match the same `material + color_hex`, the function prefers the entry whose `name` equals the AMS `tray_sub_brands` (lowercase+strip comparison) so the more specific variant wins — `PLA Basic` over generic `Black`, `Support for PLA/PETG Black` over generic `Black`, etc. (3) `_create_filament_from_external` now propagates `external.get("density")` through to `create_filament`; when the catalog entry has no density set, the existing material-table fallback inside `create_filament` still kicks in via the `if density is None` branch at line 321 — no path lost. **Behavioural caveat the user needs to know**: previously-created mis-named filaments are NOT auto-renamed by this fix. Step 1 of `_find_or_create_filament` is the internal-Spoolman-filament loop that short-circuits on `(vendor == "Bambu Lab", material, color_hex)` — and that path is unchanged. Any Bambu Lab filament created by an older Bambuddy build (or hand-edited by the user) will continue to be matched and reused on subsequent AMS reads, regardless of how wrong its name is. To pick up the corrected name, the user has to **delete the mis-named filament in Spoolman once** — then the next AMS read for the same material+color falls through to the external-library step and creates a new entry with the correct Bambu Lab name. This is deliberate: some users may have intentionally renamed Bambu Lab filaments (e.g. to follow their own naming convention or to merge variants) and a silent auto-rename would undo that. **Regression tests** in [`test_spoolman_service.py::TestFindOrCreateFilament`](backend/tests/unit/services/test_spoolman_service.py) (6 new): internal short-circuit preserves the existing match without touching the external library, non-Bambu-Lab external entries are skipped even when they sort first in SpoolmanDB, `PLA Basic` wins over generic `Black` via the `tray_sub_brands` tiebreaker (per maintainer request on #1309), no-match-anywhere falls back to `tray_sub_brands or tray_type` instead of leaking a competitor name into the create call, `id.startswith("bambulab_")` accepts entries with absent `manufacturer` field, and density propagates end-to-end through the public method instead of getting clobbered by the material-default. All 44 tests in `test_spoolman_service.py` pass; ruff clean. **Reported and patched by @ojimpo.** + - **Safety: bed-jog Z direction was inverted on A1 / A1 Mini — "Up" rammed the nozzle into the bed** ([#1334](https://github.com/maziggy/bambuddy/issues/1334), reported by william.filipcic@gmail.com) — On A1 / A1 Mini, clicking the "Up" arrow on the printer-card bed-jog control would send the nozzle straight into the build plate. Reporter triggered it with the 50 mm step and crashed their nozzle. **Root cause:** the bed-jog UI was designed against the X1 / P1 / H2 family's bed-on-Z convention. On those printers the bed is the Z-axis, Bambu's firmware homes Z=0 at the top, and ``G1 Z-`` raises the bed toward the toolhead (decreases the nozzle-bed gap). The frontend maps "Up" to negative distance with that convention in mind. **A1 / A1 Mini are bed-slingers**: the bed moves on Y, the toolhead moves on X+Z, and the firmware uses standard cartesian Z (Z+ = toolhead up). On those models ``G1 Z-10`` drives the *toolhead* down 10 mm — straight through any clearance the user had — which is exactly what the reporter saw. There was no model classification at the bed-jog code path; every printer got the same X1-convention G-code. **Fix:** new ``is_bed_slinger(model)`` helper at [`backend/app/services/printer_manager.py`](backend/app/services/printer_manager.py) (sibling to existing ``supports_chamber_temp`` / ``has_stg_cur_idle_bug``, reuses the already-defined ``A1_MODELS`` frozenset which covers display names and internal codes ``N1`` / ``N2S``). The bed-jog route at [`backend/app/api/routes/printers.py:2710`](backend/app/api/routes/printers.py) now inverts the signed distance before emitting the G-code when the printer model is in that set, so the UI "Up" semantics ("decrease nozzle-bed gap") stay consistent regardless of which physical part moves on the printer. Frontend stays untouched — single source of truth for the direction logic lives in the backend, keyed off the printer's ``model`` column, so any future bed-slinger Bambu model only needs one frozenset update. The route's ``Query`` description and docstring now state the new contract explicitly: distance is the *gap adjustment*, not the raw Z value, and the backend translates per model. **Regression tests:** 13 in [`test_bed_jog.py::TestBedJogAPI`](backend/tests/unit/test_bed_jog.py) — 6 parametrised cases prove bed-on-Z models (X1C / P1S / H2D / H2S / H2C / P2S) still emit ``G1 Z-10.00`` for a UI "Up" click (pass-through), 6 parametrised cases prove A1 / A1 Mini / A1MINI / A1-MINI / N1 / N2S emit ``G1 Z10.00`` instead (inverted, toolhead up), plus 1 symmetric "Down arrow drops the toolhead via ``G1 Z-``" case. 5 in [`test_printer_manager.py::TestIsBedSlinger`](backend/tests/unit/services/test_printer_manager.py) pin the helper's classification contract — A1 family true, every bed-on-Z model false, None / empty-string safe, case-insensitive. **Safety note:** if you own an A1 or A1 Mini and were running any 0.2.x build before this release, do not use the printer-card bed-jog buttons — they will move the toolhead in the wrong direction. The Z controls in Bambu Studio / Bambu Handy are unaffected (they generate their own model-aware G-code). - **Spoolman inventory: editing a spool's color name no longer "reverts" to the subtype on save** ([#1319](https://github.com/maziggy/bambuddy/issues/1319), reported by @MartinNYHC) — On Spoolman-backed inventory, changing a spool's color name in the edit dialog appeared to accept the new value but the inventory list column and the next edit-dialog open showed it back to the subtype string. **Three layers stacked on top of each other to produce this:** (1) `find_or_create_filament` at [`backend/app/services/spoolman.py:609`](backend/app/services/spoolman.py) matches existing Spoolman filaments by `material / name / color_hex / vendor` — `color_name` is intentionally not part of the match key (Spoolman doesn't standardise the field and most installs leave it null) — but when a match was found it returned the existing filament's id unchanged, silently dropping the new `color_name` value. The write never reached Spoolman. (2) On re-read, the helper at [`_spoolman_helpers.py:279`](backend/app/api/routes/_spoolman_helpers.py) falls back to `subtype` when `filament.color_name` is empty (without the fallback, Spoolman installs that don't fill the field would render every spool as "Unknown color"). The persisted value was still empty, so the read synthesised the column from `subtype`. (3) The edit form prefilled `color_name` from `spool.color_name` — which on Spoolman installs without `color_name` was the synth value (= subtype). If the user changed `subtype` but not `color_name`, the form silently round-tripped the OLD subtype back to Spoolman as if it were a real user-set `color_name`, which then started showing up as the persisted value on the next render — the exact "color reverts to subtype" pattern in the bug report. **Fixes:** (1) `find_or_create_filament` now patches the matched filament's `color_name` via the existing `patch_filament` PATCH wrapper when the request differs from what's stored. Convention on the parameter: `None` = "don't touch", `""` = explicit clear (patches Spoolman to `null`), any other string = set/update. (2) The PATCH route at [`spoolman_inventory.py:567`](backend/app/api/routes/spoolman_inventory.py) now uses Pydantic's `model_fields_set` to distinguish "field omitted" from "field explicitly set to null" — only the latter is a clear (mirrors the existing `storage_location` pattern at the same site). (3) The map helper now also returns `color_name_is_synthesized: bool` on every inventory record, and [`SpoolFormModal.tsx`](frontend/src/components/SpoolFormModal.tsx) checks it on prefill so the input starts blank when the value was synthesised from subtype — the user sees the real stored state and can't accidentally round-trip the synth value back. The read-side fallback is kept on purpose (the list-display "Unknown color" problem hasn't gone away — it's just that the form no longer treats the fallback as a real value). A `patch_filament` failure is caught and logged but doesn't block the match — the spool still links to the correct filament, only the colour-name update is dropped, which is the safer failure mode. **Regression tests:** 5 in [`test_spoolman_inventory_methods.py::TestFindOrCreateFilament`](backend/tests/unit/test_spoolman_inventory_methods.py) — patch-on-change, no-patch-when-unchanged, no-patch-when-`None`, clear-when-`""`-passed, and patch-failure-still-returns-match-id. 2 in [`test_spoolman_inventory_helpers.py::TestMapSpoolmanSpool`](backend/tests/unit/test_spoolman_inventory_helpers.py) — `color_name_is_synthesized` flag is `False` when a real value is stored, `True` when the fallback fires. 2 integration tests in [`test_spoolman_inventory_api.py`](backend/tests/integration/test_spoolman_inventory_api.py) — wire-level `color_name=null` clears (route translates to `""`), and `color_name` omitted from the PATCH body keeps the current value (route passes `None`). All 564 spoolman-tagged tests pass; ruff clean; frontend build clean.