137 lines
5.3 KiB
Plaintext
137 lines
5.3 KiB
Plaintext
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<void> {
|
|
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<void>(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();
|