Przeglądaj źródła

[Fix]: Send Bambu RFID Tags to Spoolman, Allow Unlink in Manual Mode, Stop Location Clearing for Generic Spools (#719)

[Fix]: Send Bambu RFID Tags to Spoolman, Allow Unlink in Manual Mode, Stop Location Clearing for Generic Spools (#719)
Dakota G 5 miesięcy temu
rodzic
commit
ac7cfe2930

+ 5 - 0
backend/app/services/spoolman.py

@@ -9,6 +9,7 @@ import httpx
 
 logger = logging.getLogger(__name__)
 
+BAMBU_RFID_TAG_LENGTH = 32
 
 @dataclass
 class SpoolmanSpool:
@@ -553,6 +554,10 @@ class SpoolmanClient:
             else:
                 spool_uuid = ""
 
+            # Only clear location for Bambu Lab spools (those with a stored 32-character RFID tag).
+            if len(spool_uuid) != BAMBU_RFID_TAG_LENGTH:
+                continue
+
             # If this spool's UUID is not in the current AMS, clear its location
             if spool_uuid not in current_tray_uuids:
                 logger.info(

+ 8 - 4
backend/tests/unit/services/test_spoolman_service.py

@@ -298,11 +298,15 @@ class TestSpoolmanClient:
     async def test_clear_location_for_removed_spools_with_cached_spools(self, client):
         """Verify clear_location_for_removed_spools uses cached spools."""
         cached = [
-            {"id": 1, "location": "Printer1 - AMS A1", "extra": {"tag": '"TAG1"'}},
-            {"id": 2, "location": "Printer1 - AMS A2", "extra": {"tag": '"TAG2"'}},
-            {"id": 3, "location": "Printer1 - AMS A3", "extra": {"tag": '"TAG3"'}},
+            {"id": 1, "location": "Printer1 - AMS A1", "extra": {"tag": '"A1B2C3D4E5F60718293A4B5C6D7E8F90"'}},
+            {"id": 2, "location": "Printer1 - AMS A2", "extra": {"tag": '"B1C2D3E4F5061728394A5B6C7D8E9F01"'}},
+            {"id": 3, "location": "Printer1 - AMS A3", "extra": {"tag": '"C1D2E3F40516273849A5B6C7D8E9F012"'}},
         ]
-        current_tags = {"TAG1", "TAG2"}  # TAG3 was removed
+        # Tag 3 was cleared, so only tags 1 and 2 are current
+        current_tags = {
+            "A1B2C3D4E5F60718293A4B5C6D7E8F90",
+            "B1C2D3E4F5061728394A5B6C7D8E9F01",
+        }
 
         with (
             patch.object(client, "get_spools", AsyncMock()) as mock_get,

+ 20 - 1
frontend/src/__tests__/components/LinkSpoolModal.test.tsx

@@ -117,7 +117,7 @@ describe('LinkSpoolModal', () => {
   });
 
   describe('linking', () => {
-    it('calls linkSpool on spool click', async () => {
+    it('uses trayUuid when linking if present (Bambu spool path)', async () => {
       render(<LinkSpoolModal {...defaultProps} />);
 
       await waitFor(() => {
@@ -126,6 +126,25 @@ describe('LinkSpoolModal', () => {
 
       fireEvent.click(screen.getByText(/Generic PLA Red/).closest('button')!);
 
+      await waitFor(() => {
+        expect(api.linkSpool).toHaveBeenCalledWith(1, {
+          spoolTag: 'A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4',
+          printerId: 1,
+          amsId: 0,
+          trayId: 0,
+        });
+      });
+    });
+
+    it('falls back to tagUid when trayUuid is missing (generic spool path)', async () => {
+      render(<LinkSpoolModal {...defaultProps} trayUuid="" />);
+
+      await waitFor(() => {
+        expect(screen.getByText(/Generic PLA Red/)).toBeInTheDocument();
+      });
+
+      fireEvent.click(screen.getByText(/Generic PLA Red/).closest('button')!);
+
       await waitFor(() => {
         expect(api.linkSpool).toHaveBeenCalledWith(1, {
           spoolTag: 'ABCD1234',

+ 2 - 1
frontend/src/components/FilamentHoverCard.tsx

@@ -21,6 +21,7 @@ interface SpoolmanConfig {
   onUnlinkSpool?: () => void;
   linkedSpoolId?: number | null; // Spoolman spool ID if this tray is already linked
   spoolmanUrl?: string | null; // Base URL for Spoolman (for "Open in Spoolman" link)
+  syncMode?: string | null; // If auto-sync is enabled, we may want to hide the unlink option for Bambu spools
 }
 
 interface InventoryConfig {
@@ -305,7 +306,7 @@ export function FilamentHoverCard({ data, children, disabled, className = '', sp
                         {t('spoolman.openInSpoolman')}
                       </a>
 
-                      {spoolman.onUnlinkSpool && data.vendor !== 'Bambu Lab' && (
+                      {spoolman.onUnlinkSpool && (data.vendor !== 'Bambu Lab' || spoolman.syncMode === 'manual') && (
                         <button
                           onClick={(e) => {
                             e.stopPropagation();

+ 4 - 4
frontend/src/components/LinkSpoolModal.tsx

@@ -22,7 +22,7 @@ export function LinkSpoolModal({ isOpen, onClose, tagUid, trayUuid, printerId, a
   const queryClient = useQueryClient();
   const { showToast } = useToast();
   const [search, setSearch] = useState('');
-  const spoolTag = tagUid || trayUuid;
+  const spoolTag = trayUuid || tagUid;
 
   const { data: spools, isLoading } = useQuery({
     queryKey: ['unlinked-spools'],
@@ -97,9 +97,9 @@ export function LinkSpoolModal({ isOpen, onClose, tagUid, trayUuid, printerId, a
               className="w-full pl-9 pr-3 py-2 bg-bambu-dark rounded-lg border border-white/10 text-white text-sm placeholder:text-bambu-gray focus:outline-none focus:border-bambu-green"
             />
           </div>
-          {(tagUid || trayUuid) && (
-            <p className="text-xs text-bambu-gray mt-2 font-mono truncate" title={tagUid || trayUuid}>
-              Tag: {tagUid || trayUuid}
+          {(trayUuid || tagUid) && (
+            <p className="text-xs text-bambu-gray mt-2 font-mono truncate" title={trayUuid || tagUid}>
+              Tag: {trayUuid || tagUid}
             </p>
           )}
         </div>

+ 17 - 1
frontend/src/pages/PrintersPage.tsx

@@ -1530,6 +1530,7 @@ function PrinterCard({
   spoolmanEnabled = false,
   linkedSpools,
   spoolmanUrl,
+  spoolmanSyncMode,
   onGetAssignment,
   onUnassignSpool,
   timeFormat = 'system',
@@ -1553,6 +1554,7 @@ function PrinterCard({
   hasUnlinkedSpools?: boolean;
   linkedSpools?: Record<string, LinkedSpoolInfo>;
   spoolmanUrl?: string | null;
+  spoolmanSyncMode?: string | null;
   spoolAssignments?: SpoolAssignment[];
   onGetAssignment?: (printerId: number, amsId: number, trayId: number) => SpoolAssignment | undefined;
   onUnassignSpool?: (printerId: number, amsId: number, trayId: number) => void;
@@ -3176,7 +3178,7 @@ function PrinterCard({
                                 const slotPreset = slotPresets?.[globalTrayId];
 
                                 // Fill level fallback chain: Spoolman → Inventory → AMS remain
-                                const trayTag = (tray?.tag_uid || tray?.tray_uuid || getFallbackSpoolTag(printer.serial_number, ams.id, slotIdx))?.toUpperCase();
+                                const trayTag = (tray?.tray_uuid || tray?.tag_uid || getFallbackSpoolTag(printer.serial_number, ams.id, slotIdx))?.toUpperCase();
                                 const linkedSpool = trayTag ? linkedSpools?.[trayTag] : undefined;
                                 const spoolmanFill = getSpoolmanFillLevel(linkedSpool);
                                 const inventoryAssignment = onGetAssignment?.(printer.id, ams.id, slotIdx);
@@ -3303,6 +3305,7 @@ function PrinterCard({
                                             ? linkedSpools?.[trayTag]?.id
                                             : undefined,
                                           spoolmanUrl,
+                                          syncMode: spoolmanSyncMode,
                                           onLinkSpool: spoolmanEnabled ? () => {
                                             const linkTag = (filamentData.trayUuid || filamentData.tagUid || getFallbackSpoolTag(printer.serial_number, ams.id, slotIdx)).toUpperCase();
                                             setLinkSpoolModal({
@@ -3619,6 +3622,7 @@ function PrinterCard({
                                         ? linkedSpools?.[htTrayTag]?.id
                                         : undefined,
                                       spoolmanUrl,
+                                      syncMode: spoolmanSyncMode,
                                       onLinkSpool: spoolmanEnabled ? () => {
                                         const linkTag = (filamentData.trayUuid || filamentData.tagUid || getFallbackSpoolTag(printer.serial_number, ams.id, htSlotId)).toUpperCase();
                                         setLinkSpoolModal({
@@ -3832,6 +3836,7 @@ function PrinterCard({
                                           ? linkedSpools?.[extTrayTag]?.id
                                           : undefined,
                                         spoolmanUrl,
+                                        syncMode: spoolmanSyncMode,
                                         onLinkSpool: spoolmanEnabled ? () => {
                                           const linkTag = (extFilamentData.trayUuid || extFilamentData.tagUid || getFallbackSpoolTag(printer.serial_number, 255, slotTrayId)).toUpperCase();
                                           setLinkSpoolModal({
@@ -5715,6 +5720,15 @@ export function PrintersPage() {
   });
   const spoolmanEnabled = spoolmanStatus?.enabled && spoolmanStatus?.connected;
 
+  // Fetch Spoolman settings to get sync mode
+  const { data: spoolmanSettings } = useQuery({
+    queryKey: ['spoolman-settings'],
+    queryFn: api.getSpoolmanSettings,
+    enabled: !!spoolmanEnabled,
+    staleTime: 60 * 1000, // 1 minute
+  });
+  const spoolmanSyncMode = spoolmanSettings?.spoolman_sync_mode;
+
   // Fetch unlinked spools to know if link button should be enabled
   const { data: unlinkedSpools } = useQuery({
     queryKey: ['unlinked-spools'],
@@ -6064,6 +6078,7 @@ export function PrintersPage() {
                     hasUnlinkedSpools={hasUnlinkedSpools}
                     linkedSpools={linkedSpools}
                     spoolmanUrl={spoolmanStatus?.url}
+                    spoolmanSyncMode={spoolmanSyncMode}
                     onGetAssignment={getAssignment}
                     onUnassignSpool={(pid, aid, tid) => unassignMutation.mutate({ printerId: pid, amsId: aid, trayId: tid })}
                     timeFormat={settings?.time_format || 'system'}
@@ -6092,6 +6107,7 @@ export function PrintersPage() {
               hasUnlinkedSpools={hasUnlinkedSpools}
               linkedSpools={linkedSpools}
               spoolmanUrl={spoolmanStatus?.url}
+              spoolmanSyncMode={spoolmanSyncMode}
               onGetAssignment={getAssignment}
               onUnassignSpool={(pid, aid, tid) => unassignMutation.mutate({ printerId: pid, amsId: aid, trayId: tid })}
               amsThresholds={settings ? {