mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 10:23:41 +00:00
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:
parent
651fa35a77
commit
8ee7d0ce7b
3 changed files with 143 additions and 52 deletions
21
src/App.tsx
21
src/App.tsx
|
|
@ -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">
|
||||
<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" />
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,31 +93,54 @@ 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.device = await navigator.bluetooth.requestDevice({
|
||||
filters: [{ services: [SERVICE_UUID] }],
|
||||
});
|
||||
|
||||
if (!this.device.gatt) {
|
||||
throw new Error("GATT not available");
|
||||
}
|
||||
console.log("Connecting");
|
||||
this.server = await this.device.gatt.connect();
|
||||
console.log("Connected");
|
||||
const service = await this.server.getPrimaryService(SERVICE_UUID);
|
||||
console.log("Got primary service");
|
||||
|
||||
this.writeCharacteristic = await service.getCharacteristic(WRITE_CHAR_UUID);
|
||||
this.readCharacteristic = await service.getCharacteristic(READ_CHAR_UUID);
|
||||
|
||||
console.log("Connected to Brother PP1 machine");
|
||||
|
||||
console.log("Send dummy command");
|
||||
this.isInitialConnection = true;
|
||||
try {
|
||||
await this.getMachineInfo();
|
||||
console.log("Dummy command success");
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
this.device = await navigator.bluetooth.requestDevice({
|
||||
filters: [{ services: [SERVICE_UUID] }],
|
||||
});
|
||||
|
||||
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");
|
||||
const service = await this.server.getPrimaryService(SERVICE_UUID);
|
||||
console.log("Got primary service");
|
||||
|
||||
this.writeCharacteristic = await service.getCharacteristic(WRITE_CHAR_UUID);
|
||||
this.readCharacteristic = await service.getCharacteristic(READ_CHAR_UUID);
|
||||
|
||||
console.log("Connected to Brother PP1 machine");
|
||||
|
||||
console.log("Send dummy command");
|
||||
try {
|
||||
await this.getMachineInfo();
|
||||
console.log("Dummy command success");
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
this.isInitialConnection = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -220,34 +265,50 @@ export class BrotherPP1Service {
|
|||
hexData,
|
||||
);
|
||||
|
||||
// Write command
|
||||
await this.writeCharacteristic.writeValueWithResponse(command);
|
||||
try {
|
||||
// Write command
|
||||
await this.writeCharacteristic.writeValueWithResponse(command);
|
||||
|
||||
// Longer delay to allow machine to prepare response
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
// Longer delay to allow machine to prepare response
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
||||
// Read response
|
||||
const responseData = await this.readCharacteristic.readValue();
|
||||
const response = new Uint8Array(responseData.buffer);
|
||||
// Read response
|
||||
const responseData = await this.readCharacteristic.readValue();
|
||||
const response = new Uint8Array(responseData.buffer);
|
||||
|
||||
const hexResponse = Array.from(response)
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join(" ");
|
||||
const hexResponse = Array.from(response)
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join(" ");
|
||||
|
||||
// Parse response
|
||||
let parsed = "";
|
||||
if (response.length >= 3) {
|
||||
const respCmdId = (response[0] << 8) | response[1];
|
||||
const status = response[2];
|
||||
parsed = ` | Status: 0x${status.toString(16).padStart(2, "0")}`;
|
||||
if (respCmdId !== cmdId) {
|
||||
parsed += ` | WARNING: Response cmd 0x${respCmdId.toString(16).padStart(4, "0")} != request cmd`;
|
||||
// Parse response
|
||||
let parsed = "";
|
||||
if (response.length >= 3) {
|
||||
const respCmdId = (response[0] << 8) | response[1];
|
||||
const status = response[2];
|
||||
parsed = ` | Status: 0x${status.toString(16).padStart(2, "0")}`;
|
||||
if (respCmdId !== cmdId) {
|
||||
parsed += ` | WARNING: Response cmd 0x${respCmdId.toString(16).padStart(4, "0")} != request cmd`;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
console.log(`[RX] ${this.getCommandName(cmdId)}:`, hexResponse, parsed);
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue