diff --git a/src/components/ProgressMonitor.tsx b/src/components/ProgressMonitor.tsx index 0f1d3ea..fe210ca 100644 --- a/src/components/ProgressMonitor.tsx +++ b/src/components/ProgressMonitor.tsx @@ -21,6 +21,7 @@ import { canResumeSewing, getStateVisualInfo, } from "../utils/machineStateHelpers"; +import { calculatePatternTime } from "../utils/timeCalculation"; export function ProgressMonitor() { // Machine store @@ -56,8 +57,15 @@ export function ProgressMonitor() { const stateVisual = getStateVisualInfo(machineStatus); - const progressPercent = patternInfo - ? ((sewingProgress?.currentStitch || 0) / patternInfo.totalStitches) * 100 + // Use PEN stitch count as fallback when machine reports 0 total stitches + const totalStitches = patternInfo + ? (patternInfo.totalStitches === 0 && pesData?.penStitches + ? pesData.penStitches.stitches.length + : patternInfo.totalStitches) + : 0; + + const progressPercent = totalStitches > 0 + ? ((sewingProgress?.currentStitch || 0) / totalStitches) * 100 : 0; // Calculate color block information from decoded penStitches @@ -102,6 +110,15 @@ export function ProgressMonitor() { currentStitch >= block.startStitch && currentStitch < block.endStitch, ); + // Calculate time based on color blocks (matches Brother app calculation) + const { totalMinutes, elapsedMinutes } = useMemo(() => { + if (colorBlocks.length === 0) { + return { totalMinutes: 0, elapsedMinutes: 0 }; + } + const result = calculatePatternTime(colorBlocks, currentStitch); + return { totalMinutes: result.totalMinutes, elapsedMinutes: result.elapsedMinutes }; + }, [colorBlocks, currentStitch]); + // Auto-scroll to current block useEffect(() => { if (currentBlockRef.current) { @@ -173,16 +190,15 @@ export function ProgressMonitor() { Total Stitches - {patternInfo.totalStitches.toLocaleString()} + {totalStitches.toLocaleString()}
- Est. Time + Total Time - {Math.floor(patternInfo.totalTime / 60)}: - {String(patternInfo.totalTime % 60).padStart(2, "0")} + {totalMinutes} min
@@ -213,16 +229,15 @@ export function ProgressMonitor() { {sewingProgress.currentStitch.toLocaleString()} /{" "} - {patternInfo?.totalStitches.toLocaleString() || 0} + {totalStitches.toLocaleString()}
- Time Elapsed + Time - {Math.floor(sewingProgress.currentTime / 60)}: - {String(sewingProgress.currentTime % 60).padStart(2, "0")} + {elapsedMinutes} / {totalMinutes} min
diff --git a/src/hooks/useBrotherMachine.ts b/src/hooks/useBrotherMachine.ts deleted file mode 100644 index e0a0648..0000000 --- a/src/hooks/useBrotherMachine.ts +++ /dev/null @@ -1,457 +0,0 @@ -import { useState, useCallback, useEffect } from "react"; -import { BrotherPP1Service, BluetoothPairingError } from "../services/BrotherPP1Service"; -import type { - MachineInfo, - PatternInfo, - SewingProgress, -} from "../types/machine"; -import { MachineStatus, MachineStatusNames } from "../types/machine"; -import { - uuidToString, -} from "../services/PatternCacheService"; -import type { IStorageService } from "../platform/interfaces/IStorageService"; -import { createStorageService } from "../platform"; -import type { PesPatternData } from "../formats/import/pesImporter"; -import { SewingMachineError } from "../utils/errorCodeHelpers"; - -export function useBrotherMachine() { - const [service] = useState(() => new BrotherPP1Service()); - const [storageService] = useState(() => createStorageService()); - const [isConnected, setIsConnected] = useState(false); - const [machineInfo, setMachineInfo] = useState(null); - const [machineStatus, setMachineStatus] = useState( - MachineStatus.None, - ); - const [machineError, setMachineError] = useState(SewingMachineError.None); - const [patternInfo, setPatternInfo] = useState(null); - const [sewingProgress, setSewingProgress] = useState( - null, - ); - const [uploadProgress, setUploadProgress] = useState(0); - const [error, setError] = useState(null); - const [isPairingError, setIsPairingError] = useState(false); - const [isCommunicating, setIsCommunicating] = useState(false); - const [isUploading, setIsUploading] = useState(false); - const [isDeleting, setIsDeleting] = useState(false); - const [resumeAvailable, setResumeAvailable] = useState(false); - const [resumeFileName, setResumeFileName] = useState(null); - const [resumedPattern, setResumedPattern] = useState<{ pesData: PesPatternData; patternOffset?: { x: number; y: number } } | null>( - null, - ); - - // Subscribe to service communication state - useEffect(() => { - const unsubscribe = service.onCommunicationChange(setIsCommunicating); - return unsubscribe; - }, [service]); - - // Subscribe to disconnect events - useEffect(() => { - const unsubscribe = service.onDisconnect(() => { - console.log('[useBrotherMachine] Device disconnected'); - setIsConnected(false); - setMachineInfo(null); - setMachineStatus(MachineStatus.None); - setMachineError(SewingMachineError.None); - setPatternInfo(null); - setSewingProgress(null); - setError('Device disconnected'); - setIsPairingError(false); - }); - return unsubscribe; - }, [service]); - - // Define checkResume first (before connect uses it) - const checkResume = useCallback(async (): Promise => { - try { - console.log("[Resume] Checking for cached pattern..."); - - // Get UUID from machine - const machineUuid = await service.getPatternUUID(); - - console.log( - "[Resume] Machine UUID:", - machineUuid ? uuidToString(machineUuid) : "none", - ); - - if (!machineUuid) { - console.log("[Resume] No pattern loaded on machine"); - setResumeAvailable(false); - setResumeFileName(null); - return null; - } - - // Check if we have this pattern cached - const uuidStr = uuidToString(machineUuid); - const cached = await storageService.getPatternByUUID(uuidStr); - - if (cached) { - console.log("[Resume] Pattern found in cache:", cached.fileName, "Offset:", cached.patternOffset); - console.log("[Resume] Auto-loading cached pattern..."); - setResumeAvailable(true); - setResumeFileName(cached.fileName); - setResumedPattern({ pesData: cached.pesData, patternOffset: cached.patternOffset }); - - // Fetch pattern info from machine - try { - const info = await service.getPatternInfo(); - setPatternInfo(info); - console.log("[Resume] Pattern info loaded from machine"); - } catch (err) { - console.error("[Resume] Failed to load pattern info:", err); - } - - // Return the cached pattern data to be loaded - return cached.pesData; - } else { - console.log("[Resume] Pattern on machine not found in cache"); - setResumeAvailable(false); - setResumeFileName(null); - return null; - } - } catch (err) { - console.error("[Resume] Failed to check resume:", err); - setResumeAvailable(false); - setResumeFileName(null); - return null; - } - }, [service, storageService]); - - const connect = useCallback(async () => { - try { - setError(null); - setIsPairingError(false); - await service.connect(); - setIsConnected(true); - - // Fetch initial machine info and status - const info = await service.getMachineInfo(); - setMachineInfo(info); - - const state = await service.getMachineState(); - setMachineStatus(state.status); - setMachineError(state.error); - - // Check for resume possibility - await checkResume(); - } catch (err) { - console.log(err); - const isPairing = err instanceof BluetoothPairingError; - setIsPairingError(isPairing); - setError(err instanceof Error ? err.message : "Failed to connect"); - setIsConnected(false); - } - }, [service, checkResume]); - - const disconnect = useCallback(async () => { - try { - await service.disconnect(); - setIsConnected(false); - setMachineInfo(null); - setMachineStatus(MachineStatus.None); - setPatternInfo(null); - setSewingProgress(null); - setError(null); - setMachineError(SewingMachineError.None); - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to disconnect"); - } - }, [service]); - - const refreshStatus = useCallback(async () => { - if (!isConnected) return; - - try { - const state = await service.getMachineState(); - setMachineStatus(state.status); - setMachineError(state.error); - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to get status"); - } - }, [service, isConnected]); - - const refreshPatternInfo = useCallback(async () => { - if (!isConnected) return; - - try { - const info = await service.getPatternInfo(); - setPatternInfo(info); - } catch (err) { - setError( - err instanceof Error ? err.message : "Failed to get pattern info", - ); - } - }, [service, isConnected]); - - const refreshProgress = useCallback(async () => { - if (!isConnected) return; - - try { - const progress = await service.getSewingProgress(); - setSewingProgress(progress); - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to get progress"); - } - }, [service, isConnected]); - - const refreshServiceCount = useCallback(async () => { - if (!isConnected || !machineInfo) return; - - try { - const counts = await service.getServiceCount(); - setMachineInfo({ - ...machineInfo, - serviceCount: counts.serviceCount, - totalCount: counts.totalCount, - }); - } catch (err) { - // Don't set error for service count failures - it's not critical - console.warn("Failed to get service count:", err); - } - }, [service, isConnected, machineInfo]); - - const loadCachedPattern = - useCallback(async (): Promise<{ pesData: PesPatternData; patternOffset?: { x: number; y: number } } | null> => { - if (!resumeAvailable) return null; - - try { - const machineUuid = await service.getPatternUUID(); - if (!machineUuid) return null; - - const uuidStr = uuidToString(machineUuid); - const cached = await storageService.getPatternByUUID(uuidStr); - - if (cached) { - console.log("[Resume] Loading cached pattern:", cached.fileName, "Offset:", cached.patternOffset); - // Refresh pattern info from machine - await refreshPatternInfo(); - return { pesData: cached.pesData, patternOffset: cached.patternOffset }; - } - - return null; - } catch (err) { - setError( - err instanceof Error ? err.message : "Failed to load cached pattern", - ); - return null; - } - }, [service, storageService, resumeAvailable, refreshPatternInfo]); - - const uploadPattern = useCallback( - async (penData: Uint8Array, pesData: PesPatternData, fileName: string, patternOffset?: { x: number; y: number }) => { - if (!isConnected) { - setError("Not connected to machine"); - return; - } - - try { - setError(null); - setUploadProgress(0); - setIsUploading(true); // Set loading state immediately - const uuid = await service.uploadPattern( - penData, - (progress) => { - setUploadProgress(progress); - }, - pesData.bounds, - patternOffset, - ); - setUploadProgress(100); - - // Cache the pattern with its UUID and offset - const uuidStr = uuidToString(uuid); - storageService.savePattern(uuidStr, pesData, fileName, patternOffset); - console.log("[Cache] Saved pattern:", fileName, "with UUID:", uuidStr, "Offset:", patternOffset); - - // Clear resume state since we just uploaded - setResumeAvailable(false); - setResumeFileName(null); - - // Refresh status after upload - // NOTE: We don't call refreshPatternInfo() here because the machine hasn't - // finished processing the pattern yet. Pattern info (stitch count, time estimate) - // is only available AFTER startMaskTrace() is called. - await refreshStatus(); - } catch (err) { - setError( - err instanceof Error ? err.message : "Failed to upload pattern", - ); - } finally { - setIsUploading(false); // Clear loading state - } - }, - [service, storageService, isConnected, refreshStatus], - ); - - const startMaskTrace = useCallback(async () => { - if (!isConnected) return; - - try { - setError(null); - await service.startMaskTrace(); - - // After mask trace, poll machine status a few times to ensure it's ready - // The machine needs time to process the pattern before pattern info is accurate - console.log('[MaskTrace] Polling machine status...'); - for (let i = 0; i < 3; i++) { - await new Promise(resolve => setTimeout(resolve, 200)); - await refreshStatus(); - } - - // Now the machine should have accurate pattern info - console.log('[MaskTrace] Refreshing pattern info...'); - await refreshPatternInfo(); - } catch (err) { - setError( - err instanceof Error ? err.message : "Failed to start mask trace", - ); - } - }, [service, isConnected, refreshStatus, refreshPatternInfo]); - - const startSewing = useCallback(async () => { - if (!isConnected) return; - - try { - setError(null); - await service.startSewing(); - await refreshStatus(); - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to start sewing"); - } - }, [service, isConnected, refreshStatus]); - - const resumeSewing = useCallback(async () => { - if (!isConnected) return; - - try { - setError(null); - await service.resumeSewing(); - await refreshStatus(); - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to resume sewing"); - } - }, [service, isConnected, refreshStatus]); - - const deletePattern = useCallback(async () => { - if (!isConnected) return; - - try { - setError(null); - setIsDeleting(true); // Set loading state immediately - - // Delete pattern from cache to prevent auto-resume - try { - const machineUuid = await service.getPatternUUID(); - if (machineUuid) { - const uuidStr = uuidToString(machineUuid); - await storageService.deletePattern(uuidStr); - console.log("[Cache] Deleted pattern with UUID:", uuidStr); - } - } catch (err) { - console.warn("[Cache] Failed to get UUID for cache deletion:", err); - } - - await service.deletePattern(); - - // Clear machine-related state but keep pattern data in UI for re-editing - setPatternInfo(null); - setSewingProgress(null); - setUploadProgress(0); // Reset upload progress to allow new uploads - setResumeAvailable(false); - setResumeFileName(null); - // NOTE: We intentionally DON'T clear setResumedPattern(null) - // so the pattern remains visible in the canvas for re-editing - // However, we DO need to preserve pesData in App.tsx for re-upload - - await refreshStatus(); - } catch (err) { - setError(err instanceof Error ? err.message : "Failed to delete pattern"); - } finally { - setIsDeleting(false); // Clear loading state - } - }, [service, storageService, isConnected, refreshStatus]); - - // Periodic status monitoring when connected - useEffect(() => { - if (!isConnected) { - return; - } - - // Determine polling interval based on machine status - let pollInterval = 2000; // Default: 2 seconds for idle states - - // Fast polling for active states - if ( - machineStatus === MachineStatus.SEWING || - machineStatus === MachineStatus.MASK_TRACING || - machineStatus === MachineStatus.SEWING_DATA_RECEIVE - ) { - pollInterval = 500; // 500ms for active operations - } else if ( - machineStatus === MachineStatus.COLOR_CHANGE_WAIT || - machineStatus === MachineStatus.MASK_TRACE_LOCK_WAIT || - machineStatus === MachineStatus.SEWING_WAIT - ) { - pollInterval = 1000; // 1 second for waiting states - } - - const interval = setInterval(async () => { - await refreshStatus(); - - // Refresh progress during sewing - if (machineStatus === MachineStatus.SEWING) { - await refreshProgress(); - } - }, pollInterval); - - // Separate interval for service count (slower update rate - every 10 seconds) - const serviceCountInterval = setInterval(async () => { - await refreshServiceCount(); - }, 10000); - - return () => { - clearInterval(interval); - clearInterval(serviceCountInterval); - }; - }, [isConnected, machineStatus, refreshStatus, refreshProgress, refreshServiceCount]); - - // Refresh pattern info when status changes to SEWING_WAIT - // (indicates pattern was just uploaded or is ready) - useEffect(() => { - if (!isConnected) return; - - if (machineStatus === MachineStatus.SEWING_WAIT && !patternInfo) { - refreshPatternInfo(); - } - }, [isConnected, machineStatus, patternInfo, refreshPatternInfo]); - - return { - isConnected, - machineInfo, - machineStatus, - machineStatusName: MachineStatusNames[machineStatus] || "Unknown", - machineError, - patternInfo, - sewingProgress, - uploadProgress, - error, - isPairingError, - isPolling: isCommunicating, - isUploading, - isDeleting, - resumeAvailable, - resumeFileName, - resumedPattern, - connect, - disconnect, - refreshStatus, - refreshPatternInfo, - uploadPattern, - startMaskTrace, - startSewing, - resumeSewing, - deletePattern, - checkResume, - loadCachedPattern, - }; -} diff --git a/src/stores/useMachineStore.ts b/src/stores/useMachineStore.ts index 5cd80c0..b33a3ee 100644 --- a/src/stores/useMachineStore.ts +++ b/src/stores/useMachineStore.ts @@ -1,16 +1,19 @@ -import { create } from 'zustand'; -import { BrotherPP1Service, BluetoothPairingError } from '../services/BrotherPP1Service'; +import { create } from "zustand"; +import { + BrotherPP1Service, + BluetoothPairingError, +} from "../services/BrotherPP1Service"; import type { MachineInfo, PatternInfo, SewingProgress, -} from '../types/machine'; -import { MachineStatus, MachineStatusNames } from '../types/machine'; -import { SewingMachineError } from '../utils/errorCodeHelpers'; -import { uuidToString } from '../services/PatternCacheService'; -import { createStorageService } from '../platform'; -import type { IStorageService } from '../platform/interfaces/IStorageService'; -import type { PesPatternData } from '../formats/import/pesImporter'; +} from "../types/machine"; +import { MachineStatus, MachineStatusNames } from "../types/machine"; +import { SewingMachineError } from "../utils/errorCodeHelpers"; +import { uuidToString } from "../services/PatternCacheService"; +import { createStorageService } from "../platform"; +import type { IStorageService } from "../platform/interfaces/IStorageService"; +import type { PesPatternData } from "../formats/import/pesImporter"; interface MachineState { // Service instances @@ -37,7 +40,10 @@ interface MachineState { // Resume state resumeAvailable: boolean; resumeFileName: string | null; - resumedPattern: { pesData: PesPatternData; patternOffset?: { x: number; y: number } } | null; + resumedPattern: { + pesData: PesPatternData; + patternOffset?: { x: number; y: number }; + } | null; // Error state error: string | null; @@ -62,14 +68,17 @@ interface MachineState { penData: Uint8Array, pesData: PesPatternData, fileName: string, - patternOffset?: { x: number; y: number } + patternOffset?: { x: number; y: number }, ) => Promise; startMaskTrace: () => Promise; startSewing: () => Promise; resumeSewing: () => Promise; deletePattern: () => Promise; checkResume: () => Promise; - loadCachedPattern: () => Promise<{ pesData: PesPatternData; patternOffset?: { x: number; y: number } } | null>; + loadCachedPattern: () => Promise<{ + pesData: PesPatternData; + patternOffset?: { x: number; y: number }; + } | null>; // Internal methods _setupSubscriptions: () => void; @@ -84,7 +93,7 @@ export const useMachineStore = create((set, get) => ({ isConnected: false, machineInfo: null, machineStatus: MachineStatus.None, - machineStatusName: MachineStatusNames[MachineStatus.None] || 'Unknown', + machineStatusName: MachineStatusNames[MachineStatus.None] || "Unknown", machineError: SewingMachineError.None, patternInfo: null, sewingProgress: null, @@ -104,16 +113,16 @@ export const useMachineStore = create((set, get) => ({ checkResume: async (): Promise => { try { const { service, storageService } = get(); - console.log('[Resume] Checking for cached pattern...'); + console.log("[Resume] Checking for cached pattern..."); const machineUuid = await service.getPatternUUID(); console.log( - '[Resume] Machine UUID:', - machineUuid ? uuidToString(machineUuid) : 'none', + "[Resume] Machine UUID:", + machineUuid ? uuidToString(machineUuid) : "none", ); if (!machineUuid) { - console.log('[Resume] No pattern loaded on machine'); + console.log("[Resume] No pattern loaded on machine"); set({ resumeAvailable: false, resumeFileName: null }); return null; } @@ -122,31 +131,39 @@ export const useMachineStore = create((set, get) => ({ const cached = await storageService.getPatternByUUID(uuidStr); if (cached) { - console.log('[Resume] Pattern found in cache:', cached.fileName, 'Offset:', cached.patternOffset); - console.log('[Resume] Auto-loading cached pattern...'); + console.log( + "[Resume] Pattern found in cache:", + cached.fileName, + "Offset:", + cached.patternOffset, + ); + console.log("[Resume] Auto-loading cached pattern..."); set({ resumeAvailable: true, resumeFileName: cached.fileName, - resumedPattern: { pesData: cached.pesData, patternOffset: cached.patternOffset }, + resumedPattern: { + pesData: cached.pesData, + patternOffset: cached.patternOffset, + }, }); // Fetch pattern info from machine try { const info = await service.getPatternInfo(); set({ patternInfo: info }); - console.log('[Resume] Pattern info loaded from machine'); + console.log("[Resume] Pattern info loaded from machine"); } catch (err) { - console.error('[Resume] Failed to load pattern info:', err); + console.error("[Resume] Failed to load pattern info:", err); } return cached.pesData; } else { - console.log('[Resume] Pattern on machine not found in cache'); + console.log("[Resume] Pattern on machine not found in cache"); set({ resumeAvailable: false, resumeFileName: null }); return null; } } catch (err) { - console.error('[Resume] Failed to check resume:', err); + console.error("[Resume] Failed to check resume:", err); set({ resumeAvailable: false, resumeFileName: null }); return null; } @@ -168,7 +185,7 @@ export const useMachineStore = create((set, get) => ({ set({ machineInfo: info, machineStatus: state.status, - machineStatusName: MachineStatusNames[state.status] || 'Unknown', + machineStatusName: MachineStatusNames[state.status] || "Unknown", machineError: state.error, }); @@ -182,7 +199,7 @@ export const useMachineStore = create((set, get) => ({ const isPairing = err instanceof BluetoothPairingError; set({ isPairingError: isPairing, - error: err instanceof Error ? err.message : 'Failed to connect', + error: err instanceof Error ? err.message : "Failed to connect", isConnected: false, }); } @@ -199,7 +216,7 @@ export const useMachineStore = create((set, get) => ({ isConnected: false, machineInfo: null, machineStatus: MachineStatus.None, - machineStatusName: MachineStatusNames[MachineStatus.None] || 'Unknown', + machineStatusName: MachineStatusNames[MachineStatus.None] || "Unknown", patternInfo: null, sewingProgress: null, error: null, @@ -207,7 +224,7 @@ export const useMachineStore = create((set, get) => ({ }); } catch (err) { set({ - error: err instanceof Error ? err.message : 'Failed to disconnect', + error: err instanceof Error ? err.message : "Failed to disconnect", }); } }, @@ -221,12 +238,12 @@ export const useMachineStore = create((set, get) => ({ const state = await service.getMachineState(); set({ machineStatus: state.status, - machineStatusName: MachineStatusNames[state.status] || 'Unknown', + machineStatusName: MachineStatusNames[state.status] || "Unknown", machineError: state.error, }); } catch (err) { set({ - error: err instanceof Error ? err.message : 'Failed to get status', + error: err instanceof Error ? err.message : "Failed to get status", }); } }, @@ -241,7 +258,8 @@ export const useMachineStore = create((set, get) => ({ set({ patternInfo: info }); } catch (err) { set({ - error: err instanceof Error ? err.message : 'Failed to get pattern info', + error: + err instanceof Error ? err.message : "Failed to get pattern info", }); } }, @@ -256,7 +274,7 @@ export const useMachineStore = create((set, get) => ({ set({ sewingProgress: progress }); } catch (err) { set({ - error: err instanceof Error ? err.message : 'Failed to get progress', + error: err instanceof Error ? err.message : "Failed to get progress", }); } }, @@ -276,7 +294,7 @@ export const useMachineStore = create((set, get) => ({ }, }); } catch (err) { - console.warn('Failed to get service count:', err); + console.warn("Failed to get service count:", err); } }, @@ -285,11 +303,17 @@ export const useMachineStore = create((set, get) => ({ penData: Uint8Array, pesData: PesPatternData, fileName: string, - patternOffset?: { x: number; y: number } + patternOffset?: { x: number; y: number }, ) => { - const { isConnected, service, storageService, refreshStatus, refreshPatternInfo } = get(); + const { + isConnected, + service, + storageService, + refreshStatus, + refreshPatternInfo, + } = get(); if (!isConnected) { - set({ error: 'Not connected to machine' }); + set({ error: "Not connected to machine" }); return; } @@ -310,7 +334,14 @@ export const useMachineStore = create((set, get) => ({ // Cache the pattern with its UUID and offset const uuidStr = uuidToString(uuid); storageService.savePattern(uuidStr, pesData, fileName, patternOffset); - console.log('[Cache] Saved pattern:', fileName, 'with UUID:', uuidStr, 'Offset:', patternOffset); + console.log( + "[Cache] Saved pattern:", + fileName, + "with UUID:", + uuidStr, + "Offset:", + patternOffset, + ); // Clear resume state since we just uploaded set({ @@ -323,7 +354,7 @@ export const useMachineStore = create((set, get) => ({ await refreshPatternInfo(); } catch (err) { set({ - error: err instanceof Error ? err.message : 'Failed to upload pattern', + error: err instanceof Error ? err.message : "Failed to upload pattern", }); } finally { set({ isUploading: false }); @@ -341,7 +372,8 @@ export const useMachineStore = create((set, get) => ({ await refreshStatus(); } catch (err) { set({ - error: err instanceof Error ? err.message : 'Failed to start mask trace', + error: + err instanceof Error ? err.message : "Failed to start mask trace", }); } }, @@ -357,7 +389,7 @@ export const useMachineStore = create((set, get) => ({ await refreshStatus(); } catch (err) { set({ - error: err instanceof Error ? err.message : 'Failed to start sewing', + error: err instanceof Error ? err.message : "Failed to start sewing", }); } }, @@ -373,7 +405,7 @@ export const useMachineStore = create((set, get) => ({ await refreshStatus(); } catch (err) { set({ - error: err instanceof Error ? err.message : 'Failed to resume sewing', + error: err instanceof Error ? err.message : "Failed to resume sewing", }); } }, @@ -392,10 +424,10 @@ export const useMachineStore = create((set, get) => ({ if (machineUuid) { const uuidStr = uuidToString(machineUuid); await storageService.deletePattern(uuidStr); - console.log('[Cache] Deleted pattern with UUID:', uuidStr); + console.log("[Cache] Deleted pattern with UUID:", uuidStr); } } catch (err) { - console.warn('[Cache] Failed to get UUID for cache deletion:', err); + console.warn("[Cache] Failed to get UUID for cache deletion:", err); } await service.deletePattern(); @@ -412,7 +444,7 @@ export const useMachineStore = create((set, get) => ({ await refreshStatus(); } catch (err) { set({ - error: err instanceof Error ? err.message : 'Failed to delete pattern', + error: err instanceof Error ? err.message : "Failed to delete pattern", }); } finally { set({ isDeleting: false }); @@ -420,8 +452,12 @@ export const useMachineStore = create((set, get) => ({ }, // Load cached pattern - loadCachedPattern: async (): Promise<{ pesData: PesPatternData; patternOffset?: { x: number; y: number } } | null> => { - const { resumeAvailable, service, storageService, refreshPatternInfo } = get(); + loadCachedPattern: async (): Promise<{ + pesData: PesPatternData; + patternOffset?: { x: number; y: number }; + } | null> => { + const { resumeAvailable, service, storageService, refreshPatternInfo } = + get(); if (!resumeAvailable) return null; try { @@ -432,7 +468,12 @@ export const useMachineStore = create((set, get) => ({ const cached = await storageService.getPatternByUUID(uuidStr); if (cached) { - console.log('[Resume] Loading cached pattern:', cached.fileName, 'Offset:', cached.patternOffset); + console.log( + "[Resume] Loading cached pattern:", + cached.fileName, + "Offset:", + cached.patternOffset, + ); await refreshPatternInfo(); return { pesData: cached.pesData, patternOffset: cached.patternOffset }; } @@ -440,7 +481,8 @@ export const useMachineStore = create((set, get) => ({ return null; } catch (err) { set({ - error: err instanceof Error ? err.message : 'Failed to load cached pattern', + error: + err instanceof Error ? err.message : "Failed to load cached pattern", }); return null; } @@ -457,17 +499,17 @@ export const useMachineStore = create((set, get) => ({ // Subscribe to disconnect events service.onDisconnect(() => { - console.log('[useMachineStore] Device disconnected'); + console.log("[useMachineStore] Device disconnected"); get()._stopPolling(); set({ isConnected: false, machineInfo: null, machineStatus: MachineStatus.None, - machineStatusName: MachineStatusNames[MachineStatus.None] || 'Unknown', + machineStatusName: MachineStatusNames[MachineStatus.None] || "Unknown", machineError: SewingMachineError.None, patternInfo: null, sewingProgress: null, - error: 'Device disconnected', + error: "Device disconnected", isPairingError: false, }); }); @@ -475,7 +517,13 @@ export const useMachineStore = create((set, get) => ({ // Start polling for status updates _startPolling: () => { - const { _stopPolling, refreshStatus, refreshProgress, refreshServiceCount } = get(); + const { + _stopPolling, + refreshStatus, + refreshProgress, + refreshServiceCount, + refreshPatternInfo, + } = get(); // Stop any existing polling _stopPolling(); @@ -510,6 +558,11 @@ export const useMachineStore = create((set, get) => ({ await refreshProgress(); } + // follows the apps logic: + if (get().resumeAvailable && get().patternInfo?.totalStitches == 0) { + await refreshPatternInfo(); + } + // Schedule next poll with updated interval const newInterval = getPollInterval(); const pollIntervalId = setTimeout(poll, newInterval); @@ -546,11 +599,18 @@ export const useMachineStore = create((set, get) => ({ useMachineStore.getState()._setupSubscriptions(); // Selector hooks for common use cases -export const useIsConnected = () => useMachineStore((state) => state.isConnected); -export const useMachineInfo = () => useMachineStore((state) => state.machineInfo); -export const useMachineStatus = () => useMachineStore((state) => state.machineStatus); -export const useMachineError = () => useMachineStore((state) => state.machineError); -export const usePatternInfo = () => useMachineStore((state) => state.patternInfo); -export const useSewingProgress = () => useMachineStore((state) => state.sewingProgress); +export const useIsConnected = () => + useMachineStore((state) => state.isConnected); +export const useMachineInfo = () => + useMachineStore((state) => state.machineInfo); +export const useMachineStatus = () => + useMachineStore((state) => state.machineStatus); +export const useMachineError = () => + useMachineStore((state) => state.machineError); +export const usePatternInfo = () => + useMachineStore((state) => state.patternInfo); +export const useSewingProgress = () => + useMachineStore((state) => state.sewingProgress); // Derived state: pattern is uploaded if machine has pattern info -export const usePatternUploaded = () => useMachineStore((state) => state.patternInfo !== null); +export const usePatternUploaded = () => + useMachineStore((state) => state.patternInfo !== null); diff --git a/src/utils/timeCalculation.ts b/src/utils/timeCalculation.ts new file mode 100644 index 0000000..e85b8e4 --- /dev/null +++ b/src/utils/timeCalculation.ts @@ -0,0 +1,67 @@ +/** + * Convert stitch count to minutes using Brother PP1 timing formula + * Formula: ((pointCount - 1) * 150 + 3000) / 60000 + * - 150ms per stitch + * - 3000ms startup time + * - Result in minutes (rounded up) + */ +export function convertStitchesToMinutes(stitchCount: number): number { + if (stitchCount <= 1) return 0; + + const timeMs = (stitchCount - 1) * 150 + 3000; + const timeMin = Math.ceil(timeMs / 60000); + + return timeMin < 1 ? 1 : timeMin; +} + +/** + * Calculate total and elapsed time for a pattern based on color blocks + * This matches the Brother app's calculation method + */ +export function calculatePatternTime( + colorBlocks: Array<{ stitchCount: number }>, + currentStitch: number +): { + totalMinutes: number; + elapsedMinutes: number; + remainingMinutes: number; +} { + let totalMinutes = 0; + let elapsedMinutes = 0; + let cumulativeStitches = 0; + + // Calculate time per color block + for (const block of colorBlocks) { + totalMinutes += convertStitchesToMinutes(block.stitchCount); + cumulativeStitches += block.stitchCount; + + if (cumulativeStitches < currentStitch) { + // This entire block is completed + elapsedMinutes += convertStitchesToMinutes(block.stitchCount); + } else if (cumulativeStitches === currentStitch) { + // We just completed this block + elapsedMinutes += convertStitchesToMinutes(block.stitchCount); + break; + } else { + // We're partway through this block + const stitchesInBlock = currentStitch - (cumulativeStitches - block.stitchCount); + elapsedMinutes += convertStitchesToMinutes(stitchesInBlock); + break; + } + } + + return { + totalMinutes, + elapsedMinutes, + remainingMinutes: Math.max(0, totalMinutes - elapsedMinutes), + }; +} + +/** + * Format minutes as MM:SS + */ +export function formatMinutes(minutes: number): string { + const mins = Math.floor(minutes); + const secs = Math.round((minutes - mins) * 60); + return `${mins}:${String(secs).padStart(2, '0')}`; +}