Files
akmon/uni_modules/ak-sbsrv/utssdk/web/dfu_manager.uts
2026-01-20 08:04:15 +08:00

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();