Add Bluetooth pairing error detection and automatic disconnect handling

Detect when device is not paired at OS level and handle automatic disconnections. The app now properly updates UI state when the device is disconnected by the OS.

Changes:
- Add BluetoothPairingError custom error class in BrotherPP1Service
- Track isInitialConnection state to differentiate pairing issues from disconnects
- Detect pairing issues in sendCommand() only during initial connection
- Add onDisconnect() subscription method to BrotherPP1Service
- Listen for 'gattserverdisconnected' events and update state automatically
- Add isPairingError state to useBrotherMachine hook
- Display pairing errors with blue info styling instead of red error styling
- Include step-by-step pairing instructions: long-press Bluetooth button on machine, then pair in OS settings
- Automatically reset connection state when device disconnects

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Jan-Henrik 2025-12-07 13:49:34 +01:00
parent 651fa35a77
commit 8ee7d0ce7b
3 changed files with 143 additions and 52 deletions

View file

@ -180,13 +180,22 @@ function App() {
<div className="flex-1 p-6 max-w-[1600px] w-full mx-auto">
{/* Global errors */}
{machine.error && (
<div className="bg-red-50 dark:bg-red-900/20 text-red-900 dark:text-red-200 px-6 py-4 rounded-lg border-l-4 border-red-600 dark:border-red-500 mb-6 shadow-md hover:shadow-lg transition-shadow animate-fadeIn">
<div className="flex items-center gap-2">
<svg className="w-5 h-5 text-red-600 dark:text-red-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<div className={`px-6 py-4 rounded-lg border-l-4 mb-6 shadow-md hover:shadow-lg transition-shadow animate-fadeIn ${
machine.isPairingError
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-900 dark:text-blue-200 border-blue-600 dark:border-blue-500'
: 'bg-red-50 dark:bg-red-900/20 text-red-900 dark:text-red-200 border-red-600 dark:border-red-500'
}`}>
<div className="flex items-center gap-3">
<svg className={`w-5 h-5 flex-shrink-0 ${machine.isPairingError ? 'text-blue-600 dark:text-blue-400' : 'text-red-600 dark:text-red-400'}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
{machine.isPairingError ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
)}
</svg>
<div>
<strong className="font-semibold">Error:</strong> {machine.error}
<div className="flex-1">
<div className="font-semibold mb-1">{machine.isPairingError ? 'Pairing Required' : 'Error'}</div>
<div className="text-sm">{machine.error}</div>
</div>
</div>
</div>

View file

@ -1,5 +1,5 @@
import { useState, useCallback, useEffect } from "react";
import { BrotherPP1Service } from "../services/BrotherPP1Service";
import { BrotherPP1Service, BluetoothPairingError } from "../services/BrotherPP1Service";
import type {
MachineInfo,
PatternInfo,
@ -27,6 +27,7 @@ export function useBrotherMachine() {
);
const [uploadProgress, setUploadProgress] = useState<number>(0);
const [error, setError] = useState<string | null>(null);
const [isPairingError, setIsPairingError] = useState(false);
const [isCommunicating, setIsCommunicating] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
@ -42,6 +43,22 @@ export function useBrotherMachine() {
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<PesPatternData | null> => {
try {
@ -101,6 +118,7 @@ export function useBrotherMachine() {
const connect = useCallback(async () => {
try {
setError(null);
setIsPairingError(false);
await service.connect();
setIsConnected(true);
@ -116,6 +134,8 @@ export function useBrotherMachine() {
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);
}
@ -399,6 +419,7 @@ export function useBrotherMachine() {
sewingProgress,
uploadProgress,
error,
isPairingError,
isPolling: isCommunicating,
isUploading,
isDeleting,

View file

@ -5,6 +5,14 @@ import type {
} from "../types/machine";
import { MachineStatus } from "../types/machine";
// Custom error for pairing issues
export class BluetoothPairingError extends Error {
constructor(message: string) {
super(message);
this.name = 'BluetoothPairingError';
}
}
// BLE Service and Characteristic UUIDs
const SERVICE_UUID = "a76eb9e0-f3ac-4990-84cf-3a94d2426b2b";
const WRITE_CHAR_UUID = "a76eb9e2-f3ac-4990-84cf-3a94d2426b2b";
@ -48,7 +56,9 @@ export class BrotherPP1Service {
private commandQueue: Array<() => Promise<void>> = [];
private isProcessingQueue = false;
private isCommunicating = false;
private isInitialConnection = false;
private communicationCallbacks: Set<(isCommunicating: boolean) => void> = new Set();
private disconnectCallbacks: Set<() => void> = new Set();
/**
* Subscribe to communication state changes
@ -64,6 +74,18 @@ export class BrotherPP1Service {
};
}
/**
* Subscribe to disconnect events
* @param callback Function called when device disconnects
* @returns Unsubscribe function
*/
onDisconnect(callback: () => void): () => void {
this.disconnectCallbacks.add(callback);
return () => {
this.disconnectCallbacks.delete(callback);
};
}
private setCommunicating(value: boolean) {
if (this.isCommunicating !== value) {
this.isCommunicating = value;
@ -71,7 +93,20 @@ export class BrotherPP1Service {
}
}
private handleDisconnect() {
console.log('[BrotherPP1Service] Device disconnected');
this.server = null;
this.writeCharacteristic = null;
this.readCharacteristic = null;
this.commandQueue = [];
this.isProcessingQueue = false;
this.setCommunicating(false);
this.disconnectCallbacks.forEach(callback => callback());
}
async connect(): Promise<void> {
this.isInitialConnection = true;
try {
this.device = await navigator.bluetooth.requestDevice({
filters: [{ services: [SERVICE_UUID] }],
});
@ -79,6 +114,12 @@ export class BrotherPP1Service {
if (!this.device.gatt) {
throw new Error("GATT not available");
}
// Listen for disconnection events
this.device.addEventListener('gattserverdisconnected', () => {
this.handleDisconnect();
});
console.log("Connecting");
this.server = await this.device.gatt.connect();
console.log("Connected");
@ -96,6 +137,10 @@ export class BrotherPP1Service {
console.log("Dummy command success");
} catch (e) {
console.log(e);
throw e;
}
} finally {
this.isInitialConnection = false;
}
}
@ -220,6 +265,7 @@ export class BrotherPP1Service {
hexData,
);
try {
// Write command
await this.writeCharacteristic.writeValueWithResponse(command);
@ -248,6 +294,21 @@ export class BrotherPP1Service {
console.log(`[RX] ${this.getCommandName(cmdId)}:`, hexResponse, parsed);
return response;
} catch (error) {
// Detect pairing issues - only during initial connection
if (this.isInitialConnection && error instanceof Error) {
const errorMsg = error.message.toLowerCase();
if (
errorMsg.includes('gatt server is disconnected') ||
(errorMsg.includes('writevaluewithresponse') && errorMsg.includes('gatt server is disconnected'))
) {
throw new BluetoothPairingError(
'Device not paired. To pair: long-press the Bluetooth button on the machine, then pair it using your operating system\'s Bluetooth settings. After pairing, try connecting again.'
);
}
}
throw error;
}
});
}