280 lines
10 KiB
Plaintext
280 lines
10 KiB
Plaintext
import type {
|
|
BleDevice,
|
|
BleConnectionState,
|
|
BleEvent,
|
|
BleEventCallback,
|
|
BleEventPayload,
|
|
BleScanResult,
|
|
BleConnectOptionsExt,
|
|
AutoBleInterfaces,
|
|
BleDataPayload,
|
|
SendDataPayload,
|
|
BleOptions,
|
|
MultiProtocolDevice,
|
|
ScanHandler,
|
|
BleProtocolType,
|
|
ScanDevicesOptions
|
|
} from '../interface.uts';
|
|
import { ProtocolHandler } from '../protocol_handler.uts';
|
|
import { BluetoothService } from '../interface.uts';
|
|
import { DeviceManager } from './device_manager.uts';
|
|
|
|
type RawProtocolHandler = {
|
|
protocol?: BleProtocolType;
|
|
scanDevices?: (options?: ScanDevicesOptions) => Promise<void>;
|
|
connect?: (device: BleDevice, options?: BleConnectOptionsExt) => Promise<void>;
|
|
disconnect?: (device: BleDevice) => Promise<void>;
|
|
sendData?: (device: BleDevice, payload?: SendDataPayload, options?: BleOptions) => Promise<void>;
|
|
autoConnect?: (device: BleDevice, options?: BleConnectOptionsExt) => Promise<AutoBleInterfaces>;
|
|
}
|
|
|
|
class DeviceContext {
|
|
device: BleDevice;
|
|
protocol: BleProtocolType;
|
|
state: BleConnectionState;
|
|
handler: ProtocolHandler;
|
|
constructor(device: BleDevice, protocol: BleProtocolType, handler: ProtocolHandler) {
|
|
this.device = device;
|
|
this.protocol = protocol;
|
|
this.state = 0;
|
|
this.handler = handler;
|
|
}
|
|
}
|
|
|
|
const deviceMap = new Map<string, DeviceContext>();
|
|
let activeProtocol: BleProtocolType = 'standard';
|
|
let activeHandler: ProtocolHandler | null = null;
|
|
const eventListeners = new Map<BleEvent, Set<BleEventCallback>>();
|
|
let defaultBluetoothService: BluetoothService | null = null;
|
|
|
|
function emit(event: BleEvent, payload: BleEventPayload) {
|
|
const listeners = eventListeners.get(event);
|
|
if (listeners != null) {
|
|
listeners.forEach(cb => {
|
|
try { cb(payload); } catch (e) { }
|
|
});
|
|
}
|
|
}
|
|
|
|
class ProtocolHandlerWrapper extends ProtocolHandler {
|
|
private _raw: RawProtocolHandler | null;
|
|
constructor(raw?: RawProtocolHandler, bluetoothService?: BluetoothService) {
|
|
super(bluetoothService);
|
|
this._raw = raw ?? null;
|
|
}
|
|
override async scanDevices(options?: ScanDevicesOptions): Promise<void> {
|
|
const rawTyped = this._raw;
|
|
if (rawTyped != null && typeof rawTyped.scanDevices == 'function') {
|
|
await rawTyped.scanDevices(options);
|
|
}
|
|
return;
|
|
}
|
|
override async connect(device: BleDevice, options?: BleConnectOptionsExt): Promise<void> {
|
|
const rawTyped = this._raw;
|
|
if (rawTyped != null && typeof rawTyped.connect == 'function') {
|
|
await rawTyped.connect(device, options);
|
|
}
|
|
return;
|
|
}
|
|
override async disconnect(device: BleDevice): Promise<void> {
|
|
const rawTyped = this._raw;
|
|
if (rawTyped != null && typeof rawTyped.disconnect == 'function') {
|
|
await rawTyped.disconnect(device);
|
|
}
|
|
return;
|
|
}
|
|
override async sendData(device: BleDevice, payload?: SendDataPayload, options?: BleOptions): Promise<void> {
|
|
const rawTyped = this._raw;
|
|
if (rawTyped != null && typeof rawTyped.sendData == 'function') {
|
|
await rawTyped.sendData(device, payload, options);
|
|
}
|
|
return;
|
|
}
|
|
override async autoConnect(device: BleDevice, options?: BleConnectOptionsExt): Promise<AutoBleInterfaces> {
|
|
const rawTyped = this._raw;
|
|
if (rawTyped != null && typeof rawTyped.autoConnect == 'function') {
|
|
return await rawTyped.autoConnect(device, options);
|
|
}
|
|
return { serviceId: '', writeCharId: '', notifyCharId: '' };
|
|
}
|
|
}
|
|
|
|
function isRawProtocolHandler(x: any): boolean {
|
|
if (x == null || typeof x !== 'object') return false;
|
|
const r = x as Record<string, unknown>;
|
|
if (typeof r['scanDevices'] == 'function') return true;
|
|
if (typeof r['connect'] == 'function') return true;
|
|
if (typeof r['disconnect'] == 'function') return true;
|
|
if (typeof r['sendData'] == 'function') return true;
|
|
if (typeof r['autoConnect'] == 'function') return true;
|
|
if (typeof r['protocol'] == 'string') return true;
|
|
return false;
|
|
}
|
|
|
|
export const registerProtocolHandler = (handler: any) => {
|
|
if (handler == null) return;
|
|
let proto: BleProtocolType = 'standard';
|
|
if (handler instanceof ProtocolHandler) {
|
|
try { proto = (handler as ProtocolHandler).protocol as BleProtocolType; } catch (e) { }
|
|
activeHandler = handler as ProtocolHandler;
|
|
} else if (isRawProtocolHandler(handler)) {
|
|
try { proto = (handler as RawProtocolHandler).protocol as BleProtocolType; } catch (e) { }
|
|
activeHandler = new ProtocolHandlerWrapper(handler as RawProtocolHandler, defaultBluetoothService);
|
|
(activeHandler as ProtocolHandler).protocol = proto;
|
|
} else {
|
|
console.warn('[AKBLE] registerProtocolHandler: unsupported handler type, ignoring', handler);
|
|
return;
|
|
}
|
|
activeProtocol = proto;
|
|
}
|
|
|
|
export const scanDevices = async (options?: ScanDevicesOptions): Promise<void> => {
|
|
ensureDefaultProtocolHandler();
|
|
if (activeHandler == null) {
|
|
console.log('[AKBLE] no active scan handler registered');
|
|
return;
|
|
}
|
|
const handler = activeHandler as ProtocolHandler;
|
|
const original = options ?? null;
|
|
const scanOptions: ScanDevicesOptions = {} as ScanDevicesOptions;
|
|
if (original != null) {
|
|
if (original.protocols != null) scanOptions.protocols = original.protocols;
|
|
if (original.optionalServices != null) scanOptions.optionalServices = original.optionalServices;
|
|
if (original.timeout != null) scanOptions.timeout = original.timeout;
|
|
}
|
|
const userFound = original?.onDeviceFound ?? null;
|
|
scanOptions.onDeviceFound = (device: BleDevice) => {
|
|
emit('deviceFound', { event: 'deviceFound', device });
|
|
if (userFound != null) {
|
|
try { userFound(device); } catch (err) { }
|
|
}
|
|
};
|
|
const userFinished = original?.onScanFinished ?? null;
|
|
scanOptions.onScanFinished = () => {
|
|
emit('scanFinished', { event: 'scanFinished' });
|
|
if (userFinished != null) {
|
|
try { userFinished(); } catch (err) { }
|
|
}
|
|
};
|
|
try {
|
|
await handler.scanDevices(scanOptions);
|
|
} catch (e) {
|
|
console.warn('[AKBLE] scanDevices handler error', e);
|
|
}
|
|
}
|
|
|
|
export const connectDevice = async (deviceId: string, protocol: BleProtocolType, options?: BleConnectOptionsExt): Promise<void> => {
|
|
const handler = activeHandler;
|
|
if (handler == null) throw new Error('No protocol handler');
|
|
const device: BleDevice = { deviceId, name: '', rssi: 0 };
|
|
await handler.connect(device, options);
|
|
const ctx = new DeviceContext(device, protocol, handler);
|
|
ctx.state = 2;
|
|
deviceMap.set(getDeviceKey(deviceId, protocol), ctx);
|
|
emit('connectionStateChanged', { event: 'connectionStateChanged', device, protocol, state: 2 });
|
|
}
|
|
|
|
export const disconnectDevice = async (deviceId: string, protocol: BleProtocolType): Promise<void> => {
|
|
const ctx = deviceMap.get(getDeviceKey(deviceId, protocol));
|
|
if (ctx == null || ctx.handler == null) return;
|
|
await ctx.handler.disconnect(ctx.device);
|
|
ctx.state = 0;
|
|
emit('connectionStateChanged', { event: 'connectionStateChanged', device: ctx.device, protocol, state: 0 });
|
|
deviceMap.delete(getDeviceKey(deviceId, protocol));
|
|
}
|
|
|
|
export const sendData = async (payload: SendDataPayload, options?: BleOptions): Promise<void> => {
|
|
const ctx = deviceMap.get(getDeviceKey(payload.deviceId, payload.protocol));
|
|
if (ctx == null) throw new Error('Device not connected');
|
|
const deviceCtx = ctx as DeviceContext;
|
|
if (deviceCtx.handler == null) throw new Error('sendData not supported for this protocol');
|
|
await deviceCtx.handler.sendData(deviceCtx.device, payload, options);
|
|
emit('dataSent', { event: 'dataSent', device: deviceCtx.device, protocol: payload.protocol, data: payload.data });
|
|
}
|
|
|
|
export const getConnectedDevices = (): MultiProtocolDevice[] => {
|
|
const result: MultiProtocolDevice[] = [];
|
|
deviceMap.forEach((ctx: DeviceContext) => {
|
|
const dev: MultiProtocolDevice = {
|
|
deviceId: ctx.device.deviceId,
|
|
name: ctx.device.name,
|
|
rssi: ctx.device.rssi,
|
|
protocol: ctx.protocol
|
|
};
|
|
result.push(dev);
|
|
});
|
|
return result;
|
|
}
|
|
|
|
export const getConnectionState = (deviceId: string, protocol: BleProtocolType): BleConnectionState => {
|
|
const ctx = deviceMap.get(getDeviceKey(deviceId, protocol));
|
|
if (ctx == null) return 0;
|
|
return ctx.state;
|
|
}
|
|
|
|
export const on = (event: BleEvent, callback: BleEventCallback) => {
|
|
if (!eventListeners.has(event)) eventListeners.set(event, new Set());
|
|
eventListeners.get(event)!.add(callback);
|
|
}
|
|
|
|
export const off = (event: BleEvent, callback?: BleEventCallback) => {
|
|
if (callback == null) {
|
|
eventListeners.delete(event);
|
|
} else {
|
|
eventListeners.get(event)?.delete(callback as BleEventCallback);
|
|
}
|
|
}
|
|
|
|
function getDeviceKey(deviceId: string, protocol: BleProtocolType): string {
|
|
return `${deviceId}|${protocol}`;
|
|
}
|
|
|
|
export const autoConnect = async (deviceId: string, protocol: BleProtocolType, options?: BleConnectOptionsExt): Promise<AutoBleInterfaces> => {
|
|
const handler = activeHandler;
|
|
if (handler == null) throw new Error('autoConnect not supported for this protocol');
|
|
const device: BleDevice = { deviceId, name: '', rssi: 0 };
|
|
return await handler.autoConnect(device, options) as AutoBleInterfaces;
|
|
}
|
|
|
|
function ensureDefaultProtocolHandler(): void {
|
|
if (activeHandler != null) return;
|
|
const service = defaultBluetoothService;
|
|
if (service == null) return;
|
|
try {
|
|
const _dm = DeviceManager.getInstance();
|
|
const _raw: RawProtocolHandler = {
|
|
protocol: 'standard',
|
|
scanDevices: (options?: ScanDevicesOptions) => {
|
|
try {
|
|
const scanOptions = options != null ? options : {} as ScanDevicesOptions;
|
|
_dm.startScan(scanOptions);
|
|
} catch (e) {
|
|
console.warn('[AKBLE] DeviceManager.startScan failed', e);
|
|
}
|
|
return Promise.resolve();
|
|
},
|
|
connect: (device, options?: BleConnectOptionsExt) => {
|
|
return _dm.connectDevice(device.deviceId, options);
|
|
},
|
|
disconnect: (device) => {
|
|
return _dm.disconnectDevice(device.deviceId);
|
|
},
|
|
autoConnect: (device, _options?: any) => {
|
|
const result: AutoBleInterfaces = { serviceId: '', writeCharId: '', notifyCharId: '' };
|
|
return Promise.resolve(result);
|
|
}
|
|
};
|
|
const _wrapper = new ProtocolHandlerWrapper(_raw, service);
|
|
activeHandler = _wrapper;
|
|
activeProtocol = _raw.protocol as BleProtocolType;
|
|
console.log('[AKBLE] default protocol handler registered', activeProtocol);
|
|
} catch (e) {
|
|
console.warn('[AKBLE] failed to register default protocol handler', e);
|
|
}
|
|
}
|
|
|
|
export const setDefaultBluetoothService = (service: BluetoothService) => {
|
|
defaultBluetoothService = service;
|
|
ensureDefaultProtocolHandler();
|
|
};
|