From f7994df84f29a566e4228e92884661762d7ae599 Mon Sep 17 00:00:00 2001 From: maziggy Date: Thu, 1 Jan 2026 15:52:43 +0100 Subject: [PATCH] - Added basic printer controls - Reprint/Stop - Pause/Resume - AMS slot re-read --- frontend/src/api/client.ts | 21 ++ frontend/src/pages/PrintersPage.tsx | 446 ++++++++++++++++++++++++---- 2 files changed, 411 insertions(+), 56 deletions(-) diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 435bfaf6d..225743c73 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -1275,6 +1275,27 @@ export const api = { method: 'POST', }), + // Print Control + stopPrint: (printerId: number) => + request<{ success: boolean; message: string }>(`/printers/${printerId}/print/stop`, { + method: 'POST', + }), + pausePrint: (printerId: number) => + request<{ success: boolean; message: string }>(`/printers/${printerId}/print/pause`, { + method: 'POST', + }), + resumePrint: (printerId: number) => + request<{ success: boolean; message: string }>(`/printers/${printerId}/print/resume`, { + method: 'POST', + }), + + // AMS Control + refreshAmsSlot: (printerId: number, amsId: number, slotId: number) => + request<{ success: boolean; message: string }>( + `/printers/${printerId}/ams/${amsId}/slot/${slotId}/refresh`, + { method: 'POST' } + ), + // MQTT Debug Logging enableMQTTLogging: (printerId: number) => request<{ logging_enabled: boolean }>(`/printers/${printerId}/logging/enable`, { diff --git a/frontend/src/pages/PrintersPage.tsx b/frontend/src/pages/PrintersPage.tsx index 06bd9ed06..e0c9ba772 100644 --- a/frontend/src/pages/PrintersPage.tsx +++ b/frontend/src/pages/PrintersPage.tsx @@ -28,6 +28,9 @@ import { Video, Search, Loader2, + Square, + Pause, + Play, } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { api, discoveryApi } from '../api/client'; @@ -41,6 +44,7 @@ import { HMSErrorModal, filterKnownHMSErrors } from '../components/HMSErrorModal import { PrinterQueueWidget } from '../components/PrinterQueueWidget'; import { AMSHistoryModal } from '../components/AMSHistoryModal'; import { FilamentHoverCard, EmptySlotHoverCard } from '../components/FilamentHoverCard'; +import { useToast } from '../contexts/ToastContext'; // Bambu Lab color code mapping (color suffix from tray_id_name -> color name) // tray_id_name format: "A00-Y2" where Y2 is the color code @@ -663,6 +667,7 @@ function PrinterCard({ }) { const queryClient = useQueryClient(); const navigate = useNavigate(); + const { showToast } = useToast(); const [showMenu, setShowMenu] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [deleteArchives, setDeleteArchives] = useState(true); @@ -672,6 +677,9 @@ function PrinterCard({ const [showPowerOnConfirm, setShowPowerOnConfirm] = useState(false); const [showPowerOffConfirm, setShowPowerOffConfirm] = useState(false); const [showHMSModal, setShowHMSModal] = useState(false); + const [showStopConfirm, setShowStopConfirm] = useState(false); + const [showPauseConfirm, setShowPauseConfirm] = useState(false); + const [showResumeConfirm, setShowResumeConfirm] = useState(false); const [amsHistoryModal, setAmsHistoryModal] = useState<{ amsId: number; amsLabel: string; @@ -828,6 +836,127 @@ function PrinterCard({ }, }); + // Print control mutations + const stopPrintMutation = useMutation({ + mutationFn: () => api.stopPrint(printer.id), + onSuccess: () => { + showToast('Print stopped'); + queryClient.invalidateQueries({ queryKey: ['printerStatus', printer.id] }); + }, + onError: (error: Error) => showToast(error.message || 'Failed to stop print', 'error'), + }); + + const pausePrintMutation = useMutation({ + mutationFn: () => api.pausePrint(printer.id), + onSuccess: () => { + showToast('Print paused'); + queryClient.invalidateQueries({ queryKey: ['printerStatus', printer.id] }); + }, + onError: (error: Error) => showToast(error.message || 'Failed to pause print', 'error'), + }); + + const resumePrintMutation = useMutation({ + mutationFn: () => api.resumePrint(printer.id), + onSuccess: () => { + showToast('Print resumed'); + queryClient.invalidateQueries({ queryKey: ['printerStatus', printer.id] }); + }, + onError: (error: Error) => showToast(error.message || 'Failed to resume print', 'error'), + }); + + // State for tracking which AMS slot is being refreshed + const [refreshingSlot, setRefreshingSlot] = useState<{ amsId: number; slotId: number } | null>(null); + // Track if we've seen the printer enter "busy" state (ams_status_main !== 0) + const seenBusyStateRef = useRef(false); + // Fallback timeout ref + const refreshTimeoutRef = useRef | null>(null); + // Minimum display time passed + const minTimePassedRef = useRef(false); + + // AMS slot refresh mutation + const refreshAmsSlotMutation = useMutation({ + mutationFn: ({ amsId, slotId }: { amsId: number; slotId: number }) => + api.refreshAmsSlot(printer.id, amsId, slotId), + onMutate: ({ amsId, slotId }) => { + // Clear any existing timeout + if (refreshTimeoutRef.current) { + clearTimeout(refreshTimeoutRef.current); + } + // Reset state + seenBusyStateRef.current = false; + minTimePassedRef.current = false; + setRefreshingSlot({ amsId, slotId }); + // Minimum display time (2 seconds) + setTimeout(() => { + minTimePassedRef.current = true; + }, 2000); + // Fallback timeout (30 seconds max) + refreshTimeoutRef.current = setTimeout(() => { + setRefreshingSlot(null); + }, 30000); + }, + onSuccess: (data) => { + showToast(data.message || 'RFID re-read initiated'); + }, + onError: (error: Error) => { + showToast(error.message || 'Failed to re-read RFID', 'error'); + if (refreshTimeoutRef.current) { + clearTimeout(refreshTimeoutRef.current); + } + setRefreshingSlot(null); + }, + }); + + // Watch ams_status_main to detect when RFID read completes + // ams_status_main: 0=idle, 2=rfid_identifying + const deferredClearRef = useRef | null>(null); + + useEffect(() => { + if (!refreshingSlot) return; + + const amsStatus = status?.ams_status_main ?? 0; + + // Track when we see non-idle state (printer is working) + if (amsStatus !== 0) { + seenBusyStateRef.current = true; + // Cancel any deferred clear since we're back to busy + if (deferredClearRef.current) { + clearTimeout(deferredClearRef.current); + deferredClearRef.current = null; + } + } + + // When we've seen busy and now idle, clear (with min time check) + if (seenBusyStateRef.current && amsStatus === 0) { + if (minTimePassedRef.current) { + // Min time passed - clear now + if (refreshTimeoutRef.current) { + clearTimeout(refreshTimeoutRef.current); + } + setRefreshingSlot(null); + } else { + // Schedule clear after min time (2 seconds from start) + if (!deferredClearRef.current) { + deferredClearRef.current = setTimeout(() => { + if (refreshTimeoutRef.current) { + clearTimeout(refreshTimeoutRef.current); + } + setRefreshingSlot(null); + }, 2000); + } + } + } + + return () => { + if (deferredClearRef.current) { + clearTimeout(deferredClearRef.current); + } + }; + }, [status?.ams_status_main, refreshingSlot]); + + // State for AMS slot menu + const [amsSlotMenu, setAmsSlotMenu] = useState<{ amsId: number; slotId: number } | null>(null); + if (shouldHide) { return null; } @@ -1195,50 +1324,102 @@ function PrinterCard({ )} - {/* Temperatures */} + {/* Temperatures + Print Controls */} {status.temperatures && viewMode === 'expanded' && (() => { // Use actual heater states from MQTT stream const nozzleHeating = status.temperatures.nozzle_heating || status.temperatures.nozzle_2_heating || false; const bedHeating = status.temperatures.bed_heating || false; const chamberHeating = status.temperatures.chamber_heating || false; + // Determine print state for control buttons + const isRunning = status.state === 'RUNNING'; + const isPaused = status.state === 'PAUSED' || status.state === 'PAUSE'; + const isPrinting = isRunning || isPaused; + const isControlBusy = stopPrintMutation.isPending || pausePrintMutation.isPending || resumePrintMutation.isPending; + return ( -
- {/* Nozzle temp - combined for dual nozzle */} -
- - {status.temperatures.nozzle_2 !== undefined ? ( - <> -

Left / Right

-

- {Math.round(status.temperatures.nozzle || 0)}°C / {Math.round(status.temperatures.nozzle_2 || 0)}°C -

- - ) : ( - <> -

Nozzle

-

- {Math.round(status.temperatures.nozzle || 0)}°C -

- - )} -
-
- -

Bed

-

- {Math.round(status.temperatures.bed || 0)}°C -

-
- {status.temperatures.chamber !== undefined && ( +
+ {/* Temperature cards */} +
+ {/* Nozzle temp - combined for dual nozzle */}
- -

Chamber

-

- {Math.round(status.temperatures.chamber || 0)}°C + + {status.temperatures.nozzle_2 !== undefined ? ( + <> +

L / R

+

+ {Math.round(status.temperatures.nozzle || 0)}° / {Math.round(status.temperatures.nozzle_2 || 0)}° +

+ + ) : ( + <> +

Nozzle

+

+ {Math.round(status.temperatures.nozzle || 0)}°C +

+ + )} +
+
+ +

Bed

+

+ {Math.round(status.temperatures.bed || 0)}°C

- )} + {status.temperatures.chamber !== undefined ? ( +
+ +

Chamber

+

+ {Math.round(status.temperatures.chamber || 0)}°C +

+
+ ) : ( +
/* Empty placeholder to maintain grid */ + )} +
+ + {/* Print control buttons */} +
+ {/* Stop button - visible when printing/paused */} + + + {/* Pause/Resume button */} + +
); })()} @@ -1340,9 +1521,14 @@ function PrinterCard({ fillLevel: hasFillLevel ? tray.remain : null, } : null; - const slotContent = ( + // Check if this specific slot is being refreshed + const isRefreshing = refreshingSlot?.amsId === ams.id && + refreshingSlot?.slotId === slotIdx; + + // Slot visual content (goes inside hover card) + const slotVisual = (
); - return filamentData ? ( - - {slotContent} - - ) : ( - - {slotContent} - + // Wrapper with menu button, dropdown, and loading overlay (outside hover card) + return ( +
+ {/* Loading overlay during RFID re-read */} + {isRefreshing && ( +
+ +
+ )} + {/* Menu button - appears on hover, hidden when printer busy */} + {status?.state !== 'RUNNING' && ( + + )} + {/* Dropdown menu */} + {status?.state !== 'RUNNING' && amsSlotMenu?.amsId === ams.id && amsSlotMenu?.slotId === slotIdx && ( +
+ +
+ )} + {/* Hover card wraps only the visual content */} + {filamentData ? ( + + {slotVisual} + + ) : ( + + {slotVisual} + + )} +
); })}
@@ -1419,9 +1651,15 @@ function PrinterCard({ fillLevel: hasFillLevel ? tray.remain : null, } : null; - const slotContent = ( + const htSlotId = tray?.id ?? 0; + // Check if this specific slot is being refreshed + const isHtRefreshing = refreshingSlot?.amsId === ams.id && + refreshingSlot?.slotId === htSlotId; + + // Slot visual content (goes inside hover card) + const slotVisual = (
{/* Row 2: Slot (left) + Stats (right stacked) */}
- {/* Slot - takes remaining width */} - {filamentData ? ( - - {slotContent} - - ) : ( - - {slotContent} - - )} + {/* Slot wrapper with menu button, dropdown, and loading overlay */} +
+ {/* Loading overlay during RFID re-read */} + {isHtRefreshing && ( +
+ +
+ )} + {/* Menu button - appears on hover, hidden when printer busy */} + {status?.state !== 'RUNNING' && ( + + )} + {/* Dropdown menu */} + {status?.state !== 'RUNNING' && amsSlotMenu?.amsId === ams.id && amsSlotMenu?.slotId === htSlotId && ( +
+ +
+ )} + {/* Hover card wraps only the visual content */} + {filamentData ? ( + + {slotVisual} + + ) : ( + + {slotVisual} + + )} +
{/* Stats stacked vertically: Temp on top, Humidity below */} {(ams.humidity != null || ams.temp != null) && (
@@ -1749,6 +2030,51 @@ function PrinterCard({ /> )} + {/* Stop Print Confirmation */} + {showStopConfirm && ( + { + stopPrintMutation.mutate(); + setShowStopConfirm(false); + }} + onCancel={() => setShowStopConfirm(false)} + /> + )} + + {/* Pause Print Confirmation */} + {showPauseConfirm && ( + { + pausePrintMutation.mutate(); + setShowPauseConfirm(false); + }} + onCancel={() => setShowPauseConfirm(false)} + /> + )} + + {/* Resume Print Confirmation */} + {showResumeConfirm && ( + { + resumePrintMutation.mutate(); + setShowResumeConfirm(false); + }} + onCancel={() => setShowResumeConfirm(false)} + /> + )} + {/* HMS Error Modal */} {showHMSModal && ( setShowEditModal(false)} /> )} + + {/* AMS Slot Menu Backdrop - closes menu when clicking outside */} + {amsSlotMenu && ( +
setAmsSlotMenu(null)} + /> + )} ); }