forked from yudi_xiao/expo-ble-app-demo
234 lines
7.8 KiB
TypeScript
234 lines
7.8 KiB
TypeScript
import {BleManager, Device, Characteristic, ScanOptions} from 'react-native-ble-plx';
|
|
import {Platform} from 'react-native';
|
|
import {BleDevice, ConnectionState, BleError, ScanResult} from './types';
|
|
import {BLE_UUIDS} from "../protocol/Constants";
|
|
|
|
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 ${device.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
|
|
};
|
|
}
|
|
}
|