Fix Bambu Lab spool detection in Spoolman sync and AMS cards

The spool detection only checked tray_uuid and tag_uid, but some Bambu Lab
  spools have RFID read issues causing these to be missing while still having
  valid tray_info_idx (Bambu filament preset ID like "GFA00").

  Backend changes (backend/app/services/spoolman.py):
  - Add tray_info_idx to AMSTray dataclass
  - Parse tray_info_idx from raw MQTT data in parse_ams_tray()
  - Check tray_info_idx first in is_bambu_lab_spool() - most reliable indicator
  - Bambu Lab presets start with "GF" (GFA00, GFB00, GFL00, etc.)
  - Fall back to tray_uuid then tag_uid for unique spool identification
  - Add warning when Bambu spool detected but no unique ID for Spoolman

  Backend changes (backend/app/api/routes/spoolman.py):
  - Update /sync/{printer_id} and /sync-all to pass tray_info_idx
  - Guard against empty spool_tag when adding to tracking set

  Frontend changes (frontend/src/pages/PrintersPage.tsx):
  - Add isBambuLabSpool() helper with same detection logic as backend
  - Update all 4 vendor detection locations in AMS cards to use new helper

  Detection priority (same on frontend and backend):
  1. tray_info_idx starts with "GF" (Bambu preset ID)
  2. tray_uuid is valid 32-char hex (non-zero)
  3. tag_uid is valid 16-char hex (non-zero)
This commit is contained in:
maziggy
2026-01-15 16:23:09 +01:00
parent 6eedf55713
commit 61c6473eca
5 changed files with 470 additions and 415 deletions
+6 -4
View File
@@ -213,7 +213,7 @@ async def sync_printer_ams(
location = client.convert_ams_slot_to_location(ams_id, tray.tray_id)
# Skip non-Bambu Lab spools (SpoolEase/third-party) - track as skipped
if not client.is_bambu_lab_spool(tray.tray_uuid, tray.tag_uid):
if not client.is_bambu_lab_spool(tray.tray_uuid, tray.tag_uid, tray.tray_info_idx):
skipped.append(
SkippedSpool(
location=location,
@@ -230,7 +230,8 @@ async def sync_printer_ams(
if tray.tray_uuid and tray.tray_uuid != "00000000000000000000000000000000"
else tray.tag_uid
)
current_tray_uuids.add(spool_tag.upper())
if spool_tag:
current_tray_uuids.add(spool_tag.upper())
try:
sync_result = await client.sync_ams_tray(tray, printer.name)
@@ -342,7 +343,7 @@ async def sync_all_printers(db: AsyncSession = Depends(get_db)):
location = f"{printer.name} - {client.convert_ams_slot_to_location(ams_id, tray.tray_id)}"
# Skip non-Bambu Lab spools (SpoolEase/third-party) - track as skipped
if not client.is_bambu_lab_spool(tray.tray_uuid, tray.tag_uid):
if not client.is_bambu_lab_spool(tray.tray_uuid, tray.tag_uid, tray.tray_info_idx):
all_skipped.append(
SkippedSpool(
location=location,
@@ -359,7 +360,8 @@ async def sync_all_printers(db: AsyncSession = Depends(get_db)):
if tray.tray_uuid and tray.tray_uuid != "00000000000000000000000000000000"
else tray.tag_uid
)
printer_tray_uuids[printer.name].add(spool_tag.upper())
if spool_tag:
printer_tray_uuids[printer.name].add(spool_tag.upper())
try:
sync_result = await client.sync_ams_tray(tray, printer.name)
+37 -13
View File
@@ -49,6 +49,7 @@ class AMSTray:
remain: int # Remaining percentage (0-100)
tag_uid: str # RFID tag UID
tray_uuid: str # Spool UUID
tray_info_idx: str # Bambu filament preset ID like "GFA00"
tray_weight: int # Spool weight in grams (usually 1000)
@@ -521,6 +522,9 @@ class SpoolmanClient:
if tray_uuid in ("", "00000000000000000000000000000000"):
tray_uuid = ""
# 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)))
@@ -533,6 +537,7 @@ class SpoolmanClient:
remain=remain,
tag_uid=tag_uid,
tray_uuid=tray_uuid,
tray_info_idx=tray_info_idx.strip(),
tray_weight=int(tray_data.get("tray_weight", 1000)),
)
@@ -552,25 +557,35 @@ class SpoolmanClient:
ams_letter = chr(ord("A") + ams_id)
return f"AMS {ams_letter}{tray_id + 1}"
def is_bambu_lab_spool(self, tray_uuid: str, tag_uid: str = "") -> bool:
def is_bambu_lab_spool(self, tray_uuid: str, tag_uid: str = "", tray_info_idx: str = "") -> bool:
"""Check if a tray has a valid Bambu Lab spool.
Bambu Lab spools have a tray_uuid (32-character hex string) and/or
a tag_uid (16-character hex string). The tray_uuid is preferred as
it's consistent across printer models, but tag_uid is accepted as
a fallback since some spools may have RFID read issues.
Bambu Lab spools can be identified by:
1. tray_uuid: 32-character hex string (preferred, consistent across printers)
2. tag_uid: 16-character hex string (RFID tag, varies between readers)
3. tray_info_idx: Bambu filament preset ID like "GFA00" (most reliable)
Non-Bambu Lab spools (SpoolEase, third-party) won't have valid
tray_uuid or tag_uid.
Non-Bambu Lab spools (SpoolEase, third-party) won't have these identifiers.
Args:
tray_uuid: The tray UUID to check (32 hex chars)
tag_uid: The RFID tag UID to check as fallback (16 hex chars)
tray_info_idx: Bambu filament preset ID like "GFA00", "GFB00"
Returns:
True if the spool has valid Bambu Lab identifiers, False otherwise.
"""
# First check tray_uuid (preferred - consistent across printer models)
# Check tray_info_idx first - Bambu filament preset IDs like "GFA00", "GFB00", etc.
# This is the most reliable indicator as it's set when the spool is recognized
if tray_info_idx:
idx = tray_info_idx.strip()
# Bambu Lab preset IDs start with "GF" followed by letter and digits
# e.g., GFA00, GFB00, GFL00, GFN00, GFG00, GFS00, GFU00
if idx and len(idx) >= 3 and idx.startswith("GF"):
logger.debug(f"Identified Bambu Lab spool via tray_info_idx: {idx}")
return True
# Check tray_uuid (preferred - consistent across printer models)
if tray_uuid:
uuid = tray_uuid.strip()
if len(uuid) == 32 and uuid != "00000000000000000000000000000000":
@@ -624,16 +639,17 @@ class SpoolmanClient:
"""
logger.debug(
f"Processing {printer_name} AMS {tray.ams_id} tray {tray.tray_id}: "
f"type={tray.tray_type}, uuid={tray.tray_uuid[:16] if tray.tray_uuid else 'none'}, "
f"type={tray.tray_type}, idx={tray.tray_info_idx or 'none'}, "
f"uuid={tray.tray_uuid[:16] if tray.tray_uuid else 'none'}, "
f"tag={tray.tag_uid[:8] if tray.tag_uid else 'none'}..."
)
# Only sync trays with valid Bambu Lab identifiers (tray_uuid or tag_uid)
if not self.is_bambu_lab_spool(tray.tray_uuid, tray.tag_uid):
if tray.tray_uuid or tray.tag_uid:
# Only sync trays with valid Bambu Lab identifiers
if not self.is_bambu_lab_spool(tray.tray_uuid, tray.tag_uid, tray.tray_info_idx):
if tray.tray_uuid or tray.tag_uid or tray.tray_info_idx:
logger.info(
f"Skipping non-Bambu Lab spool: {printer_name} AMS {tray.ams_id} tray {tray.tray_id} "
f"(tray_uuid={tray.tray_uuid}, tag_uid={tray.tag_uid})"
f"(tray_info_idx={tray.tray_info_idx}, tray_uuid={tray.tray_uuid}, tag_uid={tray.tag_uid})"
)
else:
logger.debug(f"Skipping tray without RFID tag: AMS {tray.ams_id} tray {tray.tray_id}")
@@ -644,6 +660,14 @@ class SpoolmanClient:
tray.tray_uuid if tray.tray_uuid and tray.tray_uuid != "00000000000000000000000000000000" else tray.tag_uid
)
# If no unique identifier available, we can't sync even if it's a Bambu Lab spool
if not spool_tag:
logger.warning(
f"Bambu Lab spool detected but no unique identifier for Spoolman: "
f"{printer_name} AMS {tray.ams_id} tray {tray.tray_id} (tray_info_idx={tray.tray_info_idx})"
)
return None
# Calculate remaining weight
remaining = self.calculate_remaining_weight(tray.remain, tray.tray_weight)
location = f"{printer_name} - {self.convert_ams_slot_to_location(tray.ams_id, tray.tray_id)}"
+32 -3
View File
@@ -703,6 +703,35 @@ function getWifiStrength(rssi: number | null | undefined): { label: string; colo
return { label: 'Very weak', color: 'text-red-400', bars: 1 };
}
/**
* Check if a tray contains a Bambu Lab spool.
* Uses same logic as backend: tray_info_idx (GF*), tray_uuid, or tag_uid.
*/
function isBambuLabSpool(tray: {
tray_uuid?: string | null;
tag_uid?: string | null;
tray_info_idx?: string | null;
} | null | undefined): boolean {
if (!tray) return false;
// Check tray_info_idx first (most reliable - Bambu preset IDs start with "GF")
if (tray.tray_info_idx && tray.tray_info_idx.startsWith('GF')) {
return true;
}
// Check tray_uuid (32 hex chars, non-zero)
if (tray.tray_uuid && tray.tray_uuid !== '00000000000000000000000000000000') {
return true;
}
// Check tag_uid (16 hex chars, non-zero)
if (tray.tag_uid && tray.tag_uid !== '0000000000000000') {
return true;
}
return false;
}
function CoverImage({ url, printName }: { url: string | null; printName?: string }) {
const [loaded, setLoaded] = useState(false);
const [error, setError] = useState(false);
@@ -1921,7 +1950,7 @@ function PrinterCard({
// Build filament data for hover card
const filamentData = tray?.tray_type ? {
vendor: (tray.tray_uuid ? 'Bambu Lab' : 'Generic') as 'Bambu Lab' | 'Generic',
vendor: (isBambuLabSpool(tray) ? 'Bambu Lab' : 'Generic') as 'Bambu Lab' | 'Generic',
profile: cloudInfo?.name || tray.tray_sub_brands || tray.tray_type,
colorName: getBambuColorName(tray.tray_id_name) || hexToBasicColorName(tray.tray_color),
colorHex: tray.tray_color || null,
@@ -2068,7 +2097,7 @@ function PrinterCard({
// Build filament data for hover card
const filamentData = tray?.tray_type ? {
vendor: (tray.tray_uuid ? 'Bambu Lab' : 'Generic') as 'Bambu Lab' | 'Generic',
vendor: (isBambuLabSpool(tray) ? 'Bambu Lab' : 'Generic') as 'Bambu Lab' | 'Generic',
profile: cloudInfo?.name || tray.tray_sub_brands || tray.tray_type,
colorName: getBambuColorName(tray.tray_id_name) || hexToBasicColorName(tray.tray_color),
colorHex: tray.tray_color || null,
@@ -2242,7 +2271,7 @@ function PrinterCard({
// Build filament data for hover card
const extFilamentData = {
vendor: (extTray.tray_uuid ? 'Bambu Lab' : 'Generic') as 'Bambu Lab' | 'Generic',
vendor: (isBambuLabSpool(extTray) ? 'Bambu Lab' : 'Generic') as 'Bambu Lab' | 'Generic',
profile: extCloudInfo?.name || extTray.tray_sub_brands || extTray.tray_type || 'Unknown',
colorName: getBambuColorName(extTray.tray_id_name) || hexToBasicColorName(extTray.tray_color),
colorHex: extTray.tray_color || null,
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -23,7 +23,7 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-D7518JU5.js"></script>
<script type="module" crossorigin src="/assets/index-COShNpeg.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DAZKHkJ3.css">
</head>
<body>