import { BleService } from '../interface.uts' import type { WriteCharacteristicOptions, DfuOptions, ControlParserResult } from '../interface.uts' import { DeviceManager } from './device_manager.uts' import { ServiceManager } from './service_manager.uts' import BluetoothGatt from 'android.bluetooth.BluetoothGatt' import BluetoothGattCharacteristic from 'android.bluetooth.BluetoothGattCharacteristic' import BluetoothGattDescriptor from 'android.bluetooth.BluetoothGattDescriptor' import UUID from 'java.util.UUID' // 通用 Nordic DFU UUIDs (常见设备可能使用这些;如厂商自定义请替换) const DFU_SERVICE_UUID = '0000fe59-0000-1000-8000-00805f9b34fb' const DFU_CONTROL_POINT_UUID = '8ec90001-f315-4f60-9fb8-838830daea50' const DFU_PACKET_UUID = '8ec90002-f315-4f60-9fb8-838830daea50' type DfuSession = { resolve : () => void; reject : (err ?: any) => void; onProgress ?: (p : number) => void; onLog ?: (s : string) => void; controlParser ?: (data : Uint8Array) => ControlParserResult | null; // Nordic 专用字段 bytesSent ?: number; totalBytes ?: number; useNordic ?: boolean; // PRN (packet receipt notification) support prn ?: number; packetsSincePrn ?: number; prnResolve ?: () => void; prnReject ?: (err ?: any) => void; } export class DfuManager { // 会话表,用于把 control-point 通知路由到当前 DFU 流程 private sessions : Map = new Map(); // 简化:只实现最基本的 GATT-based DFU 上传逻辑,需按设备协议调整 control point 的命令/解析 // Emit a DFU lifecycle event for a session. name should follow Nordic listener names private _emitDfuEvent(deviceId : string, name : string, payload ?: any) { console.log('[DFU][Event]', name, deviceId, payload ?? ''); const s = this.sessions.get(deviceId); if (s == null) return; if (typeof s.onLog == 'function') { try { const logFn = s.onLog as (msg : string) => void; logFn(`[${name}] ${payload != null ? JSON.stringify(payload) : ''}`); } catch (e) { } } if (name == 'onProgress' && typeof s.onProgress == 'function' && typeof payload == 'number') { try { s.onProgress(payload); } catch (e) { } } } async startDfu(deviceId : string, firmwareBytes : Uint8Array, options ?: DfuOptions) : Promise { console.log('startDfu 0') const deviceManager = DeviceManager.getInstance(); const serviceManager = ServiceManager.getInstance(); console.log('startDfu 1') const gatt : BluetoothGatt | null = deviceManager.getGattInstance(deviceId); console.log('startDfu 2') if (gatt == null) throw new Error('Device not connected'); console.log('[DFU] startDfu start deviceId=', deviceId, 'firmwareBytes=', firmwareBytes != null ? firmwareBytes.length : 0, 'options=', options); try { console.log('[DFU] requesting high connection priority for', deviceId); gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH); } catch (e) { console.warn('[DFU] requestConnectionPriority failed', e); } // 发现服务并特征 // ensure services discovered before accessing GATT; serviceManager exposes Promise-based API await serviceManager.getServices(deviceId, null); console.log('[DFU] services ensured for', deviceId); const dfuService = gatt.getService(UUID.fromString(DFU_SERVICE_UUID)); if (dfuService == null) throw new Error('DFU service not found'); const controlChar = dfuService.getCharacteristic(UUID.fromString(DFU_CONTROL_POINT_UUID)); const packetChar = dfuService.getCharacteristic(UUID.fromString(DFU_PACKET_UUID)); console.log('[DFU] dfuService=', dfuService != null ? dfuService.getUuid().toString() : null, 'controlChar=', controlChar != null ? controlChar.getUuid().toString() : null, 'packetChar=', packetChar != null ? packetChar.getUuid().toString() : null); if (controlChar == null || packetChar == null) throw new Error('DFU characteristics missing'); const packetProps = packetChar.getProperties(); const supportsWriteWithResponse = (packetProps & BluetoothGattCharacteristic.PROPERTY_WRITE) != 0; const supportsWriteNoResponse = (packetProps & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0; console.log('[DFU] packet characteristic props mask=', packetProps, 'supportsWithResponse=', supportsWriteWithResponse, 'supportsNoResponse=', supportsWriteNoResponse); // Allow caller to request a desired MTU via options for higher throughput const desiredMtu = (options != null && typeof options.mtu == 'number') ? options.mtu : 247; try { console.log('[DFU] requesting MTU=', desiredMtu, 'for', deviceId); await this._requestMtu(gatt, desiredMtu, 8000); console.log('[DFU] requestMtu completed for', deviceId); } catch (e) { console.warn('[DFU] requestMtu failed or timed out, continue with default.', e); } const mtu = desiredMtu; // 假定成功或使用期望值 const chunkSize = Math.max(20, mtu - 3); // small helper to convert a byte (possibly signed) to a two-digit hex string const byteToHex = (b : number) => { const v = (b < 0) ? (b + 256) : b; let s = v.toString(16); if (s.length < 2) s = '0' + s; return s; }; // Parameterize PRN window and timeout via options early so they are available // for session logging. Defaults: prn = 12 packets, prnTimeoutMs = 10000 ms let prnWindow = 0; if (options != null && typeof options.prn == 'number') { prnWindow = Math.max(0, Math.floor(options.prn)); } const prnTimeoutMs = (options != null && typeof options.prnTimeoutMs == 'number') ? Math.max(1000, Math.floor(options.prnTimeoutMs)) : 8000; const disablePrnOnTimeout = !(options != null && options.disablePrnOnTimeout == false); // 订阅 control point 通知并将通知路由到会话处理器 const controlHandler = (data : Uint8Array) => { // 交给会话处理器解析并触发事件 try { const hexParts: string[] = []; for (let i = 0; i < data.length; i++) { const v = data[i] as number; hexParts.push(byteToHex(v)); } const hex = hexParts.join(' '); console.log('[DFU] control notification callback invoked for', deviceId, 'raw=', Array.from(data), 'hex=', hex); } catch (e) { console.log('[DFU] control notification callback invoked for', deviceId, 'raw=', Array.from(data)); } this._handleControlNotification(deviceId, data); }; console.log('[DFU] subscribing control point for', deviceId); await serviceManager.subscribeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID, controlHandler); console.log('[DFU] subscribeCharacteristic returned for', deviceId); // 保存会话回调(用于 waitForControlEvent); 支持 Nordic 模式追踪已发送字节 this.sessions.set(deviceId, { resolve: () => { }, reject: (err ?: any) => {console.log(err) }, onProgress: null, onLog: null, controlParser: (data : Uint8Array) => this._defaultControlParser(data), bytesSent: 0, totalBytes: firmwareBytes.length, useNordic: options != null && options.useNordic == true, prn: null, packetsSincePrn: 0, prnResolve: null, prnReject: null }); console.log('[DFU] session created for', deviceId, 'totalBytes=', firmwareBytes.length); console.log('[DFU] DFU session details:', { deviceId: deviceId, totalBytes: firmwareBytes.length, chunkSize: chunkSize, prnWindow: prnWindow, prnTimeoutMs: prnTimeoutMs }); // wire options callbacks into the session (if provided) const sessRef = this.sessions.get(deviceId); if (sessRef != null) { sessRef.onProgress = (options != null && typeof options.onProgress == 'function') ? options.onProgress : null; sessRef.onLog = (options != null && typeof options.onLog == 'function') ? options.onLog : null; } // emit initial lifecycle events (Nordic-like) this._emitDfuEvent(deviceId, 'onDeviceConnecting', null); // 写入固件数据(非常保守的实现:逐包写入并等待短延迟) // --- PRN setup (optional, Nordic-style flow) --- // Parameterize PRN window and timeout via options: options.prn, options.prnTimeoutMs // Defaults were set earlier; build PRN payload using arithmetic to avoid // bitwise operators which don't map cleanly to generated Kotlin. if (prnWindow > 0) { try { // send Set PRN to device (format: [OP_CODE_SET_PRN, prn LSB, prn MSB]) // WARNING: Ensure your device uses the same opcode/format; change if needed. const prnLsb = prnWindow % 256; const prnMsb = Math.floor(prnWindow / 256) % 256; const prnPayload = new Uint8Array([0x02, prnLsb, prnMsb]); await serviceManager.writeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID, prnPayload, null); const sess0 = this.sessions.get(deviceId); if (sess0 != null) { sess0.useNordic = true; sess0.prn = prnWindow; sess0.packetsSincePrn = 0; sess0.controlParser = (data : Uint8Array) => this._nordicControlParser(data); } console.log('[DFU] Set PRN sent (prn=', prnWindow, ') for', deviceId); } catch (e) { console.warn('[DFU] Set PRN failed (continuing without PRN):', e); const sessFallback = this.sessions.get(deviceId); if (sessFallback != null) sessFallback.prn = 0; } } else { console.log('[DFU] PRN disabled (prnWindow=', prnWindow, ') for', deviceId); } // 写入固件数据(逐包写入并根据 options.waitForResponse 选择是否等待响应) let offset = 0; const total = firmwareBytes.length; this._emitDfuEvent(deviceId, 'onDfuProcessStarted', null); this._emitDfuEvent(deviceId, 'onUploadingStarted', null); // Track outstanding write operations when using fire-and-forget mode so we can // log and throttle if the Android stack becomes overwhelmed. let outstandingWrites = 0; // read tuning parameters from options in a safe, generator-friendly way let configuredMaxOutstanding = 2; let writeSleepMs = 0; let writeRetryDelay = 100; let writeMaxAttempts = 12; let writeGiveupTimeout = 60000; let drainOutstandingTimeout = 3000; let failureBackoffMs = 0; // throughput measurement let throughputWindowBytes = 0; let lastThroughputTime = Date.now(); function _logThroughputIfNeeded(force ?: boolean) { try { const now = Date.now(); const elapsed = now - lastThroughputTime; if (force == true || elapsed >= 1000) { const bytes = throughputWindowBytes; const bps = Math.floor((bytes * 1000) / Math.max(1, elapsed)); // reset window throughputWindowBytes = 0; lastThroughputTime = now; const human = `${bps} B/s`; console.log('[DFU] throughput:', human, 'elapsedMs=', elapsed); const s = this.sessions.get(deviceId); if (s != null && typeof s.onLog == 'function') { try { s.onLog?.invoke('[DFU] throughput: ' + human); } catch (e) { } } } } catch (e) { } } function _safeErr(e ?: any) { try { if (e == null) return ''; if (typeof e == 'string') return e; try { return JSON.stringify(e); } catch (e2) { } try { return (e as any).toString(); } catch (e3) { } return ''; } catch (e4) { return ''; } } try { if (options != null) { try { if (options.maxOutstanding != null) { const parsed = Math.floor(options.maxOutstanding as number); if (!isNaN(parsed) && parsed > 0) configuredMaxOutstanding = parsed; } } catch (e) { } try { if (options.writeSleepMs != null) { const parsedWs = Math.floor(options.writeSleepMs as number); if (!isNaN(parsedWs) && parsedWs >= 0) writeSleepMs = parsedWs; } } catch (e) { } try { if (options.writeRetryDelayMs != null) { const parsedRetry = Math.floor(options.writeRetryDelayMs as number); if (!isNaN(parsedRetry) && parsedRetry >= 0) writeRetryDelay = parsedRetry; } } catch (e) { } try { if (options.writeMaxAttempts != null) { const parsedAttempts = Math.floor(options.writeMaxAttempts as number); if (!isNaN(parsedAttempts) && parsedAttempts > 0) writeMaxAttempts = parsedAttempts; } } catch (e) { } try { if (options.writeGiveupTimeoutMs != null) { const parsedGiveupTimeout = Math.floor(options.writeGiveupTimeoutMs as number); if (!isNaN(parsedGiveupTimeout) && parsedGiveupTimeout > 0) writeGiveupTimeout = parsedGiveupTimeout; } } catch (e) { } try { if (options.drainOutstandingTimeoutMs != null) { const parsedDrain = Math.floor(options.drainOutstandingTimeoutMs as number); if (!isNaN(parsedDrain) && parsedDrain >= 0) drainOutstandingTimeout = parsedDrain; } } catch (e) { } } if (configuredMaxOutstanding < 1) configuredMaxOutstanding = 1; if (writeSleepMs < 0) writeSleepMs = 0; } catch (e) { } if (supportsWriteWithResponse == false && supportsWriteNoResponse == true) { if (configuredMaxOutstanding > 1) configuredMaxOutstanding = 1; if (writeSleepMs < 15) writeSleepMs = 15; if (writeRetryDelay < 150) writeRetryDelay = 150; if (writeMaxAttempts < 40) writeMaxAttempts = 40; if (writeGiveupTimeout < 120000) writeGiveupTimeout = 120000; console.log('[DFU] packet char only supports WRITE_NO_RESPONSE; serializing writes with conservative pacing'); } const maxOutstandingCeiling = configuredMaxOutstanding; let adaptiveMaxOutstanding = configuredMaxOutstanding; const minOutstandingWindow = 1; while (offset < total) { const end = Math.min(offset + chunkSize, total); const slice = firmwareBytes.subarray(offset, end); // Decide whether to wait for response per-chunk. Honor characteristic support. // Generator-friendly: avoid 'undefined' and use explicit boolean check. let finalWaitForResponse = true; if (options != null) { try { const maybe = options.waitForResponse; if (maybe == true && supportsWriteWithResponse == false && supportsWriteNoResponse == true) { // caller requested response but characteristic cannot provide it; keep true to ensure we await the write promise. finalWaitForResponse = true; } else if (maybe == false) { finalWaitForResponse = false; } else if (maybe == true) { finalWaitForResponse = true; } } catch (e) { finalWaitForResponse = true; } } const writeOpts: WriteCharacteristicOptions = { waitForResponse: finalWaitForResponse, retryDelayMs: writeRetryDelay, maxAttempts: writeMaxAttempts, giveupTimeoutMs: writeGiveupTimeout, forceWriteTypeNoResponse: finalWaitForResponse == false }; console.log('[DFU] writing packet chunk offset=', offset, 'len=', slice.length, 'waitForResponse=', finalWaitForResponse, 'outstanding=', outstandingWrites); // Fire-and-forget path: do not await the write if waitForResponse == false. if (finalWaitForResponse == false) { if (failureBackoffMs > 0) { console.log('[DFU] applying failure backoff', failureBackoffMs, 'ms before next write for', deviceId); await this._sleep(failureBackoffMs); failureBackoffMs = Math.floor(failureBackoffMs / 2); } while (outstandingWrites >= adaptiveMaxOutstanding) { await this._sleep(Math.max(1, writeSleepMs)); } // increment outstanding counter and kick the write without awaiting. outstandingWrites = outstandingWrites + 1; // fire-and-forget: start the write but don't await its Promise const writeOffset = offset; serviceManager.writeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_PACKET_UUID, slice, writeOpts).then((res) => { outstandingWrites = Math.max(0, outstandingWrites - 1); if (res == true) { if (adaptiveMaxOutstanding < maxOutstandingCeiling) { adaptiveMaxOutstanding = Math.min(maxOutstandingCeiling, adaptiveMaxOutstanding + 1); } if (failureBackoffMs > 0) failureBackoffMs = Math.floor(failureBackoffMs / 2); } // log occasional completions if ((outstandingWrites & 0x1f) == 0) { console.log('[DFU] write completion callback, outstandingWrites=', outstandingWrites, 'adaptiveWindow=', adaptiveMaxOutstanding, 'device=', deviceId); } // detect write failure signaled by service manager if (res !== true) { adaptiveMaxOutstanding = Math.max(minOutstandingWindow, Math.floor(adaptiveMaxOutstanding / 2)); failureBackoffMs = Math.min(200, Math.max(failureBackoffMs, Math.max(5, writeRetryDelay))); console.error('[DFU] writeCharacteristic returned false for device=', deviceId, 'offset=', writeOffset, 'adaptiveWindow now=', adaptiveMaxOutstanding); try { this._emitDfuEvent(deviceId, 'onError', { phase: 'write', offset: writeOffset, reason: 'write returned false' }); } catch (e) { } } }).catch((e) => { outstandingWrites = Math.max(0, outstandingWrites - 1); adaptiveMaxOutstanding = Math.max(minOutstandingWindow, Math.floor(adaptiveMaxOutstanding / 2)); failureBackoffMs = Math.min(200, Math.max(failureBackoffMs, Math.max(5, writeRetryDelay))); console.warn('[DFU] fire-and-forget write failed for device=', deviceId, e, 'adaptiveWindow now=', adaptiveMaxOutstanding); try { const errMsg ='[DFU] fire-and-forget write failed for device='; this._emitDfuEvent(deviceId, 'onError', { phase: 'write', offset: writeOffset, reason: errMsg }); } catch (e2) { } }); // account bytes for throughput throughputWindowBytes += slice.length; _logThroughputIfNeeded(false); } else { console.log('[DFU] awaiting write for chunk offset=', offset); try { const writeResult = await serviceManager.writeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_PACKET_UUID, slice, writeOpts); if (writeResult !== true) { console.error('[DFU] writeCharacteristic(await) returned false at offset=', offset, 'device=', deviceId); try { this._emitDfuEvent(deviceId, 'onError', { phase: 'write', offset: offset, reason: 'write returned false' }); } catch (e) { } // abort DFU by throwing throw new Error('write failed'); } } catch (e) { console.error('[DFU] awaiting write failed at offset=', offset, 'device=', deviceId, e); try { const errMsg = '[DFU] awaiting write failed '; this._emitDfuEvent(deviceId, 'onError', { phase: 'write', offset: offset, reason: errMsg }); } catch (e2) { } throw e; } // account bytes for throughput throughputWindowBytes += slice.length; _logThroughputIfNeeded(false); } // update PRN counters and wait when window reached const sessAfter = this.sessions.get(deviceId); if (sessAfter != null && sessAfter.useNordic == true && typeof sessAfter.prn == 'number' && (sessAfter.prn ?? 0) > 0) { sessAfter.packetsSincePrn = (sessAfter.packetsSincePrn ?? 0) + 1; if ((sessAfter.packetsSincePrn ?? 0) >= (sessAfter.prn ?? 0) && (sessAfter.prn ?? 0) > 0) { // wait for PRN (device notification) before continuing try { console.log('[DFU] reached PRN window, waiting for PRN for', deviceId, 'packetsSincePrn=', sessAfter.packetsSincePrn, 'prn=', sessAfter.prn); await this._waitForPrn(deviceId, prnTimeoutMs); console.log('[DFU] PRN received, resuming transfer for', deviceId); } catch (e) { console.warn('[DFU] PRN wait failed/timed out, continuing anyway for', deviceId, e); if (disablePrnOnTimeout) { console.warn('[DFU] disabling PRN waits after timeout for', deviceId); sessAfter.prn = 0; } } // reset counter sessAfter.packetsSincePrn = 0; } } offset = end; // 如果启用 nordic 模式,统计已发送字节 const sess = this.sessions.get(deviceId); if (sess != null && typeof sess.bytesSent == 'number') { sess.bytesSent = (sess.bytesSent ?? 0) + slice.length; } // 简单节流与日志,避免过快。默认睡眠非常短以提高吞吐量; 可在设备上调节 console.log('[DFU] wrote chunk for', deviceId, 'offset=', offset, '/', total, 'chunkSize=', slice.length, 'bytesSent=', sess != null ? sess.bytesSent : null, 'outstanding=', outstandingWrites); // emit upload progress event (percent) if available if (sess != null && typeof sess.bytesSent == 'number' && typeof sess.totalBytes == 'number') { const p = Math.floor((sess.bytesSent / sess.totalBytes) * 100); this._emitDfuEvent(deviceId, 'onProgress', p); } // yield to event loop and avoid starving the Android BLE stack await this._sleep(Math.max(0, writeSleepMs)); } // wait for outstanding writes to drain before continuing with control commands if (outstandingWrites > 0) { const drainStart = Date.now(); while (outstandingWrites > 0 && (Date.now() - drainStart) < drainOutstandingTimeout) { await this._sleep(Math.max(0, writeSleepMs)); } if (outstandingWrites > 0) { console.warn('[DFU] outstandingWrites remain after drain timeout, continuing with', outstandingWrites); } else { console.log('[DFU] outstandingWrites drained before control phase'); } } this._emitDfuEvent(deviceId, 'onUploadingCompleted', null); // force final throughput log before activate/validate _logThroughputIfNeeded(true); // 发送 activate/validate 命令到 control point(需根据设备协议实现) // 下面为占位:请替换为实际的 opcode // 在写入前先启动控制结果等待,防止设备快速响应导致丢失通知 const controlTimeout = 20000; const controlResultPromise = this._waitForControlResult(deviceId, controlTimeout); try { // control writes: pass undefined options explicitly to satisfy the generator/typechecker const activatePayload = new Uint8Array([0x04]); console.log('[DFU] sending activate/validate payload=', Array.from(activatePayload)); await serviceManager.writeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID, activatePayload, null); console.log('[DFU] activate/validate write returned for', deviceId); } catch (e) { // 写入失败时取消控制等待,避免悬挂定时器 try { const sessOnWriteFail = this.sessions.get(deviceId); if (sessOnWriteFail != null && typeof sessOnWriteFail.reject == 'function') { sessOnWriteFail.reject(e); } } catch (rejectErr) { } await controlResultPromise.catch(() => { }); try { await serviceManager.unsubscribeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID); } catch (e2) { } this.sessions.delete(deviceId); throw e; } console.log('[DFU] sent control activate/validate command to control point for', deviceId); this._emitDfuEvent(deviceId, 'onValidating', null); // 等待 control-point 返回最终结果(成功或失败),超时可配置 console.log('[DFU] waiting for control result (timeout=', controlTimeout, ') for', deviceId); try { await controlResultPromise; console.log('[DFU] control result resolved for', deviceId); } catch (err) { // 清理订阅后抛出 try { await serviceManager.unsubscribeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID); } catch (e) { } this.sessions.delete(deviceId); throw err; } // 取消订阅 try { await serviceManager.unsubscribeCharacteristic(deviceId, DFU_SERVICE_UUID, DFU_CONTROL_POINT_UUID); } catch (e) { } console.log('[DFU] unsubscribed control point for', deviceId); // 清理会话 this.sessions.delete(deviceId); console.log('[DFU] session cleared for', deviceId); return; } async _requestMtu(gatt : BluetoothGatt, mtu : number, timeoutMs : number) : Promise { return new Promise((resolve, reject) => { // 在当前项目,BluetoothGattCallback.onMtuChanged 未被封装;简单发起请求并等待短超时 try { const ok = gatt.requestMtu(Math.floor(mtu) as Int); if (!ok) { return reject(new Error('requestMtu failed')); } } catch (e) { return reject(e); } // 无 callback 监听时退回,等待一小段时间以便成功 setTimeout(() => resolve(), Math.min(2000, timeoutMs)); }); } _sleep(ms : number) { return new Promise((r) => { setTimeout(() => { r() }, ms); }); } _waitForPrn(deviceId : string, timeoutMs : number) : Promise { const session = this.sessions.get(deviceId); if (session == null) return Promise.reject(new Error('no dfu session')); return new Promise((resolve, reject) => { const timer = setTimeout(() => { // timeout waiting for PRN // clear pending handlers session.prnResolve = null; session.prnReject = null; reject(new Error('PRN timeout')); }, timeoutMs); const prnResolve = () => { clearTimeout(timer); resolve(); }; const prnReject = (err ?: any) => { clearTimeout(timer); reject(err); }; session.prnResolve = prnResolve; session.prnReject = prnReject; }); } // 默认 control point 解析器(非常通用的尝试解析:如果设备发送 progress byte 或成功码) _defaultControlParser(data : Uint8Array) : ControlParserResult | null { // 假设协议:第一个字节为 opcode, 第二字节可为状态或进度 if (data == null || data.length === 0) return null; const op = data[0]; // Nordic-style response: [0x10, requestOp, resultCode] if (op === 0x10 && data.length >= 3) { const requestOp = data[1]; const resultCode = data[2]; if (resultCode === 0x01) { return { type: 'success' }; } return { type: 'error', error: { requestOp: requestOp, resultCode: resultCode, raw: Array.from(data) } }; } // Nordic PRN notification: [0x11, LSB, MSB] -> return as progress (bytes received) if (op === 0x11 && data.length >= 3) { const lsb = data[1]; const msb = data[2]; const received = (msb << 8) | lsb; return { type: 'progress', progress: received }; } // vendor-specific opcode example: 0x60 may mean 'response/progress' for some firmwares if (op === 0x60) { if (data.length >= 3) { const requestOp = data[1]; const status = data[2]; // Known success/status codes observed in field devices if (status === 0x00 || status === 0x01 || status === 0x0A) { return { type: 'success' }; } return { type: 'error', error: { requestOp: requestOp, resultCode: status, raw: Array.from(data) } }; } if (data.length >= 2) { return { type: 'progress', progress: data[1] }; } } // 通用进度回退:若第二字节位于 0-100 之间,当作百分比 if (data.length >= 2) { const maybeProgress = data[1]; if (maybeProgress >= 0 && maybeProgress <= 100) { return { type: 'progress', progress: maybeProgress }; } } // 若找到明显的 success opcode (示例 0x01) 或 error 0xFF if (op === 0x01) return { type: 'success' }; if (op === 0xFF) return { type: 'error', error: data }; return { type: 'info' }; } // Nordic DFU control-parser(支持 Response and Packet Receipt Notification) _nordicControlParser(data : Uint8Array) : ControlParserResult | null { // Nordic opcodes (简化): // - 0x10 : Response (opcode, requestOp, resultCode) // - 0x11 : Packet Receipt Notification (opcode, value LSB, value MSB) if (data == null || data.length == 0) return null; const op = data[0]; if (op == 0x11 && data.length >= 3) { // packet receipt notif: bytes received (little endian) const lsb = data[1]; const msb = data[2]; const received = (msb << 8) | lsb; // Return received bytes as progress value; parser does not resolve device-specific session here. return { type: 'progress', progress: received }; } // Nordic vendor-specific progress/response opcode (example 0x60) if (op == 0x60) { if (data.length >= 3) { const requestOp = data[1]; const status = data[2]; if (status == 0x00 || status == 0x01 || status == 0x0A) { return { type: 'success' }; } return { type: 'error', error: { requestOp, resultCode: status, raw: Array.from(data) } }; } if (data.length >= 2) { return { type: 'progress', progress: data[1] }; } } // Response: check result code for success (0x01 may indicate success in some stacks) if (op == 0x10 && data.length >= 3) { const requestOp = data[1]; const resultCode = data[2]; // Nordic resultCode 0x01 = SUCCESS typically if (resultCode == 0x01) return { type: 'success' }; else return { type: 'error', error: { requestOp, resultCode } }; } return null; } _handleControlNotification(deviceId : string, data : Uint8Array) { const session = this.sessions.get(deviceId); if (session == null) { console.warn('[DFU] control notification received but no session for', deviceId, 'data=', Array.from(data)); return; } try { // human readable opcode mapping let opcodeName = 'unknown'; switch (data[0]) { case 0x10: opcodeName = 'Response'; break; case 0x11: opcodeName = 'PRN'; break; case 0x60: opcodeName = 'VendorProgress'; break; case 0x01: opcodeName = 'SuccessOpcode'; break; case 0xFF: opcodeName = 'ErrorOpcode'; break; } console.log('[DFU] _handleControlNotification deviceId=', deviceId, 'opcode=0x' + data[0].toString(16), 'name=', opcodeName, 'raw=', Array.from(data)); const parsed = session.controlParser != null ? session.controlParser(data) : null; if (session.onLog != null) session.onLog('DFU control notify: ' + Array.from(data).join(',')); console.log('[DFU] parsed control result=', parsed); if (parsed == null) return; if (parsed.type == 'progress' && parsed.progress != null) { // 如果在 nordic 模式 parsed.progress 可能是已接收字节数,则转换为百分比 if (session.useNordic == true && session.totalBytes != null && session.totalBytes > 0) { const percent = Math.floor((parsed.progress / session.totalBytes) * 100); session.onProgress?.(percent); // If we have written all bytes locally, log that event if (session.bytesSent != null && session.totalBytes != null && session.bytesSent >= session.totalBytes) { console.log('[DFU] all bytes written locally for', deviceId, 'bytesSent=', session.bytesSent, 'total=', session.totalBytes); // emit uploading completed once this._emitDfuEvent(deviceId, 'onUploadingCompleted', null); } // If a PRN wait is pending, resolve it (PRN indicates device received packets) if (typeof session.prnResolve == 'function') { try { session.prnResolve(); } catch (e) { } session.prnResolve = null; session.prnReject = null; session.packetsSincePrn = 0; } } else { const progress = parsed.progress if (progress != null) { console.log('[DFU] progress for', deviceId, 'progress=', progress); session.onProgress?.(progress); // also resolve PRN if was waiting (in case device reports numeric progress) if (typeof session.prnResolve == 'function') { try { session.prnResolve(); } catch (e) { } session.prnResolve = null; session.prnReject = null; session.packetsSincePrn = 0; } } } } else if (parsed.type == 'success') { console.log('[DFU] parsed success for', deviceId, 'resolving session'); session.resolve(); // Log final device-acknowledged success console.log('[DFU] device reported DFU success for', deviceId); this._emitDfuEvent(deviceId, 'onDfuCompleted', null); } else if (parsed.type == 'error') { console.error('[DFU] parsed error for', deviceId, parsed.error); session.reject(parsed.error ?? new Error('DFU device error')); this._emitDfuEvent(deviceId, 'onError', parsed.error ?? {}); } else { // info - just log } } catch (e) { session.onLog?.('control parse error: ' + e); console.error('[DFU] control parse exception for', deviceId, e); } } _waitForControlResult(deviceId : string, timeoutMs : number) : Promise { const session = this.sessions.get(deviceId); if (session == null) return Promise.reject(new Error('no dfu session')); return new Promise((resolve, reject) => { // wrap resolve/reject to clear timer const timer = setTimeout(() => { // 超时 console.error('[DFU] _waitForControlResult timeout for', deviceId); reject(new Error('DFU control timeout')); }, timeoutMs); const origResolve = () => { clearTimeout(timer); console.log('[DFU] _waitForControlResult resolved for', deviceId); resolve(); }; const origReject = (err ?: any) => { clearTimeout(timer); console.error('[DFU] _waitForControlResult rejected for', deviceId, 'err=', err); reject(err); }; // replace session handlers temporarily (guard nullable) if (session != null) { session.resolve = origResolve; session.reject = origReject; } }); } } export const dfuManager = new DfuManager();