Initial commit of akmon project

This commit is contained in:
2026-01-20 08:04:15 +08:00
commit 77a2bab985
1309 changed files with 343305 additions and 0 deletions

View File

@@ -0,0 +1,237 @@
// 设备管理相关:扫描、连接、断开、重连
import { BleDevice, BLE_CONNECTION_STATE } from '../interface.uts';
import type { BleConnectOptionsExt } from '../interface.uts';
export class DeviceManager {
private devices = {};
private servers = {};
private connectionStates = {};
private allowedServices = {};
private reconnectAttempts: number = 0;
private maxReconnectAttempts: number = 5;
private reconnectDelay: number = 2000;
private reconnectTimeoutId: number = 0;
private autoReconnect: boolean = false;
private connectionStateChangeListeners: Function[] = [];
private deviceFoundListeners: ((device: BleDevice) => void)[] = [];
private scanFinishedListeners: (() => void)[] = [];
onDeviceFound(listener: (device: BleDevice) => void) {
this.deviceFoundListeners.push(listener);
}
onScanFinished(listener: () => void) {
this.scanFinishedListeners.push(listener);
}
private emitDeviceFound(device: BleDevice) {
for (const listener of this.deviceFoundListeners) {
try { listener(device); } catch (e) {}
}
}
private emitScanFinished() {
for (const listener of this.scanFinishedListeners) {
try { listener(); } catch (e) {}
}
}
async startScan(options?: { optionalServices?: string[] } ): Promise<void> {
if (!navigator.bluetooth) throw new Error('Web Bluetooth API not supported');
try {
const scanOptions: any = { acceptAllDevices: true };
// allow callers to request optionalServices (required by Web Bluetooth to access custom services)
if (options && Array.isArray(options.optionalServices) && options.optionalServices.length > 0) {
scanOptions.optionalServices = options.optionalServices;
}
// Log the exact options passed to requestDevice for debugging optionalServices propagation
try {
console.log('[DeviceManager] requestDevice options:', JSON.stringify(scanOptions));
} catch (e) {
console.log('[DeviceManager] requestDevice options (raw):', scanOptions);
}
const device = await navigator.bluetooth.requestDevice(scanOptions);
try {
console.log('[DeviceManager] requestDevice result:', device);
} catch (e) {
console.log('[DeviceManager] requestDevice result (raw):', device);
}
if (device) {
console.log(device)
// 格式化 deviceId 为 MAC 地址格式
const formatDeviceId = (id: string): string => {
// 如果是12位16进制字符串如 'AABBCCDDEEFF'),转为 'AA:BB:CC:DD:EE:FF'
if (/^[0-9A-Fa-f]{12}$/.test(id)) {
return id.match(/.{1,2}/g)!.join(":").toUpperCase();
}
// 如果是base64无法直接转MAC保留原样
// 你可以根据实际情况扩展此处
return id;
};
const isConnected = !!this.servers[device.id];
const formattedId = formatDeviceId(device.id);
const bleDevice = { deviceId: formattedId, name: device.name, connected: isConnected };
this.devices[formattedId] = device;
this.emitDeviceFound(bleDevice);
}
this.emitScanFinished();
} catch (e) {
this.emitScanFinished();
throw e;
}
}
onConnectionStateChange(listener: (deviceId: string, state: string) => void) {
this.connectionStateChangeListeners.push(listener);
}
private emitConnectionStateChange(deviceId: string, state: string) {
for (const listener of this.connectionStateChangeListeners) {
try {
listener(deviceId, state);
} catch (e) {
// 忽略单个回调异常
}
}
}
async connectDevice(deviceId: string, options?: BleConnectOptionsExt): Promise<boolean> {
this.autoReconnect = options?.autoReconnect ?? false;
try {
const key = this.resolveDeviceKey(deviceId)
console.log(key,deviceId)
if (!key) {
// better debugging: include a short sample of known device keys
const known = Object.keys(this.devices || {}).slice(0, 20)
throw new Error(`设备未找到: ${deviceId}; 已知设备: ${known.join(',')}`)
}
const device = this.devices[key]
const server = await device.gatt.connect();
this.servers[key] = server;
this.connectionStates[key] = BLE_CONNECTION_STATE.CONNECTED;
this.reconnectAttempts = 0;
this.emitConnectionStateChange(key, 'connected');
// 监听物理断开
if (device.gatt) {
device.gatt.onconnectionstatechanged = null;
device.gatt.onconnectionstatechanged = () => {
if (!device.gatt.connected) {
this.emitConnectionStateChange(key, 'disconnected');
}
};
}
return true;
} catch (error) {
if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
return this.scheduleReconnect(deviceId);
}
throw error;
}
}
async disconnectDevice(deviceId: string): Promise<void> {
const key = this.resolveDeviceKey(deviceId)
if (!key) throw new Error('设备未找到')
const device = this.devices[key]
try {
if (device.gatt && device.gatt.connected) {
device.gatt.disconnect();
}
delete this.servers[key];
this.connectionStates[key] = BLE_CONNECTION_STATE.DISCONNECTED;
this.emitConnectionStateChange(key, 'disconnected');
} catch (e) {
throw e;
}
}
getConnectedDevices(): BleDevice[] {
const connectedDevices: BleDevice[] = [];
for (const deviceId in this.servers) {
const device = this.devices[deviceId];
if (device) {
connectedDevices.push({ deviceId: device.id, name: device.name || '未知设备', connected: true });
}
}
return connectedDevices;
}
handleDisconnect(deviceId: string) {
const key = this.resolveDeviceKey(deviceId)
const idKey = key ?? deviceId
this.connectionStates[idKey] = BLE_CONNECTION_STATE.DISCONNECTED;
this.emitConnectionStateChange(idKey, 'disconnected');
if (this.autoReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
this.scheduleReconnect(idKey);
}
}
private scheduleReconnect(deviceId: string): Promise<boolean> {
this.reconnectAttempts++;
return new Promise((resolve, reject) => {
this.reconnectTimeoutId = setTimeout(() => {
this.connectDevice(deviceId, { autoReconnect: true })
.then(resolve)
.catch(reject);
}, this.reconnectDelay);
});
}
// Resolve a device key used in this.devices map from a given deviceId.
// Accepts formatted IDs (with ':'), raw ids or case-insensitive hex strings.
private resolveDeviceKey(deviceId: string): string | null {
// Accept either a string id or an object that contains the id (UTSJSONObject or plain object)
if (deviceId == null) return null
let idCandidate: any = deviceId
if (typeof deviceId !== 'string') {
try {
// UTSJSONObject has getString
if (typeof (deviceId as any).getString === 'function') {
const got = (deviceId as any).getString('deviceId') || (deviceId as any).getString('device_id') || (deviceId as any).getString('id')
if (got) idCandidate = got
} else if (typeof deviceId === 'object') {
const got = (deviceId as any).deviceId || (deviceId as any).device_id || (deviceId as any).id
if (got) idCandidate = got
}
} catch (e) { /* ignore extraction errors */ }
}
if (!idCandidate) return null
if (this.devices[idCandidate]) return idCandidate
const normalize = (s: string) => (s || '').toString().replace(/:/g, '').toUpperCase()
const target = normalize(idCandidate)
for (const k in this.devices) {
if (k === deviceId) return k
const dev = this.devices[k]
try {
if (dev && dev.id && normalize(dev.id) === target) return k
} catch (e) { }
if (normalize(k) === target) return k
}
return null
}
cancelReconnect() {
if (this.reconnectTimeoutId) {
clearTimeout(this.reconnectTimeoutId);
this.reconnectTimeoutId = 0;
}
this.autoReconnect = false;
this.reconnectAttempts = 0;
}
setMaxReconnectAttempts(attempts: number) {
this.maxReconnectAttempts = attempts;
}
setReconnectDelay(delay: number) {
this.reconnectDelay = delay;
}
isDeviceConnected(deviceId: string): boolean {
return !!this.servers[deviceId];
}
// Public helper to obtain the GATT server for a device id using flexible matching
getServer(deviceId: string): any | null {
const key = this.resolveDeviceKey(deviceId)
if (!key) return null
return this.servers[key] ?? null
}
}