@onekeyfe/hd-core 1.2.0-alpha.3 → 1.2.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/__tests__/preInitialize.test.ts +33 -0
  2. package/__tests__/protocol-v2.test.ts +1377 -480
  3. package/dist/api/FileRead.d.ts.map +1 -1
  4. package/dist/api/FileWrite.d.ts.map +1 -1
  5. package/dist/api/FirmwareUpdateV4.d.ts +20 -2
  6. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  7. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  8. package/dist/api/firmware/getBinary.d.ts +4 -0
  9. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  10. package/dist/api/index.d.ts +5 -10
  11. package/dist/api/index.d.ts.map +1 -1
  12. package/dist/api/protocol-v2/DeviceFactoryInfoGet.d.ts +6 -0
  13. package/dist/api/protocol-v2/DeviceFactoryInfoGet.d.ts.map +1 -0
  14. package/dist/api/protocol-v2/DeviceFactoryInfoSet.d.ts +7 -0
  15. package/dist/api/protocol-v2/DeviceFactoryInfoSet.d.ts.map +1 -0
  16. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts +1 -1
  17. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts.map +1 -1
  18. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts +1 -1
  19. package/dist/api/protocol-v2/{DeviceGetDeviceInfo.d.ts → DeviceInfoGet.d.ts} +10 -10
  20. package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -0
  21. package/dist/api/protocol-v2/{FilesystemFixPermission.d.ts → FilesystemPermissionFix.d.ts} +2 -2
  22. package/dist/api/protocol-v2/FilesystemPermissionFix.d.ts.map +1 -0
  23. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts +6 -0
  24. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -0
  25. package/dist/api/protocol-v2/helpers.d.ts +24 -18
  26. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  27. package/dist/api/ton/TonSignData.d.ts +1 -5
  28. package/dist/api/ton/TonSignData.d.ts.map +1 -1
  29. package/dist/core/index.d.ts.map +1 -1
  30. package/dist/data-manager/DataManager.d.ts +1 -1
  31. package/dist/data-manager/DataManager.d.ts.map +1 -1
  32. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  33. package/dist/device/Device.d.ts +4 -1
  34. package/dist/device/Device.d.ts.map +1 -1
  35. package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
  36. package/dist/deviceProfile/buildDeviceProfile.d.ts.map +1 -1
  37. package/dist/index.d.ts +79 -54
  38. package/dist/index.js +1537 -1528
  39. package/dist/inject.d.ts.map +1 -1
  40. package/dist/protocols/protocol-v2/features.d.ts +12 -14
  41. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  42. package/dist/protocols/protocol-v2/firmware.d.ts +10 -9
  43. package/dist/protocols/protocol-v2/firmware.d.ts.map +1 -1
  44. package/dist/types/api/firmwareUpdate.d.ts +1 -6
  45. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  46. package/dist/types/api/getDeviceInfo.d.ts +2 -0
  47. package/dist/types/api/getDeviceInfo.d.ts.map +1 -1
  48. package/dist/types/api/index.d.ts +6 -11
  49. package/dist/types/api/index.d.ts.map +1 -1
  50. package/dist/types/api/protocolV2.d.ts +12 -17
  51. package/dist/types/api/protocolV2.d.ts.map +1 -1
  52. package/dist/types/device.d.ts +1 -0
  53. package/dist/types/device.d.ts.map +1 -1
  54. package/dist/types/params.d.ts +1 -0
  55. package/dist/types/params.d.ts.map +1 -1
  56. package/dist/types/settings.d.ts +29 -2
  57. package/dist/types/settings.d.ts.map +1 -1
  58. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  59. package/dist/utils/patch.d.ts +1 -1
  60. package/dist/utils/patch.d.ts.map +1 -1
  61. package/package.json +4 -4
  62. package/src/api/FileRead.ts +14 -1
  63. package/src/api/FileWrite.ts +14 -1
  64. package/src/api/FirmwareUpdateV4.ts +933 -197
  65. package/src/api/GetOnekeyFeatures.ts +1 -1
  66. package/src/api/device/DeviceRebootToBoardloader.ts +4 -4
  67. package/src/api/device/DeviceRebootToBootloader.ts +4 -4
  68. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +11 -4
  69. package/src/api/index.ts +5 -10
  70. package/src/api/protocol-v2/{FactoryGetDeviceInfo.ts → DeviceFactoryInfoGet.ts} +3 -3
  71. package/src/api/protocol-v2/DeviceFactoryInfoSet.ts +32 -0
  72. package/src/api/protocol-v2/DeviceFirmwareUpdate.ts +3 -5
  73. package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +2 -2
  74. package/src/api/protocol-v2/{DeviceGetDeviceInfo.ts → DeviceInfoGet.ts} +52 -22
  75. package/src/api/protocol-v2/DeviceReboot.ts +1 -1
  76. package/src/api/protocol-v2/{FilesystemFixPermission.ts → FilesystemPermissionFix.ts} +2 -2
  77. package/src/api/protocol-v2/{GetProtoVersion.ts → ProtocolInfoRequest.ts} +2 -2
  78. package/src/api/protocol-v2/helpers.ts +59 -43
  79. package/src/api/ton/TonSignData.ts +1 -5
  80. package/src/core/index.ts +19 -5
  81. package/src/data/messages/messages-protocol-v2.json +486 -1072
  82. package/src/data-manager/DataManager.ts +7 -0
  83. package/src/data-manager/connectSettings.ts +0 -6
  84. package/src/device/Device.ts +25 -14
  85. package/src/deviceProfile/buildDeviceFeatures.ts +60 -63
  86. package/src/deviceProfile/buildDeviceProfile.ts +77 -58
  87. package/src/inject.ts +10 -16
  88. package/src/protocols/protocol-v2/features.ts +26 -63
  89. package/src/protocols/protocol-v2/firmware.ts +11 -36
  90. package/src/types/api/firmwareUpdate.ts +10 -20
  91. package/src/types/api/getDeviceInfo.ts +2 -0
  92. package/src/types/api/index.ts +10 -20
  93. package/src/types/api/protocolV2.ts +22 -42
  94. package/src/types/device.ts +1 -0
  95. package/src/types/params.ts +4 -0
  96. package/src/types/settings.ts +44 -35
  97. package/src/utils/deviceFeaturesUtils.ts +45 -10
  98. package/dist/api/protocol-v2/DevReboot.d.ts +0 -7
  99. package/dist/api/protocol-v2/DevReboot.d.ts.map +0 -1
  100. package/dist/api/protocol-v2/DeviceGetDeviceInfo.d.ts.map +0 -1
  101. package/dist/api/protocol-v2/DeviceGetOnboardingStatus.d.ts +0 -6
  102. package/dist/api/protocol-v2/DeviceGetOnboardingStatus.d.ts.map +0 -1
  103. package/dist/api/protocol-v2/FactoryDeviceInfoSettings.d.ts +0 -7
  104. package/dist/api/protocol-v2/FactoryDeviceInfoSettings.d.ts.map +0 -1
  105. package/dist/api/protocol-v2/FactoryGetDeviceInfo.d.ts +0 -6
  106. package/dist/api/protocol-v2/FactoryGetDeviceInfo.d.ts.map +0 -1
  107. package/dist/api/protocol-v2/FilesystemFixPermission.d.ts.map +0 -1
  108. package/dist/api/protocol-v2/GetProtoVersion.d.ts +0 -6
  109. package/dist/api/protocol-v2/GetProtoVersion.d.ts.map +0 -1
  110. package/src/api/protocol-v2/DevReboot.ts +0 -24
  111. package/src/api/protocol-v2/DeviceGetOnboardingStatus.ts +0 -16
  112. package/src/api/protocol-v2/FactoryDeviceInfoSettings.ts +0 -29
@@ -1,6 +1,6 @@
1
1
  import { ERRORS, HardwareError, HardwareErrorCode, wait } from '@onekeyfe/hd-shared';
2
2
  import {
3
- DevRebootType,
3
+ DeviceRebootType,
4
4
  PROTOCOL_V2_BLE_FILE_CHUNK_SIZE,
5
5
  PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE,
6
6
  } from '@onekeyfe/hd-transport';
@@ -15,14 +15,13 @@ import {
15
15
  getFirmwareType,
16
16
  getLogger,
17
17
  } from '../utils';
18
- import { getBinary, getSysResourceBinary } from './firmware/getBinary';
18
+ import { getSysResourceBinary } from './firmware/getBinary';
19
19
  import { DataManager } from '../data-manager';
20
20
  import { FirmwareUpdateBaseMethod } from './firmware/FirmwareUpdateBaseMethod';
21
21
  import { DevicePool } from '../device/DevicePool';
22
22
  import {
23
23
  PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST,
24
24
  ProtocolV2FirmwareTargetType,
25
- protocolV2FileNameToTargetId,
26
25
  } from '../protocols/protocol-v2';
27
26
  import { requestProtocolV2DeviceInfo } from '../protocols/protocol-v2/features';
28
27
  import {
@@ -35,31 +34,162 @@ import type { FirmwareUpdateV4Params } from '../types/api/firmwareUpdate';
35
34
  import type { EFirmwareType } from '@onekeyfe/hd-shared';
36
35
  import type { PROTO } from '../constants';
37
36
  import type { TypedResponseMessage } from '../device/DeviceCommands';
38
- import type { Features } from '../types';
37
+ import type {
38
+ Features,
39
+ IFirmwareReleaseInfo,
40
+ IProtocolV2FirmwareComponent,
41
+ IProtocolV2ResourceManifestPackage,
42
+ IVersionArray,
43
+ } from '../types';
39
44
 
40
45
  const Log = getLogger(LoggerNames.Method);
41
46
 
42
47
  const SESSION_ERROR = 'session not found';
43
48
  const PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT = 60 * 1000;
44
49
  const PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT = 5 * 1000;
50
+ const PROTOCOL_V2_START_UPDATE_TIMEOUT = 60 * 1000;
45
51
  const PROTOCOL_V2_INSTALL_TIMEOUT = 5 * 60 * 1000;
46
- const PROTOCOL_V2_TARGET_STATUS_FINISHED = 0;
52
+ const PROTOCOL_V2_TARGET_STATUS_PENDING = 0;
47
53
  const PROTOCOL_V2_TARGET_STATUS_IN_PROGRESS = 1;
48
- const PROTOCOL_V2_TARGET_STATUS_FAILED = 2;
54
+ const PROTOCOL_V2_TARGET_STATUS_FINISHED = 2;
55
+ const PROTOCOL_V2_TARGET_STATUS_FAILED_MIN = 3;
49
56
  const PROTOCOL_V2_CONNECT_PROTOCOL = 'V2';
50
57
  const PROTOCOL_V2_FIRMWARE_STAGING_VOLUME = 'vol1:';
51
58
  const PROTOCOL_V2_MIN_FILE_CHUNK_SIZE = 64;
59
+ const PROTOCOL_V2_CONNECT_RETRY_COUNT = 10;
60
+ const PROTOCOL_V2_CONNECT_POLL_INTERVAL = 500;
61
+ const PROTOCOL_V2_CONNECT_SINGLE_TIMEOUT = 75 * 1000;
62
+ const PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT = 60 * 1000;
63
+ const PROTOCOL_V2_OKPP_HEADER_SIZE = 0x52a0;
64
+ const PROTOCOL_V2_OKPP_PAYLOAD_HASH_OFFSET = 0x200;
65
+ const PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET = 0x240;
66
+ const PROTOCOL_V2_OKPP_HASH_SIZE = 64;
67
+
68
+ const getProtocolV2DeviceTransferProgress = (
69
+ bytesBeforeChunk: number,
70
+ bytesAfterChunk: number,
71
+ totalBytes: number
72
+ ) => {
73
+ if (!Number.isFinite(totalBytes) || totalBytes <= 0) {
74
+ return 100;
75
+ }
76
+ if (bytesBeforeChunk <= 0 && bytesAfterChunk < totalBytes) {
77
+ return 0;
78
+ }
79
+ if (bytesAfterChunk >= totalBytes) {
80
+ return 100;
81
+ }
82
+ return Math.min(Math.max(Math.ceil((bytesAfterChunk / totalBytes) * 100), 1), 99);
83
+ };
84
+
85
+ const formatProtocolV2TransferSpeed = (bytes: number, elapsedMs: number) => {
86
+ const safeElapsedMs = Math.max(elapsedMs, 1);
87
+ return (bytes / 1024 / (safeElapsedMs / 1000)).toFixed(2);
88
+ };
52
89
 
53
90
  type ProtocolV2FirmwareUpdateStatusTarget = {
54
- target_id: number;
55
- status: number;
91
+ target_id: number | string;
92
+ status?: number | string;
93
+ payload_version?: number;
94
+ path?: string;
56
95
  };
57
96
 
58
97
  type ProtocolV2FirmwareUpdateStartResponse =
59
98
  | TypedResponseMessage<'Success'>
60
- | TypedResponseMessage<'DevFirmwareUpdateStatus'>
99
+ | TypedResponseMessage<'DeviceFirmwareUpdateStatus'>
61
100
  | undefined;
62
101
 
102
+ type ProtocolV2TargetBinary = { fileName: string; binary: ArrayBuffer; targetId: number };
103
+
104
+ type ProtocolV2RemoteComponentBinary = ProtocolV2RemoteComponentTarget & {
105
+ binary: ArrayBuffer;
106
+ };
107
+
108
+ type ProtocolV2RemoteComponentTarget = {
109
+ fileName: string;
110
+ targetId: number;
111
+ kind: 'bootloader' | 'firmware' | 'resource';
112
+ };
113
+
114
+ type ProtocolV2OkppHeader = {
115
+ type: string;
116
+ version: IVersionArray;
117
+ payloadHash: string;
118
+ headerHash: string;
119
+ };
120
+
121
+ const PROTOCOL_V2_REMOTE_COMPONENT_TARGETS: Readonly<
122
+ Record<string, ProtocolV2RemoteComponentTarget>
123
+ > = {
124
+ BOOTLOADER: {
125
+ fileName: 'bootloader.bin',
126
+ targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER,
127
+ kind: 'bootloader',
128
+ },
129
+ APPLICATION_P1: {
130
+ fileName: 'application_p1.bin',
131
+ targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1,
132
+ kind: 'firmware',
133
+ },
134
+ APPLICATION_P2: {
135
+ fileName: 'application_p2.bin',
136
+ targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2,
137
+ kind: 'firmware',
138
+ },
139
+ COPROCESSOR: {
140
+ fileName: 'coprocessor.bin',
141
+ targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_COPROCESSOR,
142
+ kind: 'firmware',
143
+ },
144
+ SE01: {
145
+ fileName: 'se01.bin',
146
+ targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE01,
147
+ kind: 'firmware',
148
+ },
149
+ SE02: {
150
+ fileName: 'se02.bin',
151
+ targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE02,
152
+ kind: 'firmware',
153
+ },
154
+ SE03: {
155
+ fileName: 'se03.bin',
156
+ targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE03,
157
+ kind: 'firmware',
158
+ },
159
+ SE04: {
160
+ fileName: 'se04.bin',
161
+ targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE04,
162
+ kind: 'firmware',
163
+ },
164
+ CRATE: {
165
+ fileName: 'resource.bin',
166
+ targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_CRATE,
167
+ kind: 'resource',
168
+ },
169
+ };
170
+
171
+ const PROTOCOL_V2_ROMLOADER_UNSUPPORTED_MESSAGE =
172
+ 'FW_MGMT_TARGET_ROMLOADER is not accepted by the current Pro2 bootloader update request. Flash romloader with the loader-specific flow instead of firmwareUpdateV4.';
173
+
174
+ // hd-transport 的历史 decode 行为会把单值 enum 输出为枚举名字符串;
175
+ // Protocol V2 沿用这个 SDK 语义,内部比较前再映射回固件协议数值。
176
+ const PROTOCOL_V2_TARGET_ID_BY_DECODED_NAME = new Map<string, number>(
177
+ Object.entries(ProtocolV2FirmwareTargetType).map(([key, value]) => [key, value])
178
+ );
179
+ const PROTOCOL_V2_TARGET_STATUS_BY_DECODED_NAME = new Map<string, number>([
180
+ ['FW_MGMT_UPDATER_TASK_STATUS_PENDING', PROTOCOL_V2_TARGET_STATUS_PENDING],
181
+ ['FW_MGMT_UPDATER_TASK_STATUS_IN_PROGRESS', PROTOCOL_V2_TARGET_STATUS_IN_PROGRESS],
182
+ ['FW_MGMT_UPDATER_TASK_STATUS_FINISHED', PROTOCOL_V2_TARGET_STATUS_FINISHED],
183
+ ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_FILE_NOT_FOUND', 3],
184
+ ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_FILE_READ', 4],
185
+ ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_FILE_WRITE', 5],
186
+ ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_VERIFY', 6],
187
+ ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_INSTALL', 7],
188
+ ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_ABORT', 8],
189
+ ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_BUSY', 9],
190
+ ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_ENTRY_OUT_OF_BOUNDS', 10],
191
+ ]);
192
+
63
193
  const isProtocolV2ReconnectProbeError = (error: unknown) => {
64
194
  const message = getProtocolV2UnknownErrorText(error).toLowerCase();
65
195
  return (
@@ -73,19 +203,165 @@ const isProtocolV2PollingTransientError = (error: unknown) => {
73
203
  return (
74
204
  isProtocolV2DeviceDisconnectedError(error) ||
75
205
  isProtocolV2ReconnectProbeError(error) ||
76
- (message.includes('response timeout') && message.includes('devicegetfirmwareupdatestatus')) ||
206
+ message.includes('libusb_transfer_timed_out') ||
207
+ (message.includes('response timeout') && message.includes('devicefirmwareupdatestatusget')) ||
77
208
  message.includes('device not found') ||
78
209
  message.includes('transportnotfound')
79
210
  );
80
211
  };
81
212
 
213
+ const isProtocolV2StartUpdateTransientError = (error: unknown) => {
214
+ const message = getProtocolV2UnknownErrorText(error).toLowerCase();
215
+ return (
216
+ isProtocolV2DeviceDisconnectedError(error) ||
217
+ isProtocolV2ReconnectProbeError(error) ||
218
+ message.includes('libusb_transfer_timed_out') ||
219
+ (message.includes('response timeout') && message.includes('devicefirmwareupdaterequest'))
220
+ );
221
+ };
222
+
223
+ const isProtocolV2TargetStatusFinished = (status: ProtocolV2FirmwareUpdateStatusTarget['status']) =>
224
+ normalizeProtocolV2TargetStatus(status) === PROTOCOL_V2_TARGET_STATUS_FINISHED;
225
+
226
+ const isProtocolV2TargetStatusInProgress = (
227
+ status: ProtocolV2FirmwareUpdateStatusTarget['status']
228
+ ) =>
229
+ normalizeProtocolV2TargetStatus(status) === PROTOCOL_V2_TARGET_STATUS_PENDING ||
230
+ normalizeProtocolV2TargetStatus(status) === PROTOCOL_V2_TARGET_STATUS_IN_PROGRESS;
231
+
232
+ const isProtocolV2TargetStatusFailed = (status: ProtocolV2FirmwareUpdateStatusTarget['status']) => {
233
+ const normalizedStatus = normalizeProtocolV2TargetStatus(status);
234
+ return (
235
+ typeof normalizedStatus === 'number' && normalizedStatus >= PROTOCOL_V2_TARGET_STATUS_FAILED_MIN
236
+ );
237
+ };
238
+
239
+ const normalizeProtocolV2TargetId = (targetId: number | string) => {
240
+ if (typeof targetId === 'number') {
241
+ return targetId;
242
+ }
243
+ return PROTOCOL_V2_TARGET_ID_BY_DECODED_NAME.get(targetId);
244
+ };
245
+
246
+ const normalizeProtocolV2TargetStatus = (
247
+ status: ProtocolV2FirmwareUpdateStatusTarget['status']
248
+ ) => {
249
+ if (typeof status === 'number') {
250
+ return status;
251
+ }
252
+ if (typeof status === 'string') {
253
+ return PROTOCOL_V2_TARGET_STATUS_BY_DECODED_NAME.get(status);
254
+ }
255
+ return undefined;
256
+ };
257
+
258
+ const normalizeProtocolV2Hex = (value?: string) => value?.replace(/^0x/i, '').toLowerCase();
259
+
260
+ const versionArrayToNumber = (version?: IVersionArray) => {
261
+ if (!version) return undefined;
262
+ return version[0] * 0x10000 + version[1] * 0x100 + version[2];
263
+ };
264
+
265
+ const versionStringToArray = (version?: string | null): IVersionArray | undefined => {
266
+ if (!version) return undefined;
267
+ const parts = version.split('.').map(part => Number(part));
268
+ if (parts.length !== 3 || parts.some(part => !Number.isFinite(part))) return undefined;
269
+ return parts as IVersionArray;
270
+ };
271
+
272
+ const compareProtocolV2Versions = (current?: IVersionArray, target?: IVersionArray) => {
273
+ const currentNumber = versionArrayToNumber(current);
274
+ const targetNumber = versionArrayToNumber(target);
275
+ if (currentNumber === undefined || targetNumber === undefined) return undefined;
276
+ return currentNumber - targetNumber;
277
+ };
278
+
279
+ const bytesToHex = (bytes: Uint8Array) =>
280
+ Array.from(bytes)
281
+ .map(byte => byte.toString(16).padStart(2, '0'))
282
+ .join('');
283
+
284
+ const hexToProtocolV2Bytes = (hex: string) => {
285
+ const normalized = hex.replace(/^0x/i, '');
286
+ if (!normalized || normalized.length % 2 !== 0 || /[^0-9a-f]/i.test(normalized)) {
287
+ return new Uint8Array(0);
288
+ }
289
+ const bytes = new Uint8Array(normalized.length / 2);
290
+ for (let i = 0; i < bytes.length; i++) {
291
+ bytes[i] = Number.parseInt(normalized.slice(i * 2, i * 2 + 2), 16);
292
+ }
293
+ return bytes;
294
+ };
295
+
296
+ const toProtocolV2Bytes = (value: unknown): Uint8Array => {
297
+ if (value instanceof Uint8Array) return value;
298
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
299
+ if (ArrayBuffer.isView(value)) {
300
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
301
+ }
302
+ if (typeof value === 'string') return hexToProtocolV2Bytes(value);
303
+ return new Uint8Array(0);
304
+ };
305
+
306
+ const toProtocolV2FiniteNumber = (value: unknown) => {
307
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
308
+ if (typeof value === 'string') {
309
+ const numeric = Number(value);
310
+ return Number.isFinite(numeric) ? numeric : undefined;
311
+ }
312
+ if (value && typeof value === 'object') {
313
+ const longLike = value as { toNumber?: () => number };
314
+ if (typeof longLike.toNumber === 'function') {
315
+ const numeric = longLike.toNumber();
316
+ return Number.isFinite(numeric) ? numeric : undefined;
317
+ }
318
+ }
319
+ return undefined;
320
+ };
321
+
322
+ const readProtocolV2Ascii = (bytes: Uint8Array, offset: number, length: number) =>
323
+ Array.from(bytes.slice(offset, offset + length))
324
+ .map(byte => String.fromCharCode(byte))
325
+ .join('');
326
+
327
+ const parseProtocolV2OkppHeader = (bytes: Uint8Array): ProtocolV2OkppHeader | null => {
328
+ if (bytes.byteLength < PROTOCOL_V2_OKPP_HEADER_SIZE) return null;
329
+ if (readProtocolV2Ascii(bytes, 0, 4) !== 'OKPP') return null;
330
+
331
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
332
+ const headerLen = view.getUint32(0x0c, true);
333
+ if (headerLen !== PROTOCOL_V2_OKPP_HEADER_SIZE) return null;
334
+
335
+ const packedVersion = view.getUint32(0x10, true);
336
+ return {
337
+ type: readProtocolV2Ascii(bytes, 0x08, 4),
338
+ version: [
339
+ Math.floor(packedVersion / 0x10000) % 0x100,
340
+ Math.floor(packedVersion / 0x100) % 0x100,
341
+ packedVersion % 0x100,
342
+ ],
343
+ payloadHash: bytesToHex(
344
+ bytes.slice(
345
+ PROTOCOL_V2_OKPP_PAYLOAD_HASH_OFFSET,
346
+ PROTOCOL_V2_OKPP_PAYLOAD_HASH_OFFSET + PROTOCOL_V2_OKPP_HASH_SIZE
347
+ )
348
+ ),
349
+ headerHash: bytesToHex(
350
+ bytes.slice(
351
+ PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET,
352
+ PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET + PROTOCOL_V2_OKPP_HASH_SIZE
353
+ )
354
+ ),
355
+ };
356
+ };
357
+
82
358
  /**
83
359
  * FirmwareUpdateV4 is the complete Protocol V2 firmware update flow.
84
360
  *
85
361
  * It intentionally does not fall back to FirmwareUpdateV3/V1 behavior:
86
362
  * - upload uses FilesystemFileWrite
87
- * - install uses DevFirmwareUpdate
88
- * - completion reboots to normal, then polls Ping
363
+ * - install uses DeviceFirmwareUpdateRequest
364
+ * - completion waits for target status to finish, reboots to normal, then polls DeviceInfo
89
365
  */
90
366
  export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareUpdateV4Params> {
91
367
  init() {
@@ -96,15 +372,23 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
96
372
 
97
373
  const { payload } = this;
98
374
 
375
+ if (typeof payload.retryCount !== 'number') {
376
+ payload.retryCount = PROTOCOL_V2_CONNECT_RETRY_COUNT;
377
+ }
378
+ if (typeof payload.pollIntervalTime !== 'number') {
379
+ payload.pollIntervalTime = PROTOCOL_V2_CONNECT_POLL_INTERVAL;
380
+ }
381
+ if (typeof payload.timeout !== 'number') {
382
+ payload.timeout = PROTOCOL_V2_CONNECT_SINGLE_TIMEOUT;
383
+ }
384
+ if (typeof payload.protocolV2DeviceInfoTimeoutMs !== 'number') {
385
+ payload.protocolV2DeviceInfoTimeoutMs = PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT;
386
+ }
387
+
99
388
  validateParams(payload, [
100
- { name: 'bleVersion', type: 'array' },
101
- { name: 'bleBinary', type: 'buffer' },
102
389
  { name: 'chunkSize', type: 'number' },
103
- { name: 'firmwareVersion', type: 'array' },
104
- { name: 'firmwareBinary', type: 'buffer' },
105
- { name: 'resourceBinary', type: 'buffer' },
390
+ { name: 'resourceBinaries', type: 'array', allowEmpty: true },
106
391
  { name: 'forcedUpdateRes', type: 'boolean' },
107
- { name: 'bootloaderVersion', type: 'array' },
108
392
  { name: 'bootloaderBinary', type: 'buffer' },
109
393
  { name: 'romloaderBinary', type: 'buffer' },
110
394
  { name: 'applicationP1Binary', type: 'buffer' },
@@ -119,12 +403,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
119
403
  ]);
120
404
 
121
405
  this.params = {
122
- bleBinary: payload.bleBinary,
123
406
  chunkSize: payload.chunkSize,
124
- firmwareBinary: payload.firmwareBinary,
125
407
  forcedUpdateRes: payload.forcedUpdateRes,
126
- bleVersion: payload.bleVersion,
127
- bootloaderVersion: payload.bootloaderVersion,
128
408
  bootloaderBinary: payload.bootloaderBinary,
129
409
  romloaderBinary: payload.romloaderBinary,
130
410
  applicationP1Binary: payload.applicationP1Binary,
@@ -134,8 +414,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
134
414
  se02Binary: payload.se02Binary,
135
415
  se03Binary: payload.se03Binary,
136
416
  se04Binary: payload.se04Binary,
137
- firmwareVersion: payload.firmwareVersion,
138
- resourceBinary: payload.resourceBinary,
417
+ resourceBinaries: payload.resourceBinaries,
139
418
  firmwareType: payload.firmwareType,
140
419
  platform: payload.platform,
141
420
  };
@@ -174,33 +453,39 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
174
453
  const deviceFirmwareType = getFirmwareType(deviceFeatures);
175
454
  const firmwareType = this.params.firmwareType ?? deviceFirmwareType;
176
455
 
177
- let resourceBinary: ArrayBuffer | null = null;
178
- let fwBinaryMap: { fileName: string; binary: ArrayBuffer; targetId?: number }[] = [];
456
+ let resourceBinaryMap: ProtocolV2TargetBinary[] = [];
457
+ let fwBinaryMap: ProtocolV2TargetBinary[] = [];
179
458
  let bootloaderBinary: ArrayBuffer | null = null;
180
459
  try {
181
460
  this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
182
- resourceBinary = await this.prepareResourceBinary(firmwareType, deviceFeatures);
183
- fwBinaryMap = await this.prepareFirmwareAndBleBinary(firmwareType, deviceFeatures);
184
- bootloaderBinary = await this.prepareBootloaderBinary(firmwareType, deviceFeatures);
185
- // DevFirmwareTargetType 拆分的目标二进制(显式 target_id,不走文件名推断)
186
- fwBinaryMap.push(...this.collectExplicitTargetBinaries());
461
+ resourceBinaryMap = await this.prepareResourceBinaries(firmwareType, deviceFeatures);
462
+ fwBinaryMap = this.collectExplicitTargetBinaries();
463
+ bootloaderBinary = this.prepareBootloaderBinary();
464
+ if (!this.hasExplicitProtocolV2Payload(fwBinaryMap)) {
465
+ const remoteBinaries = await this.prepareRemoteProtocolV2Binaries(
466
+ firmwareType,
467
+ deviceFeatures
468
+ );
469
+ resourceBinaryMap = remoteBinaries.resourceBinaryMap;
470
+ bootloaderBinary = remoteBinaries.bootloaderBinary;
471
+ fwBinaryMap = remoteBinaries.fwBinaryMap;
472
+ }
187
473
  this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
188
474
  } catch (err) {
189
475
  throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateDownloadFailed, err.message ?? err);
190
476
  }
191
477
 
192
- if (!resourceBinary && !bootloaderBinary && fwBinaryMap.length === 0) {
478
+ if (resourceBinaryMap.length === 0 && !bootloaderBinary && fwBinaryMap.length === 0) {
193
479
  throw ERRORS.TypedError(
194
480
  HardwareErrorCode.FirmwareUpdateDownloadFailed,
195
481
  'No firmware to update'
196
482
  );
197
483
  }
198
484
 
199
- // TODO: 当前 firmware-pro2 子模块的 reboot/bootloader 流程还未稳定,先暂停自动进 bootloader。
200
- // await this.enterProtocolV2BootloaderMode();
485
+ await this.enterProtocolV2BootloaderMode();
201
486
 
202
487
  await this.executeProtocolV2Update({
203
- resourceBinary,
488
+ resourceBinaryMap,
204
489
  fwBinaryMap,
205
490
  bootloaderBinary,
206
491
  });
@@ -215,19 +500,32 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
215
500
  }
216
501
 
217
502
  private async getProtocolV2DeviceFeatures(): Promise<Features> {
503
+ if (this.device.features) {
504
+ return this.device.features;
505
+ }
218
506
  if (typeof this.device.getFeatures === 'function') {
219
507
  const features = await this.device.getFeatures();
220
508
  if (features) return features;
221
509
  }
222
- if (this.device.features) {
223
- return this.device.features;
224
- }
225
510
  throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Device features not available');
226
511
  }
227
512
 
228
- private async prepareResourceBinary(firmwareType: EFirmwareType, features: Features) {
229
- if (this.params.resourceBinary) {
230
- return this.params.resourceBinary;
513
+ private async prepareResourceBinaries(firmwareType: EFirmwareType, features: Features) {
514
+ if (this.params.resourceBinaries?.length) {
515
+ this.params.resourceBinaries.forEach((binary, index) => {
516
+ if (!(binary instanceof ArrayBuffer)) {
517
+ throw ERRORS.TypedError(
518
+ HardwareErrorCode.CallMethodInvalidParameter,
519
+ `Parameter [resourceBinaries.${index}] is of type invalid and should be [buffer].`
520
+ );
521
+ }
522
+ });
523
+
524
+ return this.params.resourceBinaries.map((binary, index) => ({
525
+ fileName: `resource-${index + 1}.bin`,
526
+ binary,
527
+ targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_CRATE,
528
+ }));
231
529
  }
232
530
  const resourceUrl = DataManager.getSysResourcesLatestRelease({
233
531
  features,
@@ -237,28 +535,332 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
237
535
 
238
536
  if (resourceUrl) {
239
537
  const resource = (await getSysResourceBinary(resourceUrl)).binary;
240
- return resource;
538
+ return [
539
+ {
540
+ fileName: 'resource.bin',
541
+ binary: resource,
542
+ targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_CRATE,
543
+ },
544
+ ];
241
545
  }
242
546
  Log.warn('No resource url found');
243
- return null;
547
+ return [];
548
+ }
549
+
550
+ private prepareBootloaderBinary(): ArrayBuffer | null {
551
+ return this.params.bootloaderBinary ?? null;
552
+ }
553
+
554
+ private hasExplicitProtocolV2Payload(fwBinaryMap: ProtocolV2TargetBinary[]) {
555
+ return (
556
+ !!this.params.resourceBinaries?.length ||
557
+ !!this.params.bootloaderBinary ||
558
+ fwBinaryMap.length > 0
559
+ );
560
+ }
561
+
562
+ private getRemoteComponentEntries(release: IFirmwareReleaseInfo) {
563
+ const { components } = release;
564
+ if (!components) return [];
565
+
566
+ const orderedKeys = [
567
+ ...(release.installOrder ?? []),
568
+ ...Object.keys(components).filter(key => !release.installOrder?.includes(key)),
569
+ ];
570
+
571
+ return orderedKeys
572
+ .map(key => {
573
+ const component = components[key];
574
+ return component ? ([key, component] as const) : undefined;
575
+ })
576
+ .filter((entry): entry is readonly [string, IProtocolV2FirmwareComponent] => !!entry);
577
+ }
578
+
579
+ private getRemoteComponentTarget(key: string, component: IProtocolV2FirmwareComponent) {
580
+ const targetName = component.target?.toUpperCase();
581
+ if (targetName === 'ROMLOADER') {
582
+ throw ERRORS.TypedError(
583
+ HardwareErrorCode.RuntimeError,
584
+ PROTOCOL_V2_ROMLOADER_UNSUPPORTED_MESSAGE
585
+ );
586
+ }
587
+ const target = PROTOCOL_V2_REMOTE_COMPONENT_TARGETS[targetName];
588
+ if (!target) {
589
+ throw ERRORS.TypedError(
590
+ HardwareErrorCode.RuntimeError,
591
+ `Unsupported Protocol V2 firmware component target: ${key}/${component.target}`
592
+ );
593
+ }
594
+ return target;
595
+ }
596
+
597
+ private getProtocolV2ComponentTargetVersion(
598
+ release: IFirmwareReleaseInfo,
599
+ component: IProtocolV2FirmwareComponent,
600
+ target: ProtocolV2RemoteComponentTarget
601
+ ) {
602
+ if (component.version) return component.version;
603
+ if (target.targetId === ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER) {
604
+ return release.bootloaderVersion;
605
+ }
606
+ if (
607
+ target.targetId === ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1 ||
608
+ target.targetId === ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2
609
+ ) {
610
+ return release.version;
611
+ }
612
+ return undefined;
613
+ }
614
+
615
+ private getProtocolV2ComponentCurrentVersion(
616
+ features: Features,
617
+ target: ProtocolV2RemoteComponentTarget
618
+ ) {
619
+ switch (target.targetId) {
620
+ case ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER:
621
+ return getDeviceBootloaderVersion(features);
622
+ case ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1:
623
+ case ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2:
624
+ return getDeviceFirmwareVersion(features);
625
+ case ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_COPROCESSOR:
626
+ return getDeviceBLEFirmwareVersion(features);
627
+ case ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE01:
628
+ return versionStringToArray(features.se01Version);
629
+ case ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE02:
630
+ return versionStringToArray(features.se02Version);
631
+ case ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE03:
632
+ return versionStringToArray(features.se03Version);
633
+ case ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE04:
634
+ return versionStringToArray(features.se04Version);
635
+ default:
636
+ return undefined;
637
+ }
638
+ }
639
+
640
+ private isProtocolV2ComponentVersionSatisfied(
641
+ release: IFirmwareReleaseInfo,
642
+ component: IProtocolV2FirmwareComponent,
643
+ target: ProtocolV2RemoteComponentTarget,
644
+ features: Features
645
+ ) {
646
+ const targetVersion = this.getProtocolV2ComponentTargetVersion(release, component, target);
647
+ if (!targetVersion) return false;
648
+
649
+ const currentVersion = this.getProtocolV2ComponentCurrentVersion(features, target);
650
+ const compareResult = compareProtocolV2Versions(currentVersion, targetVersion);
651
+ return compareResult !== undefined && compareResult >= 0;
652
+ }
653
+
654
+ private getProtocolV2ResourceFilePath(path: string) {
655
+ if (path.startsWith('vol')) return path;
656
+ if (path.startsWith('/')) return `vol0:${path}`;
657
+ return `vol0:/${path}`;
658
+ }
659
+
660
+ private async readProtocolV2DeviceFileHeader(path: string) {
661
+ const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
662
+ const filePath = this.getProtocolV2ResourceFilePath(path);
663
+ const pathInfoRes = await typedCall('FilesystemPathInfoQuery', 'FilesystemPathInfo', {
664
+ path: filePath,
665
+ });
666
+ const fileSize = toProtocolV2FiniteNumber(pathInfoRes.message?.size);
667
+ if (
668
+ !pathInfoRes.message?.exist ||
669
+ pathInfoRes.message?.directory ||
670
+ fileSize === undefined ||
671
+ fileSize < PROTOCOL_V2_OKPP_HEADER_SIZE
672
+ ) {
673
+ return null;
674
+ }
675
+
676
+ const chunkSize = this.getProtocolV2FirmwareChunkSize();
677
+ const chunks: Uint8Array[] = [];
678
+ let offset = 0;
679
+ while (offset < PROTOCOL_V2_OKPP_HEADER_SIZE) {
680
+ const readLen = Math.min(chunkSize, PROTOCOL_V2_OKPP_HEADER_SIZE - offset);
681
+ const res = await typedCall('FilesystemFileRead', 'FilesystemFile', {
682
+ file: {
683
+ path: filePath,
684
+ offset,
685
+ total_size: 0,
686
+ },
687
+ chunk_len: readLen,
688
+ ui_percentage: undefined,
689
+ });
690
+ const data = toProtocolV2Bytes(res.message?.data);
691
+ if (data.byteLength === 0) return null;
692
+ chunks.push(data);
693
+ offset += data.byteLength;
694
+ }
695
+
696
+ const headerBytes = new Uint8Array(offset);
697
+ let cursor = 0;
698
+ chunks.forEach(chunk => {
699
+ headerBytes.set(chunk, cursor);
700
+ cursor += chunk.byteLength;
701
+ });
702
+ return parseProtocolV2OkppHeader(headerBytes);
703
+ }
704
+
705
+ private async isProtocolV2ResourcePackageMatched(
706
+ pkg: IProtocolV2ResourceManifestPackage,
707
+ manifestVersion?: IVersionArray
708
+ ) {
709
+ try {
710
+ const header = await this.readProtocolV2DeviceFileHeader(pkg.path);
711
+ if (!header) return false;
712
+
713
+ const expectedType = pkg.type ?? 'RESC';
714
+ const expectedVersion = pkg.version ?? manifestVersion;
715
+ if (header.type !== expectedType) return false;
716
+ if (expectedVersion && compareProtocolV2Versions(header.version, expectedVersion) !== 0) {
717
+ return false;
718
+ }
719
+
720
+ const expectedPayloadHash = normalizeProtocolV2Hex(pkg.payloadHash);
721
+ if (expectedPayloadHash && header.payloadHash !== expectedPayloadHash) return false;
722
+
723
+ const expectedHeaderHash = normalizeProtocolV2Hex(pkg.headerHash);
724
+ if (expectedHeaderHash && header.headerHash !== expectedHeaderHash) return false;
725
+
726
+ return true;
727
+ } catch (error) {
728
+ Log.log(`Protocol V2 resource package check failed for ${pkg.path}: `, error);
729
+ return false;
730
+ }
731
+ }
732
+
733
+ private async isProtocolV2ResourceManifestSatisfied(
734
+ manifest: IFirmwareReleaseInfo['resourceManifest']
735
+ ) {
736
+ if (!manifest?.packages?.length) return false;
737
+
738
+ for (const pkg of manifest.packages) {
739
+ if (!(await this.isProtocolV2ResourcePackageMatched(pkg, manifest.version))) {
740
+ return false;
741
+ }
742
+ }
743
+ return true;
744
+ }
745
+
746
+ private getProtocolV2ResourceManifest(
747
+ release: IFirmwareReleaseInfo,
748
+ component: IProtocolV2FirmwareComponent
749
+ ) {
750
+ return component.resourceManifest ?? release.resourceManifest;
244
751
  }
245
752
 
246
- private async prepareBootloaderBinary(
247
- firmwareType: EFirmwareType,
753
+ private getProtocolV2ResourceComponentFileName(key: string) {
754
+ const safeKey = key.replace(/[^a-z0-9_-]/gi, '_') || 'resource';
755
+ return `resource-${safeKey}.bin`;
756
+ }
757
+
758
+ private async shouldInstallRemoteProtocolV2Component(
759
+ release: IFirmwareReleaseInfo,
760
+ key: string,
761
+ component: IProtocolV2FirmwareComponent,
762
+ target: ProtocolV2RemoteComponentTarget,
248
763
  features: Features
249
- ): Promise<ArrayBuffer | null> {
250
- if (this.params.bootloaderBinary) {
251
- return this.params.bootloaderBinary;
764
+ ) {
765
+ if (target.kind === 'resource') {
766
+ if (
767
+ this.params.forcedUpdateRes ||
768
+ features.bootloaderMode ||
769
+ features.mode === 'bootloader'
770
+ ) {
771
+ return true;
772
+ }
773
+ const resourceMatched = await this.isProtocolV2ResourceManifestSatisfied(
774
+ this.getProtocolV2ResourceManifest(release, component)
775
+ );
776
+ if (resourceMatched) {
777
+ Log.log(`[FirmwareUpdateV4] skip Protocol V2 resource component ${key}; manifest matched`);
778
+ }
779
+ return !resourceMatched;
252
780
  }
253
781
 
254
- if (this.params.bootloaderVersion) {
255
- const bootResourceUrl = DataManager.getBootloaderResource(features, firmwareType);
256
- if (bootResourceUrl) {
257
- const bootBinary = (await getSysResourceBinary(bootResourceUrl)).binary;
258
- return bootBinary;
782
+ const versionSatisfied = this.isProtocolV2ComponentVersionSatisfied(
783
+ release,
784
+ component,
785
+ target,
786
+ features
787
+ );
788
+ if (versionSatisfied) {
789
+ Log.log(`[FirmwareUpdateV4] skip Protocol V2 component ${key}; version is up to date`);
790
+ }
791
+ return !versionSatisfied;
792
+ }
793
+
794
+ private async downloadRemoteProtocolV2Component(
795
+ key: string,
796
+ component: IProtocolV2FirmwareComponent
797
+ ): Promise<ProtocolV2RemoteComponentBinary> {
798
+ const target = this.getRemoteComponentTarget(key, component);
799
+ if (!component.url) {
800
+ throw ERRORS.TypedError(
801
+ HardwareErrorCode.RuntimeError,
802
+ `Missing Protocol V2 firmware component url: ${key}/${component.target}`
803
+ );
804
+ }
805
+
806
+ const { binary } = await getSysResourceBinary(component.url);
807
+ return {
808
+ ...target,
809
+ binary,
810
+ };
811
+ }
812
+
813
+ private async prepareRemoteProtocolV2Binaries(firmwareType: EFirmwareType, features: Features) {
814
+ const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
815
+
816
+ const resourceBinaryMap: ProtocolV2TargetBinary[] = [];
817
+ let bootloaderBinary: ArrayBuffer | null = null;
818
+ const fwBinaryMap: ProtocolV2TargetBinary[] = [];
819
+
820
+ if (!release) {
821
+ return {
822
+ resourceBinaryMap,
823
+ bootloaderBinary,
824
+ fwBinaryMap,
825
+ };
826
+ }
827
+
828
+ const entries = this.getRemoteComponentEntries(release);
829
+
830
+ for (const [key, component] of entries) {
831
+ const target = this.getRemoteComponentTarget(key, component);
832
+ const shouldInstall = await this.shouldInstallRemoteProtocolV2Component(
833
+ release,
834
+ key,
835
+ component,
836
+ target,
837
+ features
838
+ );
839
+ if (shouldInstall) {
840
+ const remoteBinary = await this.downloadRemoteProtocolV2Component(key, component);
841
+ if (remoteBinary.kind === 'resource') {
842
+ resourceBinaryMap.push({
843
+ fileName: this.getProtocolV2ResourceComponentFileName(key),
844
+ binary: remoteBinary.binary,
845
+ targetId: remoteBinary.targetId,
846
+ });
847
+ } else if (remoteBinary.kind === 'bootloader') {
848
+ bootloaderBinary = remoteBinary.binary;
849
+ } else {
850
+ fwBinaryMap.push({
851
+ fileName: remoteBinary.fileName,
852
+ binary: remoteBinary.binary,
853
+ targetId: remoteBinary.targetId,
854
+ });
855
+ }
259
856
  }
260
857
  }
261
- return null;
858
+
859
+ return {
860
+ resourceBinaryMap,
861
+ bootloaderBinary,
862
+ fwBinaryMap,
863
+ };
262
864
  }
263
865
 
264
866
  private isProtocolV2BootloaderMode() {
@@ -270,18 +872,16 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
270
872
 
271
873
  async enterProtocolV2BootloaderMode() {
272
874
  if (this.isProtocolV2BootloaderMode()) {
875
+ Log.debug('Protocol V2 device is already in bootloader mode, skip reboot to bootloader');
273
876
  return false;
274
877
  }
275
878
 
276
879
  try {
277
880
  this.postTipMessage(FirmwareUpdateTipMessage.AutoRebootToBootloader);
278
- await this.protocolV2Reboot(DevRebootType.Bootloader);
881
+ await this.protocolV2Reboot(DeviceRebootType.Bootloader);
279
882
  this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
280
- this.checkDeviceToBootloader(this.payload.connectId);
281
- await this.checkPromise?.promise;
282
- this.checkPromise = null;
283
- await wait(1500);
284
- await this.device.acquire?.();
883
+ await wait(1000);
884
+ await this.waitForProtocolV2BootloaderMode();
285
885
  return true;
286
886
  } catch (error) {
287
887
  if (error instanceof HardwareError) {
@@ -292,160 +892,212 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
292
892
  }
293
893
  }
294
894
 
895
+ private async waitForProtocolV2BootloaderMode(
896
+ timeout = PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT,
897
+ retryInterval = 1000
898
+ ) {
899
+ const startTime = Date.now();
900
+ let lastError: unknown;
901
+
902
+ while (Date.now() - startTime < timeout) {
903
+ try {
904
+ await this.reconnectProtocolV2Device();
905
+ const deviceInfo = await requestProtocolV2DeviceInfo({
906
+ commands: this.device.getCommands(),
907
+ timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
908
+ });
909
+ const features = this.device.updateProtocolV2Features(deviceInfo);
910
+ if (features?.bootloaderMode) {
911
+ return features;
912
+ }
913
+ lastError = new Error('Protocol V2 device is reachable but is not in bootloader mode');
914
+ } catch (error) {
915
+ lastError = error;
916
+ Log.log('Protocol V2 bootloader mode not ready, polling reconnect: ', error);
917
+ }
918
+ await wait(retryInterval);
919
+ }
920
+
921
+ throw ERRORS.TypedError(
922
+ HardwareErrorCode.FirmwareUpdateAutoEnterBootFailure,
923
+ `Protocol V2 bootloader not ready within ${timeout / 1000}s: ${this.normalizeErrorMessage(
924
+ lastError
925
+ )}`
926
+ );
927
+ }
928
+
295
929
  /**
296
- * 收集按 DevFirmwareTargetType 拆分的显式目标二进制。
930
+ * 收集按 DeviceFirmwareTargetType 拆分的显式目标二进制。
297
931
  * 文件名仅用于 staging 路径展示,target_id 已显式给定。
298
932
  */
299
933
  private collectExplicitTargetBinaries() {
300
- const entries: { fileName: string; binary: ArrayBuffer; targetId: number }[] = [];
934
+ const entries: ProtocolV2TargetBinary[] = [];
301
935
  const push = (binary: ArrayBuffer | undefined, fileName: string, targetId: number) => {
302
936
  if (binary) entries.push({ fileName, binary, targetId });
303
937
  };
304
938
 
305
- push(
306
- this.params.romloaderBinary,
307
- 'romloader.bin',
308
- ProtocolV2FirmwareTargetType.TARGET_MAIN_BOOT
309
- );
939
+ if (this.params.romloaderBinary) {
940
+ throw ERRORS.TypedError(
941
+ HardwareErrorCode.RuntimeError,
942
+ PROTOCOL_V2_ROMLOADER_UNSUPPORTED_MESSAGE
943
+ );
944
+ }
310
945
  push(
311
946
  this.params.applicationP1Binary,
312
947
  'application_p1.bin',
313
- ProtocolV2FirmwareTargetType.TARGET_MAIN_APP
948
+ ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1
314
949
  );
315
950
  push(
316
951
  this.params.applicationP2Binary,
317
952
  'application_p2.bin',
318
- ProtocolV2FirmwareTargetType.TARGET_MAIN_APP
953
+ ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2
954
+ );
955
+ push(
956
+ this.params.coprocessorBinary,
957
+ 'coprocessor.bin',
958
+ ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_COPROCESSOR
319
959
  );
320
- push(this.params.coprocessorBinary, 'coprocessor.bin', ProtocolV2FirmwareTargetType.TARGET_BT);
321
- push(this.params.se01Binary, 'se01.bin', ProtocolV2FirmwareTargetType.TARGET_SE1);
322
- push(this.params.se02Binary, 'se02.bin', ProtocolV2FirmwareTargetType.TARGET_SE2);
323
- push(this.params.se03Binary, 'se03.bin', ProtocolV2FirmwareTargetType.TARGET_SE3);
324
- push(this.params.se04Binary, 'se04.bin', ProtocolV2FirmwareTargetType.TARGET_SE4);
960
+ push(this.params.se01Binary, 'se01.bin', ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE01);
961
+ push(this.params.se02Binary, 'se02.bin', ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE02);
962
+ push(this.params.se03Binary, 'se03.bin', ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE03);
963
+ push(this.params.se04Binary, 'se04.bin', ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE04);
325
964
  return entries;
326
965
  }
327
966
 
328
- private async prepareFirmwareAndBleBinary(firmwareType: EFirmwareType, features: Features) {
329
- const fwBinaryMap: { fileName: string; binary: ArrayBuffer; targetId?: number }[] = [];
330
- if (this.params.firmwareBinary) {
331
- fwBinaryMap.push({
332
- fileName: 'firmware.bin',
333
- binary: this.params.firmwareBinary,
334
- });
335
- } else if (this.params.firmwareVersion) {
336
- const firmwareBinary = (
337
- await getBinary({
338
- features,
339
- version: this.params.firmwareVersion,
340
- updateType: 'firmware',
341
- isUpdateBootloader: false,
342
- firmwareType,
343
- })
344
- ).binary;
345
- fwBinaryMap.push({
346
- fileName: 'firmware.bin',
347
- binary: firmwareBinary,
348
- });
349
- }
350
-
351
- if (this.params.bleBinary) {
352
- fwBinaryMap.push({
353
- fileName: 'ble-firmware.bin',
354
- binary: this.params.bleBinary,
355
- });
356
- } else if (this.params.bleVersion) {
357
- const bleBinary = await getBinary({
358
- features,
359
- version: this.params.bleVersion,
360
- updateType: 'ble',
361
- firmwareType,
362
- });
363
- fwBinaryMap.push({
364
- fileName: 'ble-firmware.bin',
365
- binary: bleBinary.binary,
366
- });
367
- }
368
-
369
- return fwBinaryMap;
370
- }
371
-
372
967
  private async executeProtocolV2Update({
373
- resourceBinary,
968
+ resourceBinaryMap,
374
969
  fwBinaryMap,
375
970
  bootloaderBinary,
376
971
  }: {
377
- resourceBinary: ArrayBuffer | null;
378
- fwBinaryMap: { fileName: string; binary: ArrayBuffer; targetId?: number }[];
972
+ resourceBinaryMap: ProtocolV2TargetBinary[];
973
+ fwBinaryMap: ProtocolV2TargetBinary[];
379
974
  bootloaderBinary: ArrayBuffer | null;
380
975
  }) {
381
976
  let totalSize = 0;
382
977
  let processedSize = 0;
978
+ let transferredSize = 0;
383
979
 
384
- if (resourceBinary) totalSize += resourceBinary.byteLength;
980
+ for (const resource of resourceBinaryMap) totalSize += resource.binary.byteLength;
385
981
  for (const fwbinary of fwBinaryMap) totalSize += fwbinary.binary.byteLength;
386
982
  if (bootloaderBinary) totalSize += bootloaderBinary.byteLength;
387
983
 
388
984
  this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
985
+ const transferStartTime = Date.now();
986
+ const transferTransport = this.getProtocolV2FirmwareTransferTransport();
987
+ const chunkSize = this.getProtocolV2FirmwareChunkSize();
988
+ const onTransferredBytes = (bytes: number) => {
989
+ transferredSize = bytes;
990
+ };
991
+ Log.log(
992
+ `[FirmwareUpdateV4] transfer started transport=${transferTransport} total=${totalSize} bytes chunk=${chunkSize} bytes`
993
+ );
389
994
 
995
+ const resourceTargets: Array<{ target_id: number; path: string }> = [];
390
996
  const targets: Array<{ target_id: number; path: string }> = [];
391
997
 
392
- if (resourceBinary) {
393
- // resource 仅支持单文件 .bin:整文件一次上传,target path 指向该文件
394
- const resourceFilePath = `${PROTOCOL_V2_FIRMWARE_STAGING_VOLUME}resource.bin`;
395
- processedSize = await this.protocolV2CommonUpdateProcess({
396
- payload: resourceBinary,
397
- filePath: resourceFilePath,
398
- processedSize,
399
- totalSize,
400
- });
401
- targets.push({
402
- target_id: ProtocolV2FirmwareTargetType.TARGET_RESOURCE,
403
- path: resourceFilePath,
404
- });
405
- }
998
+ try {
999
+ for (const resource of resourceBinaryMap) {
1000
+ const resourceFilePath = `${PROTOCOL_V2_FIRMWARE_STAGING_VOLUME}${resource.fileName}`;
1001
+ Log.log(
1002
+ `[FirmwareUpdateV4] staging resource via FilesystemFileWrite target=${resource.targetId} path=${resourceFilePath} bytes=${resource.binary.byteLength}`
1003
+ );
1004
+ processedSize = await this.protocolV2CommonUpdateProcess({
1005
+ payload: resource.binary,
1006
+ filePath: resourceFilePath,
1007
+ processedSize,
1008
+ totalSize,
1009
+ onTransferredBytes,
1010
+ });
1011
+ transferredSize = processedSize;
1012
+ resourceTargets.push({
1013
+ target_id: resource.targetId,
1014
+ path: resourceFilePath,
1015
+ });
1016
+ }
406
1017
 
407
- if (bootloaderBinary) {
408
- const bootloaderPath = `${PROTOCOL_V2_FIRMWARE_STAGING_VOLUME}bootloader.bin`;
409
- processedSize = await this.protocolV2CommonUpdateProcess({
410
- payload: bootloaderBinary,
411
- filePath: bootloaderPath,
412
- processedSize,
413
- totalSize,
414
- });
415
- targets.push({
416
- target_id: ProtocolV2FirmwareTargetType.TARGET_MAIN_BOOT,
417
- path: bootloaderPath,
418
- });
419
- }
1018
+ if (bootloaderBinary) {
1019
+ const bootloaderPath = `${PROTOCOL_V2_FIRMWARE_STAGING_VOLUME}bootloader.bin`;
1020
+ Log.log(
1021
+ `[FirmwareUpdateV4] staging bootloader via FilesystemFileWrite target=${ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER} path=${bootloaderPath} bytes=${bootloaderBinary.byteLength}`
1022
+ );
1023
+ processedSize = await this.protocolV2CommonUpdateProcess({
1024
+ payload: bootloaderBinary,
1025
+ filePath: bootloaderPath,
1026
+ processedSize,
1027
+ totalSize,
1028
+ onTransferredBytes,
1029
+ });
1030
+ transferredSize = processedSize;
1031
+ targets.push({
1032
+ target_id: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER,
1033
+ path: bootloaderPath,
1034
+ });
1035
+ }
420
1036
 
421
- for (const fwbinary of fwBinaryMap) {
422
- const firmwarePath = `${PROTOCOL_V2_FIRMWARE_STAGING_VOLUME}${fwbinary.fileName}`;
423
- processedSize = await this.protocolV2CommonUpdateProcess({
424
- payload: fwbinary.binary,
425
- filePath: firmwarePath,
426
- processedSize,
427
- totalSize,
428
- });
429
- targets.push({
430
- target_id: fwbinary.targetId ?? protocolV2FileNameToTargetId(fwbinary.fileName),
431
- path: firmwarePath,
432
- });
433
- }
1037
+ for (const fwbinary of fwBinaryMap) {
1038
+ const firmwarePath = `${PROTOCOL_V2_FIRMWARE_STAGING_VOLUME}${fwbinary.fileName}`;
1039
+ Log.log(
1040
+ `[FirmwareUpdateV4] staging firmware via FilesystemFileWrite target=${fwbinary.targetId} path=${firmwarePath} bytes=${fwbinary.binary.byteLength}`
1041
+ );
1042
+ processedSize = await this.protocolV2CommonUpdateProcess({
1043
+ payload: fwbinary.binary,
1044
+ filePath: firmwarePath,
1045
+ processedSize,
1046
+ totalSize,
1047
+ onTransferredBytes,
1048
+ });
1049
+ transferredSize = processedSize;
1050
+ targets.push({
1051
+ target_id: fwbinary.targetId,
1052
+ path: firmwarePath,
1053
+ });
1054
+ }
434
1055
 
435
- if (totalSize > 0) {
436
- this.postProgressMessage(100, 'transferData');
1056
+ if (totalSize > 0) {
1057
+ this.postProgressMessage(100, 'transferData');
1058
+ }
1059
+
1060
+ const elapsedMs = Date.now() - transferStartTime;
1061
+ Log.log(
1062
+ `[FirmwareUpdateV4] transfer finished transport=${transferTransport} bytes=${totalSize} elapsed=${(
1063
+ elapsedMs / 1000
1064
+ ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(totalSize, elapsedMs)} KB/s`
1065
+ );
1066
+ } catch (error) {
1067
+ const elapsedMs = Date.now() - transferStartTime;
1068
+ Log.warn(
1069
+ `[FirmwareUpdateV4] transfer failed transport=${transferTransport} bytes=${transferredSize}/${totalSize} elapsed=${(
1070
+ elapsedMs / 1000
1071
+ ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(transferredSize, elapsedMs)} KB/s`
1072
+ );
1073
+ throw error;
437
1074
  }
438
1075
 
439
1076
  this.postTipMessage(FirmwareUpdateTipMessage.ConfirmOnDevice);
440
- const startResponse = await this.protocolV2StartFirmwareUpdate({ targets });
441
- await this.waitForProtocolV2FirmwareUpdateComplete(targets, startResponse);
1077
+ for (const resourceTarget of resourceTargets) {
1078
+ const resourceTargetsBatch = [resourceTarget];
1079
+ Log.log(
1080
+ `[FirmwareUpdateV4] DeviceFirmwareUpdateRequest resources=${JSON.stringify(
1081
+ resourceTargetsBatch
1082
+ )}`
1083
+ );
1084
+ const startResponse = await this.protocolV2StartFirmwareUpdate({
1085
+ targets: resourceTargetsBatch,
1086
+ });
1087
+ await this.waitForProtocolV2FirmwareUpdateComplete(resourceTargetsBatch, startResponse);
1088
+ }
1089
+ if (targets.length > 0) {
1090
+ Log.log(`[FirmwareUpdateV4] DeviceFirmwareUpdateRequest targets=${JSON.stringify(targets)}`);
1091
+ const startResponse = await this.protocolV2StartFirmwareUpdate({ targets });
1092
+ await this.waitForProtocolV2FirmwareUpdateComplete(targets, startResponse);
1093
+ }
442
1094
  }
443
1095
 
444
1096
  private async queryProtocolV2FirmwareUpdateStatus() {
445
1097
  const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
446
1098
  return typedCall(
447
- 'DevGetFirmwareUpdateStatus',
448
- 'DevFirmwareUpdateStatus',
1099
+ 'DeviceFirmwareUpdateStatusGet',
1100
+ 'DeviceFirmwareUpdateStatus',
449
1101
  {},
450
1102
  {
451
1103
  timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
@@ -465,14 +1117,32 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
465
1117
  );
466
1118
  }
467
1119
 
1120
+ private isProtocolV2NormalModeFeatures(features?: Features | null) {
1121
+ return !!features && !features.bootloaderMode && features.mode !== 'bootloader';
1122
+ }
1123
+
1124
+ private async probeProtocolV2NormalMode() {
1125
+ const deviceInfo = await requestProtocolV2DeviceInfo({
1126
+ commands: this.device.getCommands(),
1127
+ timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
1128
+ request: PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST,
1129
+ });
1130
+ const features = this.device.updateProtocolV2Features(deviceInfo);
1131
+ if (this.isProtocolV2NormalModeFeatures(features)) {
1132
+ Log.log('Protocol V2 firmware install finished; device is back in normal mode');
1133
+ return true;
1134
+ }
1135
+ return false;
1136
+ }
1137
+
468
1138
  private assertProtocolV2TargetStatus(
469
1139
  statusTargets: ProtocolV2FirmwareUpdateStatusTarget[],
470
1140
  expectedTargetIds: Set<number>
471
1141
  ) {
472
1142
  const failedTarget = statusTargets.find(
473
1143
  target =>
474
- expectedTargetIds.has(target.target_id) &&
475
- target.status === PROTOCOL_V2_TARGET_STATUS_FAILED
1144
+ expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1) &&
1145
+ isProtocolV2TargetStatusFailed(target.status)
476
1146
  );
477
1147
  if (failedTarget) {
478
1148
  throw ERRORS.TypedError(
@@ -483,8 +1153,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
483
1153
 
484
1154
  const completedTargets = statusTargets.filter(
485
1155
  target =>
486
- expectedTargetIds.has(target.target_id) &&
487
- target.status === PROTOCOL_V2_TARGET_STATUS_FINISHED
1156
+ expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1) &&
1157
+ isProtocolV2TargetStatusFinished(target.status)
488
1158
  );
489
1159
  if (completedTargets.length === expectedTargetIds.size && expectedTargetIds.size > 0) {
490
1160
  return true;
@@ -492,8 +1162,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
492
1162
 
493
1163
  const inProgressTarget = statusTargets.find(
494
1164
  target =>
495
- expectedTargetIds.has(target.target_id) &&
496
- target.status === PROTOCOL_V2_TARGET_STATUS_IN_PROGRESS
1165
+ expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1) &&
1166
+ isProtocolV2TargetStatusInProgress(target.status)
497
1167
  );
498
1168
  if (inProgressTarget) {
499
1169
  this.postProgressMessage(99, 'installingFirmware');
@@ -507,11 +1177,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
507
1177
  startResponse?: ProtocolV2FirmwareUpdateStartResponse
508
1178
  ) {
509
1179
  const expectedTargetIds = new Set(targets.map(target => target.target_id));
510
- if (startResponse?.type === 'Success') {
511
- return;
512
- }
513
- if (startResponse?.type === 'DevFirmwareUpdateStatus') {
514
- const statusTargets = (startResponse.message.targets ??
1180
+ if (startResponse?.type === 'DeviceFirmwareUpdateStatus') {
1181
+ const statusTargets = (startResponse.message.records ??
515
1182
  []) as ProtocolV2FirmwareUpdateStatusTarget[];
516
1183
  if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds)) {
517
1184
  return;
@@ -524,7 +1191,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
524
1191
  while (Date.now() - startTime < PROTOCOL_V2_INSTALL_TIMEOUT) {
525
1192
  try {
526
1193
  const statusRes = await this.queryProtocolV2FirmwareUpdateStatus();
527
- const statusTargets = (statusRes.message.targets ??
1194
+ const statusTargets = (statusRes.message.records ??
528
1195
  []) as ProtocolV2FirmwareUpdateStatusTarget[];
529
1196
  if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds)) {
530
1197
  return;
@@ -538,17 +1205,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
538
1205
  if (isProtocolV2PollingTransientError(error)) {
539
1206
  try {
540
1207
  await this.reconnectProtocolV2Device();
1208
+ if (await this.probeProtocolV2NormalMode()) {
1209
+ return;
1210
+ }
541
1211
  } catch (reconnectError) {
542
1212
  lastError = reconnectError;
543
1213
  Log.log(
544
- 'Protocol V2 firmware install reconnect/status polling failed: ',
1214
+ 'Protocol V2 firmware install reconnect/normal-mode probe failed: ',
545
1215
  reconnectError
546
1216
  );
547
1217
  }
548
1218
  try {
549
1219
  await this.pingProtocolV2Device();
550
1220
  Log.log('Protocol V2 firmware status unavailable, Ping is ready');
551
- return;
552
1221
  } catch (pingError) {
553
1222
  lastError = pingError;
554
1223
  Log.log('Protocol V2 firmware install Ping polling failed: ', pingError);
@@ -566,7 +1235,16 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
566
1235
 
567
1236
  private async exitProtocolV2BootloaderToNormal() {
568
1237
  this.postTipMessage(FirmwareUpdateTipMessage.SwitchFirmwareReconnectDevice);
569
- await this.protocolV2Reboot(DevRebootType.Normal);
1238
+ try {
1239
+ await this.reconnectProtocolV2Device();
1240
+ if (await this.probeProtocolV2NormalMode()) {
1241
+ Log.log('Protocol V2 device is already in normal mode, skip normal reboot');
1242
+ return;
1243
+ }
1244
+ } catch (error) {
1245
+ Log.log('Protocol V2 normal-mode probe before reboot failed: ', error);
1246
+ }
1247
+ await this.protocolV2Reboot(DeviceRebootType.Normal);
570
1248
  }
571
1249
 
572
1250
  private async waitForProtocolV2FinalFeatures() {
@@ -603,7 +1281,14 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
603
1281
  // 更新完成判定只需要各 target 版本号;scope 与请求内容保持一致
604
1282
  request: PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST,
605
1283
  });
606
- return this.device.updateProtocolV2Features(deviceInfo);
1284
+ const features = this.device.updateProtocolV2Features(deviceInfo);
1285
+ if (features.bootloaderMode || features.mode === 'bootloader') {
1286
+ throw ERRORS.TypedError(
1287
+ HardwareErrorCode.DeviceNotFound,
1288
+ 'Protocol V2 device is still in bootloader mode'
1289
+ );
1290
+ }
1291
+ return features;
607
1292
  } catch (error) {
608
1293
  lastError = error;
609
1294
  Log.log('Protocol V2 normal mode not ready, polling Ping: ', error);
@@ -628,15 +1313,42 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
628
1313
  const deviceDiff = await this.device.deviceConnector?.enumerate();
629
1314
  const devicesDescriptor = deviceDiff?.descriptors ?? [];
630
1315
 
631
- const { deviceList } = await DevicePool.getDevices(devicesDescriptor, this.connectId);
1316
+ if (
1317
+ DataManager.isBrowserWebUsb(DataManager.getSettings('env')) &&
1318
+ devicesDescriptor.length === 1
1319
+ ) {
1320
+ this.device.updateDescriptor(
1321
+ {
1322
+ ...devicesDescriptor[0],
1323
+ protocolType: PROTOCOL_V2_CONNECT_PROTOCOL,
1324
+ },
1325
+ true
1326
+ );
1327
+ await this.device.acquire(PROTOCOL_V2_CONNECT_PROTOCOL, { throwOnRunPromiseError: true });
1328
+ this.device.commands.disposed = false;
1329
+ this.device.getCommands().mainId = this.device.mainId ?? '';
1330
+ await this.device.initialize();
1331
+ return;
1332
+ }
1333
+
1334
+ // App 与 bootloader 序列号暂时可能不一致。V4 升级重连阶段只接受唯一枚举设备,
1335
+ // 避免继续按旧 app connectId 查缓存导致反复输出 path mismatch 日志。
1336
+ const { deviceList } = await DevicePool.getDevices(devicesDescriptor, undefined, {
1337
+ connectProtocol: PROTOCOL_V2_CONNECT_PROTOCOL,
1338
+ });
632
1339
  if (deviceList.length !== 1) {
633
1340
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
634
1341
  }
635
1342
 
1343
+ Log.debug(
1344
+ 'Protocol V2 firmware reconnect using single enumerated device:',
1345
+ deviceList[0].getConnectId()
1346
+ );
636
1347
  this.device.updateFromCache(deviceList[0]);
637
- await this.device.acquire();
1348
+ await this.device.acquire(PROTOCOL_V2_CONNECT_PROTOCOL, { throwOnRunPromiseError: true });
638
1349
  this.device.commands.disposed = false;
639
1350
  this.device.getCommands().mainId = this.device.mainId ?? '';
1351
+ await this.device.initialize();
640
1352
  }
641
1353
 
642
1354
  private async protocolV2CommonUpdateProcess({
@@ -644,10 +1356,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
644
1356
  filePath,
645
1357
  processedSize,
646
1358
  totalSize,
1359
+ onTransferredBytes,
647
1360
  }: PROTO.FirmwareUpload & {
648
1361
  filePath: string;
649
1362
  processedSize?: number;
650
1363
  totalSize?: number;
1364
+ onTransferredBytes?: (transferredBytes: number) => void;
651
1365
  }) {
652
1366
  const chunkSize = this.getProtocolV2FirmwareChunkSize();
653
1367
  let offset = 0;
@@ -663,7 +1377,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
663
1377
  const chunkLength = chunkEnd - offset;
664
1378
  const chunk = payload.slice(offset, chunkEnd);
665
1379
  const overwrite = offset === 0;
666
- const progress = getUploadProgress(chunkEnd);
1380
+ const progress = getProtocolV2DeviceTransferProgress(
1381
+ (processedSize ?? 0) + offset,
1382
+ (processedSize ?? 0) + chunkEnd,
1383
+ totalSize ?? payload.byteLength
1384
+ );
667
1385
 
668
1386
  const writeRes = await this.fileWriteWithRetry(
669
1387
  filePath,
@@ -685,12 +1403,27 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
685
1403
  );
686
1404
  }
687
1405
  offset = nextOffset;
1406
+ onTransferredBytes?.((processedSize ?? 0) + offset);
688
1407
  this.postProgressMessage(getUploadProgress(offset), 'transferData');
689
1408
  }
690
1409
 
691
1410
  return totalSize !== undefined ? (processedSize ?? 0) + payload.byteLength : 0;
692
1411
  }
693
1412
 
1413
+ private getProtocolV2FirmwareTransferTransport() {
1414
+ const env = DataManager.getSettings('env');
1415
+ if (env && DataManager.isBleConnect(env)) {
1416
+ return 'BLE';
1417
+ }
1418
+ if (
1419
+ env &&
1420
+ (DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env) || env === 'web')
1421
+ ) {
1422
+ return 'WebUSB';
1423
+ }
1424
+ return env ?? 'unknown';
1425
+ }
1426
+
694
1427
  private async fileWriteWithRetry(
695
1428
  filePath: string,
696
1429
  totalFileSize: number,
@@ -770,24 +1503,27 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
770
1503
  let response: ProtocolV2FirmwareUpdateStartResponse;
771
1504
  try {
772
1505
  response = await commands.typedCall(
773
- 'DevFirmwareUpdate',
1506
+ 'DeviceFirmwareUpdateRequest',
774
1507
  PROTOCOL_V2_FIRMWARE_UPDATE_RESPONSE_TYPES,
775
1508
  {
776
1509
  targets,
777
1510
  },
778
1511
  {
779
- intermediateTypes: ['DevFirmwareInstallProgress'],
780
- onIntermediateResponse: (response: { message?: { progress?: number } }) => {
781
- const progress = Number(response.message?.progress);
782
- if (Number.isFinite(progress)) {
783
- this.postProgressMessage(Math.min(progress, 99), 'installingFirmware');
1512
+ intermediateTypes: ['DeviceFirmwareUpdateStatus'],
1513
+ timeoutMs: PROTOCOL_V2_START_UPDATE_TIMEOUT,
1514
+ onIntermediateResponse: (response: { type?: string }) => {
1515
+ if (response.type === 'DeviceFirmwareUpdateStatus') {
1516
+ this.postProgressMessage(99, 'installingFirmware');
784
1517
  }
785
1518
  },
786
1519
  }
787
1520
  );
788
1521
  } catch (error) {
789
- if (isProtocolV2DeviceDisconnectedError(error)) {
790
- Log.log('Rebooting device');
1522
+ if (isProtocolV2StartUpdateTransientError(error)) {
1523
+ Log.log(
1524
+ 'Protocol V2 firmware update request did not return; continue status polling',
1525
+ error
1526
+ );
791
1527
  } else {
792
1528
  throw error;
793
1529
  }
@@ -796,10 +1532,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
796
1532
  return response;
797
1533
  }
798
1534
 
799
- private async protocolV2Reboot(rebootType: DevRebootType) {
1535
+ private async protocolV2Reboot(rebootType: DeviceRebootType) {
800
1536
  const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
801
1537
  try {
802
- const res = await typedCall('DevReboot', 'Success', {
1538
+ const res = await typedCall('DeviceReboot', 'Success', {
803
1539
  reboot_type: rebootType,
804
1540
  });
805
1541
  return res.message;