expo-ble-app-demo/ble/core/BleClient.ts

234 lines
7.8 KiB
TypeScript

import {BleManager, Device, Characteristic, BleError as PlxError, ScanOptions} from 'react-native-ble-plx';
import {Platform, PermissionsAndroid} from 'react-native';
import {BleDevice, ConnectionState, BleError, ScanResult} from './types';
import {BLE_UUIDS} from "@/ble";
export class BleClient {
private static instance: BleClient;
private manager: BleManager | null = null;
private connectedDevice: Device | null = null;
// Simple event system
private listeners: Map<string, Set<Function>> = new Map();
private constructor() {
if (Platform.OS !== 'web') {
this.manager = new BleManager();
}
}
public static getInstance(): BleClient {
if (!BleClient.instance) {
BleClient.instance = new BleClient();
}
return BleClient.instance;
}
public addListener(event: string, callback: Function) {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(callback);
}
public removeListener(event: string, callback: Function) {
if (this.listeners.has(event)) {
this.listeners.get(event)!.delete(callback);
}
}
private emit(event: string, ...args: any[]) {
if (this.listeners.has(event)) {
this.listeners.get(event)!.forEach(cb => cb(...args));
}
}
public async startScan(
serviceUUIDs: string[] | null = null,
options: ScanOptions = {},
onDeviceFound: (result: ScanResult) => void
): Promise<void> {
if (!this.manager) {
console.warn('BLE not supported on web');
return;
}
const state = await this.manager.state();
if (state !== 'PoweredOn') {
throw new Error(`Bluetooth is not powered on. State: ${state}`);
}
this.manager.startDeviceScan(serviceUUIDs, options, (error, device) => {
if (error) {
this.emit('scanError', error);
return;
}
if (device) {
onDeviceFound({
device,
rssi: device.rssi,
localName: device.name
});
}
});
}
public async getConnectedDevices(serviceUUIDs: string[]): Promise<BleDevice[]> {
if (!this.manager) return [];
try {
const devices = await this.manager.connectedDevices(serviceUUIDs);
return devices as BleDevice[];
} catch (e) {
throw this.normalizeError(e);
}
}
public stopScan() {
if (!this.manager) return;
this.manager.stopDeviceScan();
}
public async connect(deviceId: string): Promise<BleDevice> {
if (!this.manager) throw new Error('BLE not supported on web');
try {
this.emit('connectionStateChange', {deviceId, state: ConnectionState.CONNECTING});
let device = await this.manager.connectToDevice(deviceId, {
autoConnect: false,
requestMTU: BLE_UUIDS.REQUEST_MTU
});
if (device.mtu < BLE_UUIDS.REQUEST_MTU) {
console.log("MTU not supported, requesting default to ", BLE_UUIDS.REQUEST_MTU);
device = await device.requestMTU(BLE_UUIDS.REQUEST_MTU);
// Give some time for the stack to stabilize after MTU change
await new Promise(resolve => setTimeout(resolve, 500));
}
console.log("Connected to device with MTU = ", device.mtu);
this.connectedDevice = await device.discoverAllServicesAndCharacteristics();
this.emit('connectionStateChange', {deviceId, state: ConnectionState.CONNECTED});
// Handle disconnection monitoring
device.onDisconnected((error, disconnectedDevice) => {
this.connectedDevice = null;
this.emit('connectionStateChange', {
deviceId: disconnectedDevice.id,
state: ConnectionState.DISCONNECTED
});
this.emit('disconnected', disconnectedDevice);
});
return this.connectedDevice;
} catch (error: any) {
this.emit('connectionStateChange', {deviceId, state: ConnectionState.DISCONNECTED});
throw this.normalizeError(error);
}
}
public async disconnect(deviceId?: string): Promise<void> {
const id = deviceId || this.connectedDevice?.id;
if (!id) return;
if (!this.manager) return;
try {
this.emit('connectionStateChange', {deviceId: id, state: ConnectionState.DISCONNECTING});
await this.manager.cancelDeviceConnection(id);
// onDisconnected callback will handle the state update
} catch (error: any) {
throw this.normalizeError(error);
}
}
public async read(
deviceId: string,
serviceUUID: string,
characteristicUUID: string
): Promise<string> {
if (!this.manager) throw new Error('BLE not supported on web');
try {
const char = await this.manager.readCharacteristicForDevice(
deviceId,
serviceUUID,
characteristicUUID
);
return char.value || ''; // Base64 string
} catch (e) {
throw this.normalizeError(e);
}
}
public async write(
deviceId: string,
serviceUUID: string,
characteristicUUID: string,
dataBase64: string,
response: boolean = true
): Promise<Characteristic> {
if (!this.manager) throw new Error('BLE not supported on web');
let result: Characteristic | null;
try {
if (response) {
result = await this.manager.writeCharacteristicWithResponseForDevice(
deviceId, serviceUUID, characteristicUUID, dataBase64
);
} else {
result = await this.manager.writeCharacteristicWithoutResponseForDevice(
deviceId, serviceUUID, characteristicUUID, dataBase64
);
}
return result
} catch (e) {
throw this.normalizeError(e);
}
}
public async monitor(
deviceId: string,
serviceUUID: string,
characteristicUUID: string,
listener: (error: BleError | null, value: string | null) => void
) {
if (!this.manager) {
listener({message: 'BLE not supported on web', errorCode: 0, reason: null}, null);
return {
remove: () => {
}
};
}
return this.manager.monitorCharacteristicForDevice(
deviceId,
serviceUUID,
characteristicUUID,
(error, char) => {
if (error) {
listener(this.normalizeError(error), null);
} else {
listener(null, char?.value || null);
}
}
);
}
public async requestMtu(deviceId: string, mtu: number): Promise<number> {
if (!this.manager) return 23;
try {
const device = await this.manager.requestMTUForDevice(deviceId, mtu);
return device.mtu;
} catch (e) {
console.warn("MTU negotiation failed", e);
// iOS doesn't allow explicit MTU request usually, so we might ignore or return default
return 23;
}
}
public getConnectedDevice(): Device | null {
return this.connectedDevice;
}
private normalizeError(e: any): BleError {
// wrapper to convert PlxError to our BleError
return {
errorCode: e.errorCode || 0,
message: e.message || 'Unknown error',
reason: e.reason
};
}
}