@onekeyfe/hd-core 1.2.0-alpha.1 → 1.2.0-alpha.10

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