This commit is contained in:
maziggy
2026-02-13 07:53:14 +01:00
parent a6f6df2e34
commit e3f3c90c3d
3 changed files with 91 additions and 27 deletions
+36
View File
@@ -6,12 +6,14 @@ from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from backend.app.core.auth import RequirePermissionIfAuthEnabled
from backend.app.core.database import get_db
from backend.app.core.permissions import Permission
from backend.app.models.printer import Printer
from backend.app.models.settings import Settings
from backend.app.models.spool_assignment import SpoolAssignment
from backend.app.models.user import User
from backend.app.services.printer_manager import printer_manager
from backend.app.services.spoolman import (
@@ -230,6 +232,22 @@ async def sync_printer_ams(
detail=f"Failed to connect to Spoolman after multiple retries: {str(e)}",
)
# Load inventory weights as fallback (when AMS MQTT data lacks remain values)
inv_weights: dict[tuple[int, int], float] = {}
try:
assign_result = await db.execute(
select(SpoolAssignment)
.options(selectinload(SpoolAssignment.spool))
.where(SpoolAssignment.printer_id == printer_id)
)
for assignment in assign_result.scalars().all():
spool = assignment.spool
if spool and spool.label_weight > 0:
remaining = max(0.0, spool.label_weight - (spool.weight_used or 0))
inv_weights[(assignment.ams_id, assignment.tray_id)] = remaining
except Exception as e:
logger.debug("Could not load inventory weights for printer %s: %s", printer_id, e)
for ams_unit in ams_units:
if not isinstance(ams_unit, dict):
continue
@@ -270,11 +288,13 @@ async def sync_printer_ams(
current_tray_uuids.add(spool_tag.upper())
try:
inv_remaining = inv_weights.get((ams_id, tray.tray_id))
sync_result = await client.sync_ams_tray(
tray,
printer.name,
disable_weight_sync=disable_weight_sync,
cached_spools=cached_spools,
inventory_remaining=inv_remaining,
)
if sync_result:
synced += 1
@@ -361,6 +381,19 @@ async def sync_all_printers(
detail=f"Failed to connect to Spoolman after multiple retries: {str(e)}",
)
# Load inventory assignments for weight fallback (when AMS MQTT data lacks remain values)
# Key: (printer_id, ams_id, tray_id) → remaining_weight in grams
inventory_weights: dict[tuple[int, int, int], float] = {}
try:
assign_result = await db.execute(select(SpoolAssignment).options(selectinload(SpoolAssignment.spool)))
for assignment in assign_result.scalars().all():
spool = assignment.spool
if spool and spool.label_weight > 0:
remaining = max(0.0, spool.label_weight - (spool.weight_used or 0))
inventory_weights[(assignment.printer_id, assignment.ams_id, assignment.tray_id)] = remaining
except Exception as e:
logger.debug("Could not load inventory assignments for weight fallback: %s", e)
for printer in printers:
state = printer_manager.get_status(printer.id)
if not state or not state.raw_data:
@@ -435,11 +468,14 @@ async def sync_all_printers(
printer_tray_uuids[printer.name].add(spool_tag.upper())
try:
# Look up inventory weight as fallback when AMS data is invalid
inv_remaining = inventory_weights.get((printer.id, ams_id, tray.tray_id))
sync_result = await client.sync_ams_tray(
tray,
printer.name,
disable_weight_sync=disable_weight_sync,
cached_spools=cached_spools,
inventory_remaining=inv_remaining,
)
if sync_result:
total_synced += 1
+22
View File
@@ -771,6 +771,26 @@ async def on_ams_change(printer_id: int, ams_data: list):
)
return
# Load inventory weights as fallback (when AMS MQTT data lacks remain values)
from sqlalchemy.orm import selectinload
from backend.app.models.spool_assignment import SpoolAssignment
inventory_weights: dict[tuple[int, int], float] = {}
try:
assign_result = await db.execute(
select(SpoolAssignment)
.options(selectinload(SpoolAssignment.spool))
.where(SpoolAssignment.printer_id == printer_id)
)
for assignment in assign_result.scalars().all():
spool = assignment.spool
if spool and spool.label_weight > 0:
remaining = max(0.0, spool.label_weight - (spool.weight_used or 0))
inventory_weights[(assignment.ams_id, assignment.tray_id)] = remaining
except Exception as e:
logger.debug("Could not load inventory weights for printer %s: %s", printer_id, e)
# Sync each AMS tray
synced = 0
for ams_unit in ams_data:
@@ -783,11 +803,13 @@ async def on_ams_change(printer_id: int, ams_data: list):
continue # Empty tray
try:
inv_remaining = inventory_weights.get((ams_id, tray.tray_id))
result = await client.sync_ams_tray(
tray,
printer_name,
disable_weight_sync=disable_weight_sync,
cached_spools=cached_spools,
inventory_remaining=inv_remaining,
)
if result:
synced += 1
+33 -27
View File
@@ -658,8 +658,8 @@ class SpoolmanClient:
# Get tray_info_idx (Bambu filament preset ID like "GFA00")
tray_info_idx = tray_data.get("tray_info_idx", "") or ""
# Get remaining percentage, ensure non-negative
remain = max(0, int(tray_data.get("remain", 0)))
# Get remaining percentage (-1 means unknown/not read by AMS)
remain = int(tray_data.get("remain", -1))
return AMSTray(
ams_id=ams_id,
@@ -760,6 +760,7 @@ class SpoolmanClient:
printer_name: str,
disable_weight_sync: bool = False,
cached_spools: list[dict] | None = None,
inventory_remaining: float | None = None,
) -> dict | None:
"""Sync a single AMS tray to Spoolman.
@@ -777,6 +778,8 @@ class SpoolmanClient:
cached_spools: Optional pre-fetched list of spools to search (avoids API calls).
When provided, this cache is passed to find_spool_by_tag to avoid redundant
API calls during batch sync operations.
inventory_remaining: Optional fallback remaining weight (grams) from the built-in
inventory when AMS MQTT data has invalid remain/tray_weight values.
Returns:
Synced spool dictionary or None if skipped or failed.
@@ -800,17 +803,31 @@ class SpoolmanClient:
return None
# Determine which identifier to use for Spoolman (prefer tray_uuid, fallback to tag_uid)
spool_tag = (
tray.tray_uuid if tray.tray_uuid and tray.tray_uuid != "00000000000000000000000000000000" else tray.tag_uid
)
# Zero-filled values mean the AMS hasn't read the RFID tag — treat as no tag
zero_uuid = "00000000000000000000000000000000"
zero_tag = "0000000000000000"
spool_tag = None
if tray.tray_uuid and tray.tray_uuid != zero_uuid:
spool_tag = tray.tray_uuid
elif tray.tag_uid and tray.tag_uid != zero_tag:
spool_tag = tray.tag_uid
# Calculate remaining weight (skip if data is invalid/unavailable)
# Some firmware sends remain=-1 (→0 after max) and tray_weight=0, making weight unreliable
remaining = (
self.calculate_remaining_weight(tray.remain, tray.tray_weight)
if tray.remain > 0 and tray.tray_weight > 0
else None
)
# Calculate remaining weight
# Primary: AMS MQTT data (remain percentage + tray_weight)
# Fallback: Built-in inventory tracked weight (when firmware sends invalid remain/tray_weight)
if tray.remain >= 0 and tray.tray_weight > 0:
remaining = self.calculate_remaining_weight(tray.remain, tray.tray_weight)
elif inventory_remaining is not None:
remaining = inventory_remaining
logger.debug(
"Using inventory weight fallback for %s AMS %s tray %s: %.1fg",
printer_name,
tray.ams_id,
tray.tray_id,
remaining,
)
else:
remaining = None
location = f"{printer_name} - {self.convert_ams_slot_to_location(tray.ams_id, tray.tray_id)}"
if spool_tag:
@@ -842,7 +859,8 @@ class SpoolmanClient:
)
# Fallback path: no RFID tag available (newer firmware may not expose UUIDs)
# Match existing Spoolman spools by their location (AMS slot position)
# Only update existing spools matched by location — never create new ones without a tag
# to avoid duplicates when old spools exist from previous RFID-based syncs
existing = self._find_spool_by_location(location, cached_spools)
if existing:
logger.info(
@@ -856,23 +874,11 @@ class SpoolmanClient:
location=location,
)
# No existing spool at this location — create a new one without a tag
logger.info(
"Creating new spool in Spoolman for %s at %s (no RFID tag available)",
tray.tray_sub_brands,
"No existing spool found at '%s' — skipping (no RFID tag to create with)",
location,
)
filament = await self._find_or_create_filament(tray)
if not filament:
logger.error("Failed to find or create filament for %s", tray.tray_sub_brands)
return None
return await self.create_spool(
filament_id=filament["id"],
remaining_weight=remaining,
location=location,
comment="Created by Bambuddy",
)
return None
async def _find_or_create_filament(self, tray: AMSTray) -> dict | None:
"""Find existing filament or create new one.