import * as BluetoothManager from './bluetooth_manager.uts' // 默认 Nordic DFU UUIDs (web 模式也可用,如设备使用自定义请传入 options) const DFU_SERVICE_UUID = '00001530-1212-EFDE-1523-785FEABCD123' const DFU_CONTROL_POINT_UUID = '00001531-1212-EFDE-1523-785FEABCD123' const DFU_PACKET_UUID = '00001532-1212-EFDE-1523-785FEABCD123' export class WebDfuManager { // startDfu: deviceId, firmwareBytes (Uint8Array), options // options: { serviceId?, writeCharId?, notifyCharId?, chunkSize?, onProgress?, onLog?, useNordic?, controlParser?, controlTimeout? } async startDfu(deviceId: string, firmwareBytes: Uint8Array, options?: any): Promise { options = options || {}; // 1. ensure connected and discover services let svcInfo; if (options.serviceId && options.writeCharId && options.notifyCharId) { svcInfo = { serviceId: options.serviceId, writeCharId: options.writeCharId, notifyCharId: options.notifyCharId }; } else { svcInfo = await BluetoothManager.autoConnect(deviceId); } const serviceId = svcInfo.serviceId; const writeCharId = svcInfo.writeCharId; const notifyCharId = svcInfo.notifyCharId; const chunkSize = options.chunkSize ?? 20; // control parser const controlParser = options.controlParser ?? (options.useNordic ? this._nordicControlParser.bind(this) : this._defaultControlParser.bind(this)); // subscribe notifications on control/notify char let finalizeSub; let resolved = false; const promise = new Promise(async (resolve, reject) => { const cb = (payload) => { try { const data = payload.data instanceof Uint8Array ? payload.data : new Uint8Array(payload.data); options.onLog?.('control notify: ' + Array.from(data).join(',')); const parsed = controlParser(data); if (!parsed) return; if (parsed.type === 'progress' && parsed.progress != null) { if (options.useNordic && svcInfo && svcInfo.totalBytes) { const percent = Math.floor((parsed.progress / svcInfo.totalBytes) * 100); options.onProgress?.(percent); } else { options.onProgress?.(parsed.progress); } } else if (parsed.type === 'success') { resolved = true; resolve(); } else if (parsed.type === 'error') { reject(parsed.error ?? new Error('DFU device error')); } } catch (e) { options.onLog?.('control handler error: ' + e); } }; await BluetoothManager.subscribeCharacteristic(deviceId, serviceId, notifyCharId, cb); finalizeSub = async () => { try { await BluetoothManager.subscribeCharacteristic(deviceId, serviceId, notifyCharId, () => {}); } catch(e){} }; // write firmware in chunks try { let offset = 0; const total = firmwareBytes.length; // attach totalBytes for nordic if needed svcInfo.totalBytes = total; while (offset < total) { const end = Math.min(offset + chunkSize, total); const slice = firmwareBytes.subarray(offset, end); // writeValue accepts ArrayBuffer await BluetoothManager.writeCharacteristic(deviceId, serviceId, writeCharId, slice.buffer); offset = end; // optimistic progress options.onProgress?.(Math.floor((offset / total) * 100)); await this._sleep(options.chunkDelay ?? 6); } // send validate/activate command to control point (placeholder) try { await BluetoothManager.writeCharacteristic(deviceId, serviceId, writeCharId, new Uint8Array([0x04]).buffer); } catch (e) { // ignore } // wait for control success or timeout const timeoutMs = options.controlTimeout ?? 20000; const t = setTimeout(() => { if (!resolved) reject(new Error('DFU control timeout')); }, timeoutMs); } catch (e) { reject(e); } }); try { await promise; } finally { // unsubscribe notifications try { await BluetoothManager.unsubscribeCharacteristic(deviceId, serviceId, notifyCharId); } catch(e) {} } } _sleep(ms: number) { return new Promise((r) => setTimeout(r, ms)); } _defaultControlParser(data: Uint8Array) { if (!data || data.length === 0) return null; if (data.length >= 2) { const maybeProgress = data[1]; if (maybeProgress >= 0 && maybeProgress <= 100) return { type: 'progress', progress: maybeProgress }; } const op = data[0]; if (op === 0x01) return { type: 'success' }; if (op === 0xFF) return { type: 'error', error: data }; return { type: 'info' }; } _nordicControlParser(data: Uint8Array) { if (!data || data.length === 0) return null; const op = data[0]; // 0x11 = Packet Receipt Notification if (op === 0x11 && data.length >= 3) { const lsb = data[1]; const msb = data[2]; const received = (msb << 8) | lsb; return { type: 'progress', progress: received }; } // 0x10 = Response if (op === 0x10 && data.length >= 3) { const resultCode = data[2]; if (resultCode === 0x01) return { type: 'success' }; return { type: 'error', error: { resultCode } }; } return null; } } export const dfuManager = new WebDfuManager();