mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 21:47:39 +02:00
fix(spoolman): resolve -1 in ams_mapping to external spool (#1276)
BambuStudio encodes virtual tray IDs (254/255) as -1 in the flat ams_mapping array — a convention already documented in bambu_mqtt.py:start_print(). The spoolman tracking helper was treating -1 as "unmapped, use position-based default", which mapped slot_id=1 to AMS tray 0 and credited external-spool prints to whatever Spoolman spool happened to be linked to AMS slot 0. The reporter's TPU prints on an H2S were credited to a PLA spool for ~49g over 4 prints before being noticed (regression of #853). When slot_to_tray[slot_id-1] == -1 and ams_trays contains 254/255, return the external tray ID directly. Prefers 254 over 255 (matches single-nozzle tray_now reporting + the vir_slot id=255->254 remap in bambu_mqtt.py:864). Legacy fall-through preserved for callers that don't pass ams_trays. Root cause investigation and patch by @ojimpo.
This commit is contained in:
@@ -8,6 +8,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
|
||||
- **External-spool prints no longer credit usage to AMS slot 0's Spoolman spool** ([#1276](https://github.com/maziggy/bambuddy/issues/1276), reported and diagnosed by @ojimpo — regression of #853) — On a single-filament external-spool print (TPU loaded in `vir_slot id=254` on the reporter's H2S + AMS 2 Pro), `_resolve_global_tray_id` in `spoolman_tracking.py` was crediting the usage to whatever Spoolman spool happened to be linked to AMS slot 0 — a completely unrelated material in the reporter's case. ~48.94 g of TPU was credited to a PLA spool across 4 prints before they noticed. Root cause: BambuStudio encodes virtual tray IDs (254/255) as `-1` in the flat `ams_mapping` array it sends to the printer (a convention already documented in `bambu_mqtt.py:start_print()`), but the spoolman tracking helper was treating `-1` as "unmapped → use position-based default" and the default mapped `slot_id=1` → `global_tray_id=0`. When `slot_to_tray[slot_id-1] == -1` and `ams_trays` contains an external slot (254 or 255), the helper now returns the external tray ID directly, matching the convention `start_print()` uses on the other side of the pipeline. Prefers 254 over 255 (consistent with single-nozzle `tray_now` reporting and the `vir_slot` id=255→254 remap in `bambu_mqtt.py:864`). Legacy behavior preserved when `ams_trays` is empty or contains no external slot (callers that don't pass `ams_trays` keep the position-based fallback). Two regression tests cover the reporter's exact scenario (`ams_trays={0,1,2,3,254}, slot_to_tray=[-1]` → 254) plus the H2D-deputy case and the fall-through-when-no-external case. **Root cause investigation and patch by @ojimpo.**
|
||||
|
||||
- **Virtual-printer queue mode now honors workflow default print options** ([#1235](https://github.com/maziggy/bambuddy/issues/1235), reported by @jc21, root cause and patch by @jc21 in #1277) — Prints sent from Bambu Studio (or any slicer) to a VP in `print_queue` mode arrived in the queue with `bed_levelling`, `flow_cali`, `vibration_cali`, `layer_inspect`, and `timelapse` set to the SQLAlchemy column-level defaults, never the user's workflow preferences. The reporter happened to have every workflow default set to the opposite of the column defaults, so prints appeared to have all five options inverted; every queue item required hand-editing before dispatch. The manual `POST /print-queue/` endpoint reads these fields off the request body (the frontend pulls them from settings before submitting), but the VP-FTP-receive path at `backend/app/services/virtual_printer/manager.py:_add_to_print_queue` constructed `PrintQueueItem` without touching them at all — SQLAlchemy then filled in `bed_levelling=True, flow_cali=False, vibration_cali=True, layer_inspect=False, timelapse=False` regardless of what was in the DB. Fix reads `default_bed_levelling` / `default_flow_cali` / `default_vibration_cali` / `default_layer_inspect` / `default_timelapse` via the existing `get_setting()` helper (same pattern already used in the function for `virtual_printer_archive_name_source`) and passes them explicitly to `PrintQueueItem`. A small `_bool_setting()` helper maps `None → AppSettings schema default`, so a fresh install with no workflow page customization behaves identically to before. Regression tests: `test_add_to_print_queue_uses_workflow_defaults_from_settings` (verifies all five settings flow through with values opposite to the column defaults, matching the reporter's exact scenario) and `test_add_to_print_queue_falls_back_to_schema_defaults_when_unset` (verifies the no-DB-row path).
|
||||
|
||||
- **Linking a Spoolman spool to an AMS-HT slot no longer fails with a CHECK constraint error** ([#1274](https://github.com/maziggy/bambuddy/issues/1274), reported by guillaume.houba) — On H2C / H2D, AMS-HT units report `ams_id` 128+ (one ams_id per unit, single tray). The `spoolman_slot_assignments` table's `ck_ams_id_range` constraint only allowed 0-7 (standard AMS) or 255 (external), so the upsert on `POST /spoolman/inventory/slot-assignments` blew up with `IntegrityError: CHECK constraint failed: ck_ams_id_range` and the user had no way to link any spool to an AMS-HT slot. Widened the constraint formula to `(ams_id >= 0 AND ams_id <= 7) OR (ams_id >= 128 AND ams_id <= 191) OR ams_id = 255` — matches the value range the internal `spool_assignment` table already accepts and leaves room for up to 64 AMS-HT units (the existing `bambu_mqtt`/usage-tracker code uses the same 128-based addressing). Updated in the ORM model (`models/spoolman_slot_assignment.py`) and both the SQLite/Postgres `CREATE TABLE` DDL in `core/database.py`. New idempotent migration `_migrate_widen_spoolman_slot_ams_id_range`: Postgres path runs `DROP CONSTRAINT IF EXISTS` + `ADD CONSTRAINT` (no data risk — the new formula is strictly wider than the old); SQLite path detects the stale formula in `sqlite_master`, table-rebuilds via the standard `_v2` rename pattern used elsewhere in this file (`_migrate_update_auto_link_constraint` at `database.py:418`), and leaves pre-constraint legacy tables untouched. Tests: `test_ams_id_check_admits_ams_ht_range` (ORM + DDL formula) and `test_assign_accepts_ams_ht_id` (end-to-end `POST /slot-assignments` with `ams_id=128`).
|
||||
|
||||
@@ -106,15 +106,30 @@ def _resolve_global_tray_id(slot_id: int, slot_to_tray: list | None, ams_trays:
|
||||
"""Map a 1-based slot_id to a global_tray_id using optional custom mapping.
|
||||
|
||||
Custom mapping: slot_to_tray[slot_id - 1] is used when >= 0.
|
||||
A value of -1 in the custom mapping means the slicer routed this slot to
|
||||
the external spool. BambuStudio converts virtual tray IDs (254/255) to -1
|
||||
in the flat ams_mapping array before sending to the printer — see
|
||||
start_print() in bambu_mqtt.py which documents this convention. We mirror
|
||||
it here: when -1 is seen, look up the external spool's actual
|
||||
global_tray_id (254/255) in ams_trays rather than falling through to the
|
||||
position-based default (which would map slot_id=1 to the first AMS tray
|
||||
and credit an unrelated spool — see #1276, regression of #853).
|
||||
Position-based default: uses sorted ams_trays keys so external spools (ID 254/255)
|
||||
naturally follow standard AMS trays, matching the slicer's slot numbering.
|
||||
A value of -1 in the custom mapping means unmapped (uses position-based default).
|
||||
Final fallback: slot_id - 1 (legacy, works for pure AMS without external spools).
|
||||
"""
|
||||
if slot_to_tray and slot_id <= len(slot_to_tray):
|
||||
mapped_tray = slot_to_tray[slot_id - 1]
|
||||
if mapped_tray >= 0:
|
||||
return mapped_tray
|
||||
if mapped_tray == -1 and ams_trays:
|
||||
# -1 means external spool. 254 = VIRTUAL_TRAY_DEPUTY_ID (main on
|
||||
# single-nozzle, left/deputy on H2D dual-nozzle); 255 =
|
||||
# VIRTUAL_TRAY_MAIN_ID. Prefer 254 when both exist since that's
|
||||
# what single-nozzle printers report via tray_now.
|
||||
for ext_id in (254, 255):
|
||||
if ext_id in ams_trays:
|
||||
return ext_id
|
||||
# Position-based default: sort available tray IDs so external spools (254/255)
|
||||
# come after standard AMS trays, matching the slicer's slot assignment order.
|
||||
if ams_trays:
|
||||
|
||||
@@ -91,6 +91,35 @@ class TestResolveGlobalTrayId:
|
||||
mapping = []
|
||||
assert _resolve_global_tray_id(1, mapping) == 0
|
||||
|
||||
def test_minus_one_resolves_to_external_spool_when_present(self):
|
||||
"""#1276 (regression of #853): -1 in slot_to_tray is BambuStudio's
|
||||
encoding for "external spool used" — look up the external spool in
|
||||
ams_trays rather than falling through to the position-based default
|
||||
(which would credit an unrelated AMS tray). Reporter ojimpo's H2S
|
||||
had AMS slot 0 occupied with PLA and ran a TPU external-spool print;
|
||||
the bug credited the TPU usage to the PLA spool.
|
||||
"""
|
||||
# Single external spool (most common: H2S/X1C/P1S + external)
|
||||
assert _resolve_global_tray_id(1, [-1], ams_trays={254: {}}) == 254
|
||||
# AMS occupied with material AND external in use — fix prevents
|
||||
# crediting AMS slot 0 (the actual bug from #1276)
|
||||
assert _resolve_global_tray_id(1, [-1], ams_trays={0: {}, 1: {}, 2: {}, 3: {}, 254: {}}) == 254
|
||||
# H2D-style deputy nozzle at 255
|
||||
assert _resolve_global_tray_id(1, [-1], ams_trays={0: {}, 255: {}}) == 255
|
||||
# Both external slots present (multi-nozzle) — prefer 254 (main on
|
||||
# single-nozzle, deputy on H2D — matches tray_now reporting)
|
||||
assert _resolve_global_tray_id(1, [-1], ams_trays={254: {}, 255: {}}) == 254
|
||||
|
||||
def test_minus_one_falls_through_when_no_external_in_ams_trays(self):
|
||||
"""If -1 is seen but ams_trays has no external spool (254/255),
|
||||
fall through to position-based default (legacy behavior preserved
|
||||
for callers that don't pass ams_trays or pre-fix call sites).
|
||||
"""
|
||||
# ams_trays without external — fall through to legacy behavior
|
||||
assert _resolve_global_tray_id(1, [-1], ams_trays={0: {}, 1: {}}) == 0
|
||||
# No ams_trays passed at all — legacy fallback
|
||||
assert _resolve_global_tray_id(1, [-1]) == 0
|
||||
|
||||
|
||||
class TestFallbackTagHelpers:
|
||||
"""Tests for frontend-mirrored fallback tag helpers."""
|
||||
|
||||
Reference in New Issue
Block a user