From b7ea024df3720d8b00987e24a3ade34f270dc3be Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Sun, 7 Dec 2025 00:02:22 +0100 Subject: [PATCH] Improve polling indicator with service-level communication state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, isPolling was manually set in individual refresh functions, which didn't cover all BLE communication and could have race conditions. Now the communication indicator is driven by the service layer: - Added isCommunicating state to BrotherPP1Service - setCommunicating() called at queue processing boundaries - onCommunicationChange() callback subscription pattern - Hook subscribes to service communication state automatically Benefits: - Indicator shows for ALL BLE commands (status, progress, service count, etc) - Prevents race conditions from overlapping command queue processing - More accurate representation of actual BLE communication - Cleaner code - no manual setIsPolling in each function The indicator now properly shows whenever the command queue is being processed, regardless of which specific command triggered it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- src/hooks/useBrotherMachine.ts | 41 +++++++++++++++++++++++++------ src/services/BrotherPP1Service.ts | 25 +++++++++++++++++++ 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/hooks/useBrotherMachine.ts b/src/hooks/useBrotherMachine.ts index 1229b57..845e5d5 100644 --- a/src/hooks/useBrotherMachine.ts +++ b/src/hooks/useBrotherMachine.ts @@ -27,7 +27,7 @@ export function useBrotherMachine() { ); const [uploadProgress, setUploadProgress] = useState(0); const [error, setError] = useState(null); - const [isPolling, setIsPolling] = useState(false); + const [isCommunicating, setIsCommunicating] = useState(false); const [isUploading, setIsUploading] = useState(false); const [isDeleting, setIsDeleting] = useState(false); const [resumeAvailable, setResumeAvailable] = useState(false); @@ -36,6 +36,12 @@ export function useBrotherMachine() { null, ); + // Subscribe to service communication state + useEffect(() => { + const unsubscribe = service.onCommunicationChange(setIsCommunicating); + return unsubscribe; + }, [service]); + // Define checkResume first (before connect uses it) const checkResume = useCallback(async (): Promise => { try { @@ -133,14 +139,11 @@ export function useBrotherMachine() { if (!isConnected) return; try { - setIsPolling(true); const state = await service.getMachineState(); setMachineStatus(state.status); setMachineError(state.error); } catch (err) { setError(err instanceof Error ? err.message : "Failed to get status"); - } finally { - setIsPolling(false); } }, [service, isConnected]); @@ -168,6 +171,22 @@ export function useBrotherMachine() { } }, [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; @@ -349,8 +368,16 @@ export function useBrotherMachine() { } }, pollInterval); - return () => clearInterval(interval); - }, [isConnected, machineStatus, refreshStatus, refreshProgress]); + // 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) @@ -372,7 +399,7 @@ export function useBrotherMachine() { sewingProgress, uploadProgress, error, - isPolling, + isPolling: isCommunicating, isUploading, isDeleting, resumeAvailable, diff --git a/src/services/BrotherPP1Service.ts b/src/services/BrotherPP1Service.ts index f480ad5..d19618c 100644 --- a/src/services/BrotherPP1Service.ts +++ b/src/services/BrotherPP1Service.ts @@ -47,6 +47,29 @@ export class BrotherPP1Service { private readCharacteristic: BluetoothRemoteGATTCharacteristic | null = null; private commandQueue: Array<() => Promise> = []; private isProcessingQueue = false; + private isCommunicating = false; + private communicationCallbacks: Set<(isCommunicating: boolean) => void> = new Set(); + + /** + * Subscribe to communication state changes + * @param callback Function called when communication state changes + * @returns Unsubscribe function + */ + onCommunicationChange(callback: (isCommunicating: boolean) => void): () => void { + this.communicationCallbacks.add(callback); + // Immediately call with current state + callback(this.isCommunicating); + return () => { + this.communicationCallbacks.delete(callback); + }; + } + + private setCommunicating(value: boolean) { + if (this.isCommunicating !== value) { + this.isCommunicating = value; + this.communicationCallbacks.forEach(callback => callback(value)); + } + } async connect(): Promise { this.device = await navigator.bluetooth.requestDevice({ @@ -103,6 +126,7 @@ export class BrotherPP1Service { } this.isProcessingQueue = true; + this.setCommunicating(true); while (this.commandQueue.length > 0) { const command = this.commandQueue.shift(); @@ -117,6 +141,7 @@ export class BrotherPP1Service { } this.isProcessingQueue = false; + this.setCommunicating(false); } /**