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

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