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')}`;
+}