mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-22 03:49:18 +02:00
The slot card on PrintersPage shows slot_preset_mappings.preset_name first in its display fallback chain. Three write paths swap which spool occupies a given slot: - internal manual assign (inventory.apply_spool_to_slot_via_mqtt) - internal RFID auto-assign (spool_tag_matcher.auto_assign_spool) - Spoolman RFID sync (main.auto_sync_spoolman_ams_trays) Only the first one was reconciling the row. After an RFID-driven spool change, the card kept surfacing the previous spool's preset name until the user opened Configure Slot manually. Reporter saw H2D-1 / AMS-B3 displaying "Bambu PLA Silk+" for a freshly-inserted Bambu PLA-CF spool. The matching row in slot_preset_mappings was last written in March when a PLA Silk+ spool had been in that slot - confirmed live in the database. New backend/app/services/slot_preset_writer.py exposes a primitive upsert_slot_preset plus two derivation wrappers: one for the internal Spool ORM object, one for the Spoolman API dict shape. All three call sites now go through the helper, so the row stays in lockstep with the assigned spool regardless of inventory mode. Bug shape exists in both inventory modes and the patch fixes both per feedback_inventory_modes_parity. The Spoolman path was latent for users who'd never manually picked a slot preset; the same "stale row overrides correct catalog name" symptom appeared for those who had. Existing stale rows self-heal on the next RFID-driven swap.
159 lines
5.2 KiB
Python
159 lines
5.2 KiB
Python
"""Shared upsert for the slot_preset_mappings row that drives the AMS slot
|
|
card's displayed preset name.
|
|
|
|
Three call sites must keep this row in sync with the currently-assigned spool:
|
|
|
|
- ``api.routes.inventory.apply_spool_to_slot_via_mqtt`` (internal manual assign)
|
|
- ``services.spool_tag_matcher.auto_assign_spool`` (internal RFID auto-assign)
|
|
- ``main.auto_sync_spoolman_ams_trays`` (Spoolman RFID-driven sync)
|
|
|
|
If any of them skips this row, the slot card surfaces the previous spool's
|
|
preset name because the PrintersPage display chain consults
|
|
slot_preset_mappings.preset_name first — it overrides cloudInfo.name and the
|
|
spool's own slicer_filament_name.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from backend.app.models.slot_preset import SlotPresetMapping
|
|
from backend.app.models.spool import Spool
|
|
from backend.app.utils.filament_ids import filament_id_to_setting_id
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def upsert_slot_preset(
|
|
*,
|
|
db: AsyncSession,
|
|
printer_id: int,
|
|
ams_id: int,
|
|
tray_id: int,
|
|
preset_id: str,
|
|
preset_name: str,
|
|
preset_source: str = "cloud",
|
|
) -> None:
|
|
"""Primitive upsert. No-op when ``preset_id`` is empty (the column is
|
|
NOT NULL on the model, and an empty string isn't a useful key to
|
|
overwrite by). Soft-fails on DB errors so a broken upsert never
|
|
cascades into the surrounding spool-assign flow.
|
|
"""
|
|
if not preset_id:
|
|
return
|
|
try:
|
|
existing = await db.execute(
|
|
select(SlotPresetMapping).where(
|
|
SlotPresetMapping.printer_id == printer_id,
|
|
SlotPresetMapping.ams_id == ams_id,
|
|
SlotPresetMapping.tray_id == tray_id,
|
|
)
|
|
)
|
|
mapping = existing.scalar_one_or_none()
|
|
if mapping:
|
|
mapping.preset_id = preset_id
|
|
mapping.preset_name = preset_name
|
|
mapping.preset_source = preset_source
|
|
else:
|
|
mapping = SlotPresetMapping(
|
|
printer_id=printer_id,
|
|
ams_id=ams_id,
|
|
tray_id=tray_id,
|
|
preset_id=preset_id,
|
|
preset_name=preset_name,
|
|
preset_source=preset_source,
|
|
)
|
|
db.add(mapping)
|
|
await db.commit()
|
|
except Exception as e:
|
|
logger.warning(
|
|
"Failed to save slot preset mapping for printer=%d ams=%d tray=%d: %s",
|
|
printer_id,
|
|
ams_id,
|
|
tray_id,
|
|
e,
|
|
)
|
|
|
|
|
|
async def upsert_slot_preset_for_spool(
|
|
*,
|
|
db: AsyncSession,
|
|
spool: Spool,
|
|
printer_id: int,
|
|
ams_id: int,
|
|
tray_id: int,
|
|
tray_info_idx: str = "",
|
|
tray_sub_brands: str = "",
|
|
tray_type: str = "",
|
|
setting_id: str = "",
|
|
) -> None:
|
|
"""Convenience wrapper for internal-mode call sites — derives the
|
|
(preset_id, preset_name, preset_source) triple from a ``Spool`` ORM object,
|
|
then defers to ``upsert_slot_preset``.
|
|
|
|
Local numeric ``spool.slicer_filament`` (e.g. ``"50"``) → ``local_50``;
|
|
cloud-form ids (GFS… / GFA… via ``filament_id_to_setting_id`` on the
|
|
tray's ``tray_info_idx``) → standard setting_id form.
|
|
"""
|
|
preset_name = spool.slicer_filament_name or tray_sub_brands or tray_type
|
|
preset_source = "cloud"
|
|
sf = spool.slicer_filament or ""
|
|
if sf:
|
|
base_sf_mapping = sf.split("_")[0] if "_" in sf else sf
|
|
try:
|
|
int(base_sf_mapping)
|
|
preset_id = f"local_{base_sf_mapping}"
|
|
preset_source = "local"
|
|
except (ValueError, TypeError):
|
|
preset_id = filament_id_to_setting_id(tray_info_idx) if tray_info_idx else setting_id
|
|
else:
|
|
preset_id = filament_id_to_setting_id(tray_info_idx) if tray_info_idx else ""
|
|
|
|
await upsert_slot_preset(
|
|
db=db,
|
|
printer_id=printer_id,
|
|
ams_id=ams_id,
|
|
tray_id=tray_id,
|
|
preset_id=preset_id,
|
|
preset_name=preset_name,
|
|
preset_source=preset_source,
|
|
)
|
|
|
|
|
|
async def upsert_slot_preset_for_spoolman_spool(
|
|
*,
|
|
db: AsyncSession,
|
|
spoolman_spool: dict,
|
|
tray_info_idx: str,
|
|
tray_sub_brands: str,
|
|
tray_type: str,
|
|
printer_id: int,
|
|
ams_id: int,
|
|
tray_id: int,
|
|
) -> None:
|
|
"""Spoolman-mode counterpart. Derives ``preset_name`` from the Spoolman
|
|
filament's display name (falling back to material if absent) and
|
|
``preset_id`` from the AMS-reported tray_info_idx (the cloud filament
|
|
id the printer is currently using). ``preset_source`` is always
|
|
``"cloud"`` since Spoolman doesn't carry a local-preset concept.
|
|
|
|
The ``spoolman_spool`` dict is the shape returned by
|
|
``SpoolmanClient.sync_ams_tray`` — ``spool["filament"]["name"]`` etc.
|
|
"""
|
|
filament = spoolman_spool.get("filament") or {}
|
|
preset_name = filament.get("name") or filament.get("material") or tray_sub_brands or tray_type
|
|
preset_id = filament_id_to_setting_id(tray_info_idx) if tray_info_idx else ""
|
|
|
|
await upsert_slot_preset(
|
|
db=db,
|
|
printer_id=printer_id,
|
|
ams_id=ams_id,
|
|
tray_id=tray_id,
|
|
preset_id=preset_id,
|
|
preset_name=preset_name or "",
|
|
preset_source="cloud",
|
|
)
|