Initial commit
This commit is contained in:
136
uni_modules/ak-sbsrv/utssdk/web/dfu_manager.uts
Normal file
136
uni_modules/ak-sbsrv/utssdk/web/dfu_manager.uts
Normal file
@@ -0,0 +1,136 @@
|
||||
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();
|
||||
Reference in New Issue
Block a user