@onekeyfe/hd-core 1.2.0-alpha.99 → 1.2.1-alpha.0

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 (200) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +20 -4
  2. package/__tests__/DeviceCommands.test.ts +203 -27
  3. package/__tests__/base64Data.test.ts +69 -0
  4. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +48 -2
  5. package/__tests__/check-firmware-release-protocol-v2.test.ts +82 -13
  6. package/__tests__/device-connector-protocol.test.ts +21 -0
  7. package/__tests__/device-lifecycle-events.test.ts +427 -7
  8. package/__tests__/device-pool-state.test.ts +25 -0
  9. package/__tests__/device-settings.test.ts +237 -9
  10. package/__tests__/device-state-events.test.ts +2 -2
  11. package/__tests__/device-state-mapper.test.ts +61 -5
  12. package/__tests__/device-utils.test.ts +15 -1
  13. package/__tests__/device-wallet-session-store.test.ts +147 -21
  14. package/__tests__/deviceSettings.test.ts +7 -3
  15. package/__tests__/deviceUploadNft.test.ts +36 -4
  16. package/__tests__/firmware-update/device-update-bootloader.test.ts +13 -3
  17. package/__tests__/firmware-update/firmware-artifact-source.test.ts +61 -9
  18. package/__tests__/firmware-update/firmware-prepared-host-digest.test.ts +19 -9
  19. package/__tests__/firmware-update/firmware-prepared-resource-archive.test.ts +21 -0
  20. package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +63 -0
  21. package/__tests__/firmware-update/firmware-update-plan.test.ts +59 -55
  22. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +19 -5
  23. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +116 -0
  24. package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +979 -0
  25. package/__tests__/firmware-update/firmware-update-v4-parameters.test.ts +85 -0
  26. package/__tests__/get-device-state.test.ts +60 -21
  27. package/__tests__/logBlockEvent.test.ts +13 -1
  28. package/__tests__/open-wallet-session.test.ts +127 -1
  29. package/__tests__/protocol-v2-firmware-targets.test.ts +23 -0
  30. package/__tests__/protocol-v2-resources.test.ts +185 -88
  31. package/__tests__/protocol-v2-unlock-policy.test.ts +235 -2
  32. package/__tests__/protocol-v2.test.ts +2199 -486
  33. package/__tests__/refresh-device-state.test.ts +3 -1
  34. package/__tests__/resourceBase64Boundary.test.ts +48 -0
  35. package/__tests__/search-devices.test.ts +79 -32
  36. package/__tests__/ton-sign-message.test.ts +84 -0
  37. package/dist/api/BaseMethod.d.ts +2 -1
  38. package/dist/api/BaseMethod.d.ts.map +1 -1
  39. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  40. package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
  41. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  42. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  43. package/dist/api/FirmwareUpdateV4.d.ts +20 -6
  44. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  45. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  46. package/dist/api/SearchDevices.d.ts.map +1 -1
  47. package/dist/api/UploadPortfolio.d.ts +1 -1
  48. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  49. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  50. package/dist/api/device/DeviceChangePin.d.ts.map +1 -1
  51. package/dist/api/device/DeviceRebootToBoardloader.d.ts.map +1 -1
  52. package/dist/api/device/DeviceRebootToBootloader.d.ts.map +1 -1
  53. package/dist/api/device/DeviceSettings.d.ts.map +1 -1
  54. package/dist/api/device/DeviceVerify.d.ts +1 -0
  55. package/dist/api/device/DeviceVerify.d.ts.map +1 -1
  56. package/dist/api/device/DeviceWipe.d.ts.map +1 -1
  57. package/dist/api/firmware/FirmwareArtifactSource.d.ts +5 -1
  58. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -1
  59. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -1
  60. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +2 -2
  61. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  62. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +0 -15
  63. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  64. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
  65. package/dist/api/firmware/getBinary.d.ts +3 -2
  66. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  67. package/dist/api/firmware/protocolV2Release.d.ts.map +1 -1
  68. package/dist/api/helpers/base64Data.d.ts +16 -0
  69. package/dist/api/helpers/base64Data.d.ts.map +1 -0
  70. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts +3 -1
  71. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts.map +1 -1
  72. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts.map +1 -1
  73. package/dist/api/protocol-v2/DeviceInfoGet.d.ts +1 -1
  74. package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -1
  75. package/dist/api/protocol-v2/DeviceReboot.d.ts +1 -1
  76. package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
  77. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +2 -3
  78. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  79. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts +2 -3
  80. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  81. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  82. package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
  83. package/dist/core/index.d.ts +6 -1
  84. package/dist/core/index.d.ts.map +1 -1
  85. package/dist/data-manager/DataManager.d.ts +4 -3
  86. package/dist/data-manager/DataManager.d.ts.map +1 -1
  87. package/dist/data-manager/TransportManager.d.ts.map +1 -1
  88. package/dist/device/Device.d.ts +16 -3
  89. package/dist/device/Device.d.ts.map +1 -1
  90. package/dist/device/DeviceCommands.d.ts +4 -4
  91. package/dist/device/DeviceCommands.d.ts.map +1 -1
  92. package/dist/device/DeviceConnector.d.ts +1 -1
  93. package/dist/device/DeviceConnector.d.ts.map +1 -1
  94. package/dist/device/DevicePool.d.ts.map +1 -1
  95. package/dist/device/DeviceStateMapper.d.ts.map +1 -1
  96. package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
  97. package/dist/events/logBlockEvent.d.ts.map +1 -1
  98. package/dist/events/ui-request.d.ts +3 -0
  99. package/dist/events/ui-request.d.ts.map +1 -1
  100. package/dist/index.d.ts +54 -84
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +2150 -1469
  103. package/dist/protocols/protocol-v2/features.d.ts +13 -5
  104. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  105. package/dist/protocols/protocol-v2/index.d.ts +2 -2
  106. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  107. package/dist/protocols/protocol-v2/resources.d.ts +12 -7
  108. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  109. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +3 -1
  110. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -1
  111. package/dist/protocols/protocol-v2/walletSession.d.ts +3 -0
  112. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  113. package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
  114. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
  115. package/dist/types/api/protocolV2.d.ts +1 -1
  116. package/dist/types/api/protocolV2.d.ts.map +1 -1
  117. package/dist/types/device.d.ts.map +1 -1
  118. package/dist/types/settings.d.ts +3 -31
  119. package/dist/types/settings.d.ts.map +1 -1
  120. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  121. package/dist/utils/deviceFeaturesUtils.d.ts +3 -0
  122. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  123. package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
  124. package/dist/utils/deviceSettings.d.ts +1 -0
  125. package/dist/utils/deviceSettings.d.ts.map +1 -1
  126. package/dist/utils/index.d.ts +1 -1
  127. package/dist/utils/index.d.ts.map +1 -1
  128. package/dist/utils/patch.d.ts +1 -1
  129. package/dist/utils/patch.d.ts.map +1 -1
  130. package/dist/utils/pro2Nft.d.ts +7 -0
  131. package/dist/utils/pro2Nft.d.ts.map +1 -1
  132. package/package.json +6 -4
  133. package/src/api/BaseMethod.ts +8 -1
  134. package/src/api/CheckAllFirmwareRelease.ts +2 -4
  135. package/src/api/CheckBLEFirmwareRelease.ts +2 -2
  136. package/src/api/CheckBootloaderRelease.ts +3 -2
  137. package/src/api/CheckFirmwareRelease.ts +2 -2
  138. package/src/api/FirmwareUpdateV2.ts +12 -9
  139. package/src/api/FirmwareUpdateV3.ts +29 -19
  140. package/src/api/FirmwareUpdateV4.ts +934 -739
  141. package/src/api/OpenWalletSession.ts +39 -9
  142. package/src/api/PromptWebDeviceAccess.ts +2 -2
  143. package/src/api/SearchDevices.ts +6 -3
  144. package/src/api/UploadPortfolio.ts +9 -2
  145. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +2 -1
  146. package/src/api/device/DeviceChangePin.ts +4 -1
  147. package/src/api/device/DeviceRebootToBoardloader.ts +3 -1
  148. package/src/api/device/DeviceRebootToBootloader.ts +3 -1
  149. package/src/api/device/DeviceSettings.ts +34 -20
  150. package/src/api/device/DeviceVerify.ts +25 -0
  151. package/src/api/device/DeviceWipe.ts +4 -1
  152. package/src/api/firmware/FirmwareArtifactSource.ts +41 -14
  153. package/src/api/firmware/FirmwarePreparedResourceArchive.ts +16 -27
  154. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +9 -3
  155. package/src/api/firmware/FirmwareUpdatePlan.ts +37 -77
  156. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +10 -6
  157. package/src/api/firmware/protocolV2Release.ts +7 -2
  158. package/src/api/helpers/base64Data.ts +85 -0
  159. package/src/api/protocol-v2/DeviceFactoryInfoSet.ts +1 -1
  160. package/src/api/protocol-v2/DeviceFirmwareUpdate.ts +6 -28
  161. package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +7 -1
  162. package/src/api/protocol-v2/DeviceInfoGet.ts +3 -3
  163. package/src/api/protocol-v2/DeviceReboot.ts +5 -0
  164. package/src/api/protocol-v2/DeviceUploadNft.ts +61 -9
  165. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +53 -19
  166. package/src/api/protocol-v2/helpers.ts +8 -0
  167. package/src/api/ton/TonSignMessage.ts +5 -3
  168. package/src/core/index.ts +264 -55
  169. package/src/data/messages/messages-protocol-v2.json +140 -13
  170. package/src/data-manager/DataManager.ts +46 -39
  171. package/src/data-manager/TransportManager.ts +3 -1
  172. package/src/device/Device.ts +210 -44
  173. package/src/device/DeviceCommands.ts +27 -18
  174. package/src/device/DeviceConnector.ts +17 -3
  175. package/src/device/DevicePool.ts +9 -2
  176. package/src/device/DeviceStateMapper.ts +30 -24
  177. package/src/deviceProfile/buildDeviceFeatures.ts +10 -5
  178. package/src/events/logBlockEvent.ts +14 -1
  179. package/src/events/ui-request.ts +3 -0
  180. package/src/index.ts +0 -10
  181. package/src/protocols/protocol-v2/features.ts +22 -5
  182. package/src/protocols/protocol-v2/index.ts +2 -0
  183. package/src/protocols/protocol-v2/resources.ts +110 -147
  184. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +13 -1
  185. package/src/protocols/protocol-v2/walletSession.ts +65 -22
  186. package/src/types/api/firmwareUpdate.ts +1 -1
  187. package/src/types/api/firmwareUpdatePlan.ts +2 -2
  188. package/src/types/api/protocolV2.ts +1 -1
  189. package/src/types/device.ts +2 -1
  190. package/src/types/settings.ts +6 -34
  191. package/src/utils/deviceFeaturesCompat.ts +9 -4
  192. package/src/utils/deviceFeaturesUtils.ts +4 -0
  193. package/src/utils/deviceInfoUtils.ts +9 -2
  194. package/src/utils/deviceSettings.ts +11 -4
  195. package/src/utils/index.ts +1 -0
  196. package/src/utils/pro2Nft.ts +31 -8
  197. package/__tests__/firmware-memory-host.test.ts +0 -126
  198. package/dist/api/firmware/FirmwareMemoryHost.d.ts +0 -22
  199. package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +0 -1
  200. package/src/api/firmware/FirmwareMemoryHost.ts +0 -143
@@ -1,7 +1,15 @@
1
- import { EDeviceType, ERRORS, HardwareError, HardwareErrorCode, wait } from '@onekeyfe/hd-shared';
1
+ import {
2
+ EDeviceType,
3
+ ERRORS,
4
+ HardwareError,
5
+ HardwareErrorCode,
6
+ isBleStaleBondHardwareError,
7
+ wait,
8
+ } from '@onekeyfe/hd-shared';
2
9
  import JSZip from 'jszip';
3
10
  import {
4
11
  DeviceRebootType,
12
+ DeviceSessionPinType,
5
13
  PROTOCOL_V2_BLE_FILE_CHUNK_SIZE,
6
14
  PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE,
7
15
  PROTOCOL_V2_BLE_FIRMWARE_FILE_CHUNK_SIZE,
@@ -9,7 +17,7 @@ import {
9
17
  } from '@onekeyfe/hd-transport';
10
18
  import { sha256 } from '@noble/hashes/sha256';
11
19
 
12
- import { FirmwareUpdateTipMessage, UI_REQUEST } from '../events/ui-request';
20
+ import { FirmwareUpdateTipMessage, UI_REQUEST, createUiMessage } from '../events/ui-request';
13
21
  import { validateParams } from './helpers/paramsValidator';
14
22
  import {
15
23
  LoggerNames,
@@ -31,31 +39,30 @@ import {
31
39
  } from '../protocols/protocol-v2';
32
40
  import { requestProtocolV2DeviceInfo } from '../protocols/protocol-v2/features';
33
41
  import {
34
- parseProtocolV2ResourceManifest,
35
- selectProtocolV2ResourceManifestFiles,
42
+ PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH,
43
+ PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH,
44
+ PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_SIZE,
45
+ type ProtocolV2ResourcePackageHeader,
46
+ isProtocolV2ResourceArchiveEntryName,
47
+ parseProtocolV2ResourcePackage,
48
+ parseProtocolV2ResourcePackageHeader,
36
49
  } from '../protocols/protocol-v2/resources';
37
50
  import {
38
51
  getProtocolV2UnknownErrorText,
39
52
  isProtocolV2DeviceDisconnectedError,
40
53
  } from './protocol-v2/helpers';
54
+ import { isProtocolV2ResponseTimeout } from './helpers/protocolV2FileWrite';
41
55
  import {
42
56
  openFirmwareByteSource,
43
57
  readFirmwareByteSourceFully,
44
58
  writeFirmwareByteSource,
45
59
  } from './firmware/FirmwareArtifactSource';
46
- import {
47
- registerFirmwareUpdateHostBinding,
48
- resolveFirmwareUpdateHostBinding,
49
- unregisterFirmwareUpdateHostBinding,
50
- } from './firmware/FirmwareHostBinding';
60
+ import { resolveFirmwareUpdateHostBinding } from './firmware/FirmwareHostBinding';
51
61
  import {
52
62
  assertFirmwareUpdatePreparedPlanBinding,
53
63
  assertFirmwareUpdatePreparedPlanDeviceIdentity,
54
- prepareFirmwareUpdatePlan,
55
64
  validateFirmwareUpdatePreparedPlan,
56
65
  } from './firmware/FirmwareUpdatePreparedPlan';
57
- import { prepareFirmwareUpdateV4MemoryHost } from './firmware/FirmwareMemoryHost';
58
- import { buildProtocolV2LocalFirmwareUpdatePlan } from './firmware/FirmwareUpdatePlan';
59
66
 
60
67
  import type {
61
68
  FirmwareArtifactReference,
@@ -70,13 +77,9 @@ import type {
70
77
  IFirmwareReleaseInfo,
71
78
  IProtocolV2FirmwareComponent,
72
79
  IVersionArray,
80
+ KnownDevice,
73
81
  } from '../types';
74
82
  import type { FirmwareByteSource } from './firmware/FirmwareArtifactSource';
75
- import type {
76
- FirmwareMemoryArtifact,
77
- FirmwareMemoryArtifactEntry,
78
- FirmwareUpdateV4MemoryHost,
79
- } from './firmware/FirmwareMemoryHost';
80
83
 
81
84
  const Log = getLogger(LoggerNames.Method);
82
85
 
@@ -88,9 +91,9 @@ const PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT = 90 * 1000;
88
91
  const PROTOCOL_V2_FINAL_RECONNECT_TIMEOUT = 3 * 60 * 1000;
89
92
  const PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT = 5 * 1000;
90
93
  const PROTOCOL_V2_FIRMWARE_STATUS_RESPONSE_TIMEOUT = 15 * 1000;
91
- const PROTOCOL_V2_START_UPDATE_TIMEOUT = 3 * 60 * 1000;
92
- const PROTOCOL_V2_INSTALL_TIMEOUT = 8 * 60 * 1000;
93
- const PROTOCOL_V2_MISSING_TARGET_STATUS_GRACE_TIMEOUT = 30 * 1000;
94
+ const PROTOCOL_V2_INSTALL_TIMEOUT = 5 * 60 * 1000;
95
+ const PROTOCOL_V2_INSTALL_STATUS_INITIAL_DELAY = 1000;
96
+ const PROTOCOL_V2_INSTALL_FINISHED_AFTER_DISCONNECT_POLLS = 4;
94
97
  const PROTOCOL_V2_TARGET_STATUS_PENDING = 0;
95
98
  const PROTOCOL_V2_TARGET_STATUS_IN_PROGRESS = 1;
96
99
  const PROTOCOL_V2_TARGET_STATUS_FINISHED = 2;
@@ -103,12 +106,10 @@ const PROTOCOL_V2_CONNECT_POLL_INTERVAL = 500;
103
106
  const PROTOCOL_V2_CONNECT_SINGLE_TIMEOUT = 75 * 1000;
104
107
  const PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT = 30 * 1000;
105
108
  const PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT = 3;
109
+ const PROTOCOL_V2_TRANSFER_PROGRESS_HEARTBEAT_MS = 1000;
106
110
  const PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE = 'FirmwareInstallStatusConflict';
107
- const PROTOCOL_V2_OKPP_HEADER_SIZE = 0x52a0;
108
- const PROTOCOL_V2_OKPP_PAYLOAD_HASH_OFFSET = 0x200;
109
- const PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET = 0x240;
110
- const PROTOCOL_V2_OKPP_HASH_SIZE = 64;
111
- const PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES = 1024 * 1024;
111
+ const PROTOCOL_V2_INSTALL_FAILED_CODE = 'FirmwareInstallFailed';
112
+ const PROTOCOL_V2_INSTALL_TIMEOUT_CODE = 'FirmwareInstallTimeout';
112
113
  const PROTOCOL_V2_RESOURCE_FILE_MAX_COUNT = 512;
113
114
  const PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES = 256 * 1024 * 1024;
114
115
 
@@ -151,6 +152,18 @@ export function assertProtocolV2FirmwareTargetsSupported(
151
152
  if (params.se04Binary && (!hasExplicitTargetSelection || requestedTargets.has('se04'))) {
152
153
  unsupportedTargets.add('se04');
153
154
  }
155
+ if (
156
+ params.componentArtifacts?.se03 &&
157
+ (!hasExplicitTargetSelection || requestedTargets.has('se03'))
158
+ ) {
159
+ unsupportedTargets.add('se03');
160
+ }
161
+ if (
162
+ params.componentArtifacts?.se04 &&
163
+ (!hasExplicitTargetSelection || requestedTargets.has('se04'))
164
+ ) {
165
+ unsupportedTargets.add('se04');
166
+ }
154
167
 
155
168
  if (!unsupportedTargets.size || deviceType === EDeviceType.Pro2) return;
156
169
 
@@ -168,26 +181,26 @@ const getProtocolV2DeviceTransferProgress = (
168
181
  totalBytes: number
169
182
  ) => {
170
183
  if (!Number.isFinite(totalBytes) || totalBytes <= 0) {
171
- return 100;
184
+ return 0;
172
185
  }
173
186
  if (bytesBeforeChunk <= 0 && bytesAfterChunk < totalBytes) {
174
187
  return 0;
175
188
  }
176
- if (bytesAfterChunk >= totalBytes) {
177
- return 100;
178
- }
179
189
  return Math.min(Math.max(Math.ceil((bytesAfterChunk / totalBytes) * 100), 1), 99);
180
190
  };
181
191
 
182
192
  type ProtocolV2FirmwareUpdateStatusTarget = {
183
193
  target_id: number | string;
184
194
  status?: number | string;
195
+ progress_percent?: number;
196
+ phase_info?: {
197
+ phase: number | string;
198
+ progress_percent: number;
199
+ };
185
200
  payload_version?: number;
186
201
  path?: string;
187
202
  };
188
203
 
189
- type ProtocolV2FirmwareUpdateStartResponse = TypedResponseMessage<'Success'>;
190
-
191
204
  type ProtocolV2TargetBinary = { fileName: string; binary: ArrayBuffer; targetId: number };
192
205
  type ProtocolV2InstallItem = ProtocolV2TargetBinary & {
193
206
  kind: ProtocolV2RemoteComponentTarget['kind'];
@@ -221,7 +234,11 @@ type ProtocolV2ResourceBundleSource = {
221
234
 
222
235
  type ProtocolV2LocalResourceArchive = {
223
236
  binary: ArrayBuffer;
224
- materializedEntries: FirmwareMemoryArtifactEntry[];
237
+ resources: Array<{
238
+ entryName: string;
239
+ binary: ArrayBuffer;
240
+ header: ProtocolV2ResourcePackageHeader;
241
+ }>;
225
242
  };
226
243
 
227
244
  type JSZipSizedEntry = JSZip.JSZipObject & {
@@ -231,26 +248,11 @@ type JSZipSizedEntry = JSZip.JSZipObject & {
231
248
  };
232
249
  };
233
250
 
234
- type ProtocolV2ExecutionPhaseKind =
235
- | 'resource-sync'
236
- | 'bootloader-install'
237
- | 'bootloader-verify'
238
- | 'component-install'
239
- | 'final-verify';
240
-
241
- type ProtocolV2ExecutionPhase = {
242
- kind: ProtocolV2ExecutionPhaseKind;
251
+ type ProtocolV2TransferBatch = {
243
252
  installSources: ProtocolV2InstallSource[];
244
253
  resourceSources: ProtocolV2ResourceBundleSource[];
245
254
  };
246
255
 
247
- type ProtocolV2OkppHeader = {
248
- type: string;
249
- version: IVersionArray;
250
- payloadHash: string;
251
- headerHash: string;
252
- };
253
-
254
256
  const PROTOCOL_V2_REMOTE_COMPONENT_TARGETS: Readonly<
255
257
  Record<string, ProtocolV2RemoteComponentTarget>
256
258
  > = {
@@ -302,18 +304,9 @@ const PROTOCOL_V2_FIRMWARE_STAGING_PATHS = new Set(
302
304
  )
303
305
  );
304
306
 
305
- const PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH = 'vol0:/loaders/bootloader/boot_resource.okpkg';
306
- const PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH = `${PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH}.staging`;
307
-
308
307
  const isProtocolV2BootResourcePackagePath = (devicePath: string) =>
309
308
  typeof devicePath === 'string' &&
310
- devicePath.replace(/^vol0:(?!\/)/i, 'vol0:/').toLowerCase() ===
311
- PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH;
312
-
313
- const resolveProtocolV2ResourceWritePath = (devicePath: string) =>
314
- isProtocolV2BootResourcePackagePath(devicePath)
315
- ? PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH
316
- : devicePath;
309
+ devicePath.toLowerCase() === PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH;
317
310
 
318
311
  const PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID = new Map<
319
312
  number,
@@ -381,11 +374,22 @@ const isProtocolV2FirmwareStatusEndpointUnavailable = (error: unknown) => {
381
374
  );
382
375
  };
383
376
 
377
+ const isProtocolV2InstallResponseTimeout = (error: unknown) => {
378
+ if (!error || typeof error !== 'object') return false;
379
+ const { code } = error as { code?: number | string };
380
+ return (
381
+ code === 'response-timeout' ||
382
+ /\bresponse timeout\b/i.test(getProtocolV2UnknownErrorText(error))
383
+ );
384
+ };
385
+
384
386
  const isProtocolV2TerminalInstallStatusError = (error: unknown) =>
385
- error instanceof HardwareError &&
386
- (error.errorCode === HardwareErrorCode.FirmwareError ||
387
- error.errorCode === HardwareErrorCode.FirmwareVerificationFailed ||
388
- error.params?.firmwareUpdateCode === PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE);
387
+ isBleStaleBondHardwareError(error) ||
388
+ (error instanceof HardwareError &&
389
+ (error.errorCode === HardwareErrorCode.ActionCancelled ||
390
+ error.errorCode === HardwareErrorCode.FirmwareError ||
391
+ error.errorCode === HardwareErrorCode.FirmwareVerificationFailed ||
392
+ error.params?.firmwareUpdateCode === PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE));
389
393
 
390
394
  const isProtocolV2TargetStatusFinished = (status: ProtocolV2FirmwareUpdateStatusTarget['status']) =>
391
395
  normalizeProtocolV2TargetStatus(status) === PROTOCOL_V2_TARGET_STATUS_FINISHED;
@@ -422,6 +426,18 @@ const normalizeProtocolV2TargetStatus = (
422
426
  return undefined;
423
427
  };
424
428
 
429
+ const PROTOCOL_V2_INSTALL_PHASE_BY_DECODED_VALUE = new Map<
430
+ number | string,
431
+ 'prepare' | 'install' | 'verify'
432
+ >([
433
+ [0, 'prepare'],
434
+ [1, 'install'],
435
+ [2, 'verify'],
436
+ ['FW_MGMT_UPDATER_PHASE_PREPARE', 'prepare'],
437
+ ['FW_MGMT_UPDATER_PHASE_INSTALL', 'install'],
438
+ ['FW_MGMT_UPDATER_PHASE_VERIFY', 'verify'],
439
+ ]);
440
+
425
441
  const normalizeProtocolV2Hex = (value?: string) => value?.replace(/^0x/i, '').toLowerCase();
426
442
 
427
443
  const versionArrayToNumber = (version?: IVersionArray) => {
@@ -479,40 +495,9 @@ const toProtocolV2FiniteNumber = (value: unknown) => {
479
495
  return undefined;
480
496
  };
481
497
 
482
- const readProtocolV2Ascii = (bytes: Uint8Array, offset: number, length: number) =>
483
- Array.from(bytes.slice(offset, offset + length))
484
- .map(byte => String.fromCharCode(byte))
485
- .join('');
486
-
487
- const parseProtocolV2OkppHeader = (bytes: Uint8Array): ProtocolV2OkppHeader | null => {
488
- if (bytes.byteLength < PROTOCOL_V2_OKPP_HEADER_SIZE) return null;
489
- if (readProtocolV2Ascii(bytes, 0, 4) !== 'OKPP') return null;
490
-
491
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
492
- const headerLen = view.getUint32(0x0c, true);
493
- if (headerLen !== PROTOCOL_V2_OKPP_HEADER_SIZE) return null;
494
-
495
- const packedVersion = view.getUint32(0x10, true);
496
- return {
497
- type: readProtocolV2Ascii(bytes, 0x08, 4),
498
- version: [
499
- Math.floor(packedVersion / 0x10000) % 0x100,
500
- Math.floor(packedVersion / 0x100) % 0x100,
501
- packedVersion % 0x100,
502
- ],
503
- payloadHash: bytesToHex(
504
- bytes.slice(
505
- PROTOCOL_V2_OKPP_PAYLOAD_HASH_OFFSET,
506
- PROTOCOL_V2_OKPP_PAYLOAD_HASH_OFFSET + PROTOCOL_V2_OKPP_HASH_SIZE
507
- )
508
- ),
509
- headerHash: bytesToHex(
510
- bytes.slice(
511
- PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET,
512
- PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET + PROTOCOL_V2_OKPP_HASH_SIZE
513
- )
514
- ),
515
- };
498
+ const normalizeProtocolV2ProgressPercent = (value: unknown) => {
499
+ const progress = toProtocolV2FiniteNumber(value);
500
+ return progress === undefined ? undefined : Math.min(100, Math.max(0, progress));
516
501
  };
517
502
 
518
503
  export const isProtocolV2FirmwareFingerprintValid = (
@@ -557,7 +542,7 @@ export const assertProtocolV2ReconnectIdentity = (
557
542
  *
558
543
  * It intentionally does not fall back to FirmwareUpdateV3/V1 behavior:
559
544
  * - upload uses FilesystemFileWrite
560
- * - install uses DeviceFirmwareUpdateRequest
545
+ * - install uses DeviceFirmwareUpdateStage followed by an empty DeviceFirmwareUpdateRequest
561
546
  * - completion waits for target status to finish, reboots to normal, then polls DeviceInfo
562
547
  */
563
548
  export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareUpdateV4Params> {
@@ -579,19 +564,56 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
579
564
 
580
565
  private protocolV2CompletedTargetVersions = new Map<number, number>();
581
566
 
567
+ private protocolV2CompletedTargetIds = new Set<number>();
568
+
582
569
  private protocolV2LatestFinalFeatures?: Features;
583
570
 
584
- private protocolV2FinalStatusVerified = false;
571
+ private protocolV2LatestFinalDeviceInfo?: ProtocolV2DeviceInfo;
572
+
573
+ private protocolV2InstallBaselineVersions = new Map<number, string>();
574
+
575
+ private protocolV2InstallNeedsReconnect = false;
576
+
577
+ private protocolV2InstallDisconnectObserved = false;
578
+
579
+ private protocolV2InstallTerminalSuccessObserved = false;
580
+
581
+ private protocolV2LastRuntimeProbeFeatures?: Features;
582
+
583
+ private protocolV2LastTransferProgress?: number;
584
+
585
+ private protocolV2LastTransferProgressAt = 0;
585
586
 
586
587
  init() {
587
588
  this.allowDeviceMode = [UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE];
588
589
  this.requireDeviceMode = [];
589
590
  this.unlockPolicy = 'unlock-before-run';
591
+ // Protocol V2 device-management actions are not wallet-scoped, so either
592
+ // the main PIN or an Attach PIN may authorize them.
593
+ this.protocolV2PreUnlockPinType = DeviceSessionPinType.Any;
590
594
  this.useDevicePassphraseState = false;
591
595
  this.skipForceUpdateCheck = true;
592
596
 
593
597
  const { payload } = this;
594
598
 
599
+ const removedResourceInputs = ['resourceFiles', 'resourceBundleArtifacts'].filter(input =>
600
+ Object.prototype.hasOwnProperty.call(payload, input)
601
+ );
602
+ if (removedResourceInputs.length > 0) {
603
+ throw ERRORS.TypedError(
604
+ HardwareErrorCode.CallMethodInvalidParameter,
605
+ `Protocol V2 ${removedResourceInputs.join(
606
+ ' and '
607
+ )} inputs are no longer supported; use resourceArchiveBinary for local updates or preparedPlan with hostBindingGeneration for remote updates`
608
+ );
609
+ }
610
+ if (payload.hostBindingGeneration !== undefined && !payload.preparedPlan) {
611
+ throw ERRORS.TypedError(
612
+ HardwareErrorCode.CallMethodInvalidParameter,
613
+ 'Protocol V2 hostBindingGeneration requires preparedPlan for remote updates; use direct binaries or resourceArchiveBinary for local updates'
614
+ );
615
+ }
616
+
595
617
  this.protocolV2HasExplicitTargetSelection = payload.targetsToUpdate !== undefined;
596
618
 
597
619
  if (typeof payload.retryCount !== 'number') {
@@ -661,10 +683,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
661
683
  payload.hostBindingGeneration,
662
684
  preparedPlan.preparedPlanDigest
663
685
  ).artifactReader;
664
- } else if (payload.hostBindingGeneration !== undefined) {
665
- artifactReader = resolveFirmwareUpdateHostBinding(
666
- payload.hostBindingGeneration
667
- ).artifactReader;
668
686
  }
669
687
  if (preparedPlan) {
670
688
  assertFirmwareUpdatePreparedPlanBinding({
@@ -787,6 +805,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
787
805
  private async runProtocolV2() {
788
806
  await this.captureProtocolV2PhysicalIdentity();
789
807
  const deviceFeatures = await this.getProtocolV2DeviceFeatures();
808
+ this.protocolV2InstallBaselineVersions =
809
+ this.getProtocolV2ObservableTargetVersions(deviceFeatures);
810
+ this.protocolV2LastRuntimeProbeFeatures = undefined;
790
811
  const currentDeviceType = this.device.getCurrentDeviceType();
791
812
  const capabilityDeviceType =
792
813
  currentDeviceType === EDeviceType.Pro2 || currentDeviceType === EDeviceType.Neo
@@ -800,21 +821,14 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
800
821
  const deviceFirmwareType = getFirmwareType(deviceFeatures);
801
822
  const firmwareType = this.params.firmwareType ?? deviceFirmwareType;
802
823
  this.validateExpectedTargetVersions();
824
+ const wantsResources = !!this.params.targetsToUpdate?.includes('resource');
803
825
 
804
826
  if (
805
827
  !this.params.preparedPlan &&
806
828
  this.params.resourceArchiveBinary &&
807
829
  this.params.targetsToUpdate?.includes('resource')
808
830
  ) {
809
- const localMemoryHost = await this.prepareProtocolV2LocalMemoryHost({
810
- features: deviceFeatures,
811
- firmwareType,
812
- });
813
- try {
814
- return await this.runProtocolV2PreparedArtifacts(deviceFeatures, firmwareType);
815
- } finally {
816
- localMemoryHost.release();
817
- }
831
+ return this.runProtocolV2DirectArtifacts({});
818
832
  }
819
833
 
820
834
  const hasPreparedComponentArtifacts = Object.values(this.params.componentArtifacts ?? {}).some(
@@ -823,16 +837,16 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
823
837
  if (this.params.preparedPlan || hasPreparedComponentArtifacts) {
824
838
  return this.runProtocolV2PreparedArtifacts(deviceFeatures, firmwareType);
825
839
  }
826
- const wantsResources = !!this.params.targetsToUpdate?.includes('resource');
827
840
 
828
841
  let fwBinaryMap: ProtocolV2TargetBinary[] = [];
829
842
  let bootloaderBinary: ArrayBuffer | null = null;
830
843
  let installItems: ProtocolV2InstallItem[] | undefined;
844
+ let explicitInstallItems: ProtocolV2InstallItem[] | undefined;
831
845
  try {
832
846
  this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
833
847
  fwBinaryMap = this.collectExplicitTargetBinaries();
834
848
  bootloaderBinary = this.prepareBootloaderBinary();
835
- const explicitInstallItems = this.buildProtocolV2InstallItems({
849
+ explicitInstallItems = this.buildProtocolV2InstallItems({
836
850
  bootloaderBinary,
837
851
  fwBinaryMap,
838
852
  });
@@ -840,17 +854,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
840
854
  const needsRemoteFirmware = this.params.targetsToUpdate?.length
841
855
  ? missingFirmwareTargets.length > 0
842
856
  : explicitInstallItems.length === 0;
843
- if (wantsResources) {
844
- throw ERRORS.TypedError(
845
- HardwareErrorCode.RuntimeError,
846
- 'Protocol V2 resource archive must be provided through a local or external PreparedPlan',
847
- {
848
- firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
849
- }
850
- );
851
- }
857
+ const needsSdkManagedArtifacts = needsRemoteFirmware || wantsResources;
852
858
  if (
853
- needsRemoteFirmware &&
859
+ needsSdkManagedArtifacts &&
854
860
  (this.params.artifactReader ||
855
861
  DataManager.getSettings('firmwareManifestMode') === 'external-only')
856
862
  ) {
@@ -862,9 +868,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
862
868
  }
863
869
  );
864
870
  }
865
- if (needsRemoteFirmware) {
871
+ if (needsSdkManagedArtifacts) {
866
872
  // Remote updates must use a freshly fetched config before any reboot or file write.
867
- await DataManager.forceReloadData();
873
+ await DataManager.forceReloadData({
874
+ requireResources: wantsResources,
875
+ resourceDeviceType:
876
+ capabilityDeviceType === EDeviceType.Neo ? EDeviceType.Neo : EDeviceType.Pro2,
877
+ });
868
878
  }
869
879
  if (needsRemoteFirmware) {
870
880
  const remoteBinaries = await this.prepareRemoteProtocolV2Binaries(
@@ -887,6 +897,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
887
897
  targetId: item.targetId,
888
898
  }));
889
899
  }
900
+ if (wantsResources) {
901
+ this.params.resourceArchiveBinary = await this.downloadRemoteProtocolV2ResourceArchive(
902
+ deviceFeatures,
903
+ firmwareType
904
+ );
905
+ }
890
906
  this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
891
907
  } catch (err) {
892
908
  if (
@@ -903,6 +919,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
903
919
  throw normalizeFirmwarePreparationError(err);
904
920
  }
905
921
 
922
+ if (wantsResources && this.params.resourceArchiveBinary) {
923
+ return this.runProtocolV2DirectArtifacts({
924
+ availableInstallItems: installItems ?? explicitInstallItems,
925
+ announceDownload: false,
926
+ });
927
+ }
928
+
906
929
  if (!bootloaderBinary && fwBinaryMap.length === 0 && !installItems?.length) {
907
930
  throw ERRORS.TypedError(
908
931
  HardwareErrorCode.FirmwareUpdateDownloadFailed,
@@ -1053,132 +1076,69 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1053
1076
  return installSources;
1054
1077
  }
1055
1078
 
1056
- private async prepareProtocolV2LocalMemoryHost({
1057
- features,
1058
- firmwareType,
1079
+ private async runProtocolV2DirectArtifacts({
1080
+ availableInstallItems,
1081
+ announceDownload = true,
1059
1082
  }: {
1060
- features: Features;
1061
- firmwareType: EFirmwareType;
1062
- }): Promise<FirmwareUpdateV4MemoryHost> {
1063
- const availableInstallItems = this.buildProtocolV2InstallItems({
1064
- bootloaderBinary: this.prepareBootloaderBinary(),
1065
- fwBinaryMap: this.collectExplicitTargetBinaries(),
1066
- });
1067
- const requestedComponentTargets = new Set(
1068
- (this.params.targetsToUpdate ?? []).filter(
1069
- (target): target is Exclude<FirmwareUpdateV4Target, 'resource' | 'boot_resources'> =>
1070
- target !== 'resource' && target !== 'boot_resources'
1071
- )
1072
- );
1073
- const localComponentTargets = new Set(
1074
- availableInstallItems.flatMap(item => {
1075
- const target = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(item.targetId);
1076
- return target ? [target] : [];
1077
- })
1078
- );
1079
- const missingTarget = Array.from(requestedComponentTargets).find(
1080
- target => !localComponentTargets.has(target)
1081
- );
1082
- if (missingTarget) {
1083
- throw ERRORS.TypedError(
1084
- HardwareErrorCode.RuntimeError,
1085
- `Protocol V2 local update has no binary for requested target ${missingTarget}`,
1086
- {
1087
- firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
1088
- artifactName: missingTarget,
1089
- }
1083
+ availableInstallItems?: ProtocolV2InstallItem[];
1084
+ announceDownload?: boolean;
1085
+ }) {
1086
+ try {
1087
+ if (announceDownload) {
1088
+ this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
1089
+ }
1090
+ const installItems = this.filterProtocolV2LocalInstallItems(
1091
+ availableInstallItems ??
1092
+ this.buildProtocolV2InstallItems({
1093
+ bootloaderBinary: this.prepareBootloaderBinary(),
1094
+ fwBinaryMap: this.collectExplicitTargetBinaries(),
1095
+ })
1090
1096
  );
1091
- }
1092
- const installItems = this.filterProtocolV2LocalInstallItems(availableInstallItems);
1093
-
1094
- const planArtifacts: Parameters<typeof buildProtocolV2LocalFirmwareUpdatePlan>[0]['artifacts'] =
1095
- [];
1096
- const memoryArtifacts: FirmwareMemoryArtifact[] = [];
1097
- for (const item of installItems) {
1098
- const target = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(item.targetId);
1099
- if (!target || item.binary.byteLength <= 0) {
1097
+ const installSources = await Promise.all(
1098
+ installItems.map(async item => ({
1099
+ fileName: item.fileName,
1100
+ source: await this.openProtocolV2MemorySource(item.binary),
1101
+ targetId: item.targetId,
1102
+ kind: item.kind,
1103
+ }))
1104
+ );
1105
+ const resourceSources = this.params.resourceArchiveBinary
1106
+ ? await this.createProtocolV2ResourceSourcesFromArchive(this.params.resourceArchiveBinary)
1107
+ : [];
1108
+ if (installSources.length === 0 && resourceSources.length === 0) {
1100
1109
  throw ERRORS.TypedError(
1101
- HardwareErrorCode.RuntimeError,
1102
- `Protocol V2 local firmware artifact is invalid: ${item.fileName}`,
1103
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1110
+ HardwareErrorCode.FirmwareUpdateDownloadFailed,
1111
+ 'No firmware to update'
1104
1112
  );
1105
1113
  }
1106
- const artifactId = `component:${target}`;
1107
- planArtifacts.push({
1108
- artifactId,
1109
- target,
1110
- container: 'raw',
1111
- logicalName: item.fileName,
1112
- expectedSize: item.binary.byteLength,
1113
- expectedSha256: bytesToHex(sha256(new Uint8Array(item.binary))),
1114
- ...(this.params.expectedTargetVersions?.[target]
1115
- ? { targetVersion: this.params.expectedTargetVersions[target] }
1116
- : {}),
1114
+ if (announceDownload) {
1115
+ this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
1116
+ }
1117
+ return await this.executeProtocolV2SourceUpdate({
1118
+ installSources,
1119
+ resourceSources,
1117
1120
  });
1118
- memoryArtifacts.push({ artifactId, binary: item.binary });
1121
+ } finally {
1122
+ await this.closeProtocolV2PreparedSources();
1119
1123
  }
1124
+ }
1120
1125
 
1121
- const resourceArchive = await this.prepareProtocolV2LocalResourceArchive(
1122
- this.params.resourceArchiveBinary as ArrayBuffer
1123
- );
1124
- const resourceArtifactId = 'resource:archive';
1125
- planArtifacts.push({
1126
- artifactId: resourceArtifactId,
1127
- target: 'resource',
1128
- container: 'zip',
1129
- logicalName: 'protocol-v2-local-resource-archive',
1130
- expectedSize: resourceArchive.binary.byteLength,
1131
- expectedSha256: bytesToHex(sha256(new Uint8Array(resourceArchive.binary))),
1132
- });
1133
- memoryArtifacts.push({
1134
- artifactId: resourceArtifactId,
1135
- binary: resourceArchive.binary,
1136
- materializedEntries: resourceArchive.materializedEntries,
1137
- });
1138
-
1139
- const plan = buildProtocolV2LocalFirmwareUpdatePlan({
1140
- features,
1141
- firmwareType,
1142
- platform: this.params.platform,
1143
- artifacts: planArtifacts,
1144
- });
1145
- let memoryHost: FirmwareUpdateV4MemoryHost | undefined;
1146
- try {
1147
- memoryHost = prepareFirmwareUpdateV4MemoryHost({
1148
- sdk: {
1149
- prepareFirmwareUpdatePlan,
1150
- registerFirmwareUpdateHostBinding,
1151
- unregisterFirmwareUpdateHostBinding,
1152
- },
1153
- plan,
1154
- artifacts: memoryArtifacts,
1155
- });
1156
- const preparedPlan = validateFirmwareUpdatePreparedPlan(memoryHost.preparedPlan);
1157
- assertFirmwareUpdatePreparedPlanBinding({
1158
- preparedPlan,
1159
- executor: 'v4',
1160
- platform: this.params.platform,
1161
- scopeTargets: [],
1162
- bindings: [],
1163
- });
1164
- assertFirmwareUpdatePreparedPlanDeviceIdentity({
1165
- preparedPlan,
1166
- deviceIdentity: this.protocolV2ExpectedSerialNumber,
1167
- deviceModel: this.getProtocolV2PreparedPlanDeviceModel(features),
1126
+ private async createProtocolV2ResourceSourcesFromArchive(
1127
+ binary: ArrayBuffer
1128
+ ): Promise<ProtocolV2ResourceBundleSource[]> {
1129
+ const archive = await this.prepareProtocolV2LocalResourceArchive(binary);
1130
+ const sources: ProtocolV2ResourceBundleSource[] = [];
1131
+ for (const resource of archive.resources) {
1132
+ sources.push({
1133
+ name: resource.entryName.split('/').pop() ?? resource.entryName,
1134
+ source: await this.openProtocolV2MemorySource(resource.binary),
1135
+ devicePath: resource.header.devicePath,
1136
+ version: resource.header.version,
1137
+ payloadHash: resource.header.payloadHash,
1138
+ headerHash: resource.header.headerHash,
1168
1139
  });
1169
- const hostBinding = resolveFirmwareUpdateHostBinding(
1170
- memoryHost.hostBindingGeneration,
1171
- preparedPlan.preparedPlanDigest
1172
- );
1173
- this.params.preparedPlan = preparedPlan;
1174
- this.params.targetsToUpdate = [...preparedPlan.targetsToUpdate] as FirmwareUpdateV4Target[];
1175
- this.params.artifactReader = hostBinding.artifactReader;
1176
- this.params.componentArtifacts = undefined;
1177
- return memoryHost;
1178
- } catch (error) {
1179
- memoryHost?.release();
1180
- throw error;
1181
1140
  }
1141
+ return sources;
1182
1142
  }
1183
1143
 
1184
1144
  private async prepareProtocolV2LocalResourceArchive(
@@ -1202,130 +1162,67 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1202
1162
  );
1203
1163
  }
1204
1164
  const zipEntries = Object.values(zip.files);
1165
+ const resourceEntries = zipEntries.filter(
1166
+ entry => !entry.dir && isProtocolV2ResourceArchiveEntryName(entry.name)
1167
+ );
1205
1168
  if (
1206
- zipEntries.some(entry => entry.unsafeOriginalName && entry.unsafeOriginalName !== entry.name)
1169
+ resourceEntries.length === 0 ||
1170
+ resourceEntries.length > PROTOCOL_V2_RESOURCE_FILE_MAX_COUNT
1207
1171
  ) {
1208
1172
  throw ERRORS.TypedError(
1209
1173
  HardwareErrorCode.RuntimeError,
1210
- 'Protocol V2 local resource ZIP contains an unsafe entry path',
1211
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1212
- );
1213
- }
1214
- const entries = zipEntries.filter(entry => !entry.dir);
1215
- if (entries.length === 0 || entries.length > PROTOCOL_V2_RESOURCE_FILE_MAX_COUNT + 1) {
1216
- throw ERRORS.TypedError(
1217
- HardwareErrorCode.RuntimeError,
1218
- 'Protocol V2 local resource ZIP entry set is invalid',
1174
+ 'Protocol V2 resource ZIP has no valid resource package set',
1219
1175
  { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1220
1176
  );
1221
1177
  }
1222
- let declaredUncompressedSize = 0;
1223
- let declaredCompressedSize = 0;
1224
- for (const entry of entries) {
1225
- const sizes = getProtocolV2ZipEntrySizes(entry);
1226
- declaredCompressedSize += sizes.compressedSize;
1227
- declaredUncompressedSize += sizes.uncompressedSize;
1228
- const entryLimit =
1229
- entry.name === 'manifest.json'
1230
- ? PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
1231
- : PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES;
1178
+
1179
+ let totalSize = 0;
1180
+ const resources: ProtocolV2LocalResourceArchive['resources'] = [];
1181
+ const devicePaths = new Set<string>();
1182
+ for (const entry of resourceEntries) {
1183
+ const { compressedSize, uncompressedSize } = getProtocolV2ZipEntrySizes(entry);
1184
+ totalSize += uncompressedSize;
1232
1185
  if (
1233
- sizes.uncompressedSize > entryLimit ||
1234
- declaredCompressedSize > binary.byteLength ||
1235
- declaredUncompressedSize >
1236
- PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES + PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
1186
+ compressedSize > binary.byteLength ||
1187
+ uncompressedSize > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES ||
1188
+ totalSize > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES
1237
1189
  ) {
1238
1190
  throw ERRORS.TypedError(
1239
1191
  HardwareErrorCode.RuntimeError,
1240
- 'Protocol V2 local resource ZIP declared size exceeds the allowed limit',
1192
+ `Protocol V2 resource package size is invalid: ${entry.name}`,
1241
1193
  { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1242
1194
  );
1243
1195
  }
1244
- }
1245
- const manifestEntry = zip.file('manifest.json');
1246
- if (!manifestEntry) {
1247
- throw ERRORS.TypedError(
1248
- HardwareErrorCode.RuntimeError,
1249
- 'Protocol V2 local resource ZIP has no manifest.json',
1250
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1251
- );
1252
- }
1253
- const manifestBinary = await manifestEntry.async('arraybuffer');
1254
- if (
1255
- manifestBinary.byteLength <= 0 ||
1256
- manifestBinary.byteLength > PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
1257
- ) {
1258
- throw ERRORS.TypedError(
1259
- HardwareErrorCode.RuntimeError,
1260
- 'Protocol V2 local resource manifest size is invalid',
1261
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1262
- );
1263
- }
1264
-
1265
- let manifestValue: unknown;
1266
- try {
1267
- manifestValue = JSON.parse(new TextDecoder().decode(manifestBinary));
1268
- } catch (error) {
1269
- throw ERRORS.TypedError(
1270
- HardwareErrorCode.RuntimeError,
1271
- `Protocol V2 local resource manifest is invalid: ${String(error)}`,
1272
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1273
- );
1274
- }
1275
- const manifest = parseProtocolV2ResourceManifest(manifestValue);
1276
- const selectedFiles = selectProtocolV2ResourceManifestFiles({
1277
- manifest,
1278
- targetsToUpdate: this.params.targetsToUpdate ?? [],
1279
- });
1280
- const expectedEntryNames = new Set([
1281
- 'manifest.json',
1282
- ...selectedFiles.map(file => file.archive_path),
1283
- ]);
1284
- if (
1285
- entries.length !== expectedEntryNames.size ||
1286
- entries.some(entry => !expectedEntryNames.has(entry.name))
1287
- ) {
1288
- throw ERRORS.TypedError(
1289
- HardwareErrorCode.RuntimeError,
1290
- 'Protocol V2 local resource ZIP contains an unexpected entry',
1291
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1292
- );
1293
- }
1294
-
1295
- let totalSize = 0;
1296
- const materializedEntries: FirmwareMemoryArtifactEntry[] = [
1297
- { entryName: 'manifest.json', binary: manifestBinary },
1298
- ];
1299
- for (const file of selectedFiles) {
1300
- const entry = zip.file(file.archive_path);
1301
- if (!entry) {
1196
+ const fileBinary = await entry.async('arraybuffer');
1197
+ if (fileBinary.byteLength !== uncompressedSize) {
1302
1198
  throw ERRORS.TypedError(
1303
1199
  HardwareErrorCode.RuntimeError,
1304
- `Protocol V2 local resource ZIP is missing ${file.archive_path}`,
1305
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1200
+ `Protocol V2 resource package is incomplete: ${entry.name}`,
1201
+ { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
1306
1202
  );
1307
1203
  }
1308
- const { uncompressedSize } = getProtocolV2ZipEntrySizes(entry);
1309
- totalSize += uncompressedSize;
1310
- if (uncompressedSize !== file.size || totalSize > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES) {
1204
+ let header: ProtocolV2ResourcePackageHeader;
1205
+ try {
1206
+ header = parseProtocolV2ResourcePackage(fileBinary);
1207
+ } catch (error) {
1311
1208
  throw ERRORS.TypedError(
1312
1209
  HardwareErrorCode.RuntimeError,
1313
- `Protocol V2 local resource file declared size is invalid: ${file.archive_path}`,
1210
+ `Protocol V2 resource package is invalid: ${entry.name}: ${String(error)}`,
1314
1211
  { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1315
1212
  );
1316
1213
  }
1317
- const fileBinary = await entry.async('arraybuffer');
1318
- const digest = bytesToHex(sha256(new Uint8Array(fileBinary)));
1319
- if (fileBinary.byteLength !== file.size || digest !== file.sha256.toLowerCase()) {
1214
+ const canonicalDevicePath = header.devicePath.toLowerCase();
1215
+ if (devicePaths.has(canonicalDevicePath)) {
1320
1216
  throw ERRORS.TypedError(
1321
1217
  HardwareErrorCode.RuntimeError,
1322
- `Protocol V2 local resource file does not match manifest: ${file.archive_path}`,
1323
- { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
1218
+ `Protocol V2 resource ZIP contains duplicate device path: ${header.devicePath}`,
1219
+ { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1324
1220
  );
1325
1221
  }
1326
- materializedEntries.push({ entryName: file.archive_path, binary: fileBinary });
1222
+ devicePaths.add(canonicalDevicePath);
1223
+ resources.push({ entryName: entry.name, binary: fileBinary, header });
1327
1224
  }
1328
- return { binary, materializedEntries };
1225
+ return { binary, resources };
1329
1226
  }
1330
1227
 
1331
1228
  private async prepareProtocolV2ResourceSources(): Promise<ProtocolV2ResourceBundleSource[]> {
@@ -1390,110 +1287,20 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1390
1287
  );
1391
1288
  }
1392
1289
 
1393
- // materializedEntries 由宿主生成,只有与获批 ZIP 的规范字节完全一致后才能使用。
1394
- const verifiedArchive = await this.prepareProtocolV2LocalResourceArchive(archiveBinary);
1395
- const entries = archiveArtifact.materializedEntries ?? [];
1396
- const entriesByName = new Map(entries.map(entry => [entry.entryName, entry] as const));
1397
- if (
1398
- entries.length !== verifiedArchive.materializedEntries.length ||
1399
- verifiedArchive.materializedEntries.some(entry => {
1400
- const preparedEntry = entriesByName.get(entry.entryName);
1401
- const digest = bytesToHex(sha256(new Uint8Array(entry.binary)));
1402
- return (
1403
- !preparedEntry ||
1404
- preparedEntry.artifact.size !== entry.binary.byteLength ||
1405
- preparedEntry.artifact.sha256.toLowerCase() !== digest
1406
- );
1407
- })
1408
- ) {
1409
- throw ERRORS.TypedError(
1410
- HardwareErrorCode.RuntimeError,
1411
- 'Protocol V2 prepared resource entries do not match the approved archive',
1412
- { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
1413
- );
1414
- }
1415
-
1416
- const verifiedEntriesByName = new Map(
1417
- verifiedArchive.materializedEntries.map(entry => [entry.entryName, entry.binary] as const)
1418
- );
1419
- const manifestBinary = verifiedEntriesByName.get('manifest.json');
1420
- if (!manifestBinary) {
1421
- throw ERRORS.TypedError(
1422
- HardwareErrorCode.RuntimeError,
1423
- 'Protocol V2 prepared resource archive has no valid manifest.json',
1424
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1425
- );
1426
- }
1427
-
1428
- let manifestValue: unknown;
1429
- try {
1430
- manifestValue = JSON.parse(new TextDecoder().decode(manifestBinary));
1431
- } catch (error) {
1432
- throw ERRORS.TypedError(
1433
- HardwareErrorCode.RuntimeError,
1434
- `Protocol V2 prepared resource manifest is invalid: ${String(error)}`,
1435
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1436
- );
1437
- }
1438
- const manifest = parseProtocolV2ResourceManifest(manifestValue);
1439
- const selectedFiles = selectProtocolV2ResourceManifestFiles({
1440
- manifest,
1441
- targetsToUpdate: this.params.targetsToUpdate ?? [],
1442
- });
1443
- if (selectedFiles.length > PROTOCOL_V2_RESOURCE_FILE_MAX_COUNT) {
1444
- throw ERRORS.TypedError(
1445
- HardwareErrorCode.RuntimeError,
1446
- 'Protocol V2 prepared resource archive contains too many files',
1447
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1448
- );
1449
- }
1450
-
1451
- let totalSize = 0;
1452
- const sources: ProtocolV2ResourceBundleSource[] = [];
1453
- for (const [index, file] of selectedFiles.entries()) {
1454
- const binary = verifiedEntriesByName.get(file.archive_path);
1455
- if (!binary || binary.byteLength !== file.size) {
1456
- throw ERRORS.TypedError(
1457
- HardwareErrorCode.RuntimeError,
1458
- `Protocol V2 prepared resource file does not match manifest: ${file.archive_path}`,
1459
- { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
1460
- );
1461
- }
1462
- totalSize += binary.byteLength;
1463
- if (totalSize > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES) {
1464
- throw ERRORS.TypedError(
1465
- HardwareErrorCode.RuntimeError,
1466
- 'Protocol V2 prepared resource archive exceeds the total size limit',
1467
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1468
- );
1469
- }
1470
- // 使用从获批 ZIP 中提取的规范字节,避免宿主在校验后替换 entry reader 内容。
1471
- const source = await this.openProtocolV2MemorySource(binary);
1472
- const header =
1473
- source.size >= PROTOCOL_V2_OKPP_HEADER_SIZE
1474
- ? parseProtocolV2OkppHeader(
1475
- new Uint8Array(await source.readAt(0, PROTOCOL_V2_OKPP_HEADER_SIZE))
1476
- )
1477
- : null;
1478
- sources.push({
1479
- name: file.original_name || `resource-${index}`,
1480
- source,
1481
- devicePath: file.device_path,
1482
- ...(header
1483
- ? {
1484
- version: header.version,
1485
- payloadHash: header.payloadHash,
1486
- headerHash: header.headerHash,
1487
- }
1488
- : {}),
1489
- });
1490
- }
1491
- return sources;
1290
+ // The verified ZIP bytes are authoritative. Non-okpkg entries are release
1291
+ // metadata only; each RESOURCE package carries its own device path.
1292
+ return this.createProtocolV2ResourceSourcesFromArchive(archiveBinary);
1492
1293
  }
1493
1294
 
1494
- private async runProtocolV2PreparedArtifacts(features: Features, firmwareType: EFirmwareType) {
1295
+ private async runProtocolV2PreparedArtifacts(
1296
+ features: Features,
1297
+ firmwareType: EFirmwareType,
1298
+ announceDownload = true
1299
+ ) {
1495
1300
  try {
1496
- this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
1301
+ if (announceDownload) {
1302
+ this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
1303
+ }
1497
1304
  const installSources = await this.prepareProtocolV2InstallSources(firmwareType, features);
1498
1305
  const resourceSources = await this.prepareProtocolV2ResourceSources();
1499
1306
  if (installSources.length === 0 && resourceSources.length === 0) {
@@ -1502,7 +1309,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1502
1309
  'No firmware to update'
1503
1310
  );
1504
1311
  }
1505
- this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
1312
+ if (announceDownload) {
1313
+ this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
1314
+ }
1506
1315
 
1507
1316
  return await this.executeProtocolV2SourceUpdate({
1508
1317
  installSources,
@@ -1557,21 +1366,20 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1557
1366
  const expected = this.params?.expectedTargetVersions;
1558
1367
  if (!expected) return;
1559
1368
  const features = this.protocolV2LatestFinalFeatures;
1560
- const requestedTargets = this.getProtocolV2RequestedTargets();
1561
- const applicationTargets = requestedTargets.filter(
1562
- target => target === 'app_v1' || target === 'app_v2'
1563
- );
1369
+ const deviceInfo = this.protocolV2LatestFinalDeviceInfo;
1564
1370
  const visibleVersions: Partial<Record<FirmwareUpdateV4Target, string>> = {
1565
1371
  boot: features ? getDeviceBootloaderVersion(features).join('.') : undefined,
1372
+ // Protocol V2 firmware exposes P1 as the device firmware version after reboot.
1373
+ app_v1: features
1374
+ ? getDeviceFirmwareVersion(features).join('.')
1375
+ : deviceInfo?.main_mcu?.application?.version,
1376
+ app_v2: deviceInfo?.main_mcu?.application_data?.version,
1566
1377
  coprocessor: features ? getDeviceBLEFirmwareVersion(features).join('.') : undefined,
1567
1378
  se01: features?.se01Version ?? undefined,
1568
1379
  se02: features?.se02Version ?? undefined,
1569
1380
  se03: features?.se03Version ?? undefined,
1570
1381
  se04: features?.se04Version ?? undefined,
1571
1382
  };
1572
- if (features && applicationTargets.length === 1) {
1573
- visibleVersions[applicationTargets[0]] = getDeviceFirmwareVersion(features).join('.');
1574
- }
1575
1383
  const expectedEntries = (
1576
1384
  Object.entries(expected) as Array<[FirmwareUpdateV4Target, string]>
1577
1385
  ).filter(([target]) => !targets || targets.includes(target));
@@ -1588,9 +1396,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1588
1396
  Math.floor(statusVersion / 0x100) % 0x100
1589
1397
  }.${statusVersion % 0x100}`;
1590
1398
  if (!observedVersion) {
1591
- if (this.protocolV2FinalStatusVerified) {
1399
+ const hasCompleteTargetEvidence =
1400
+ targetId !== undefined && this.protocolV2CompletedTargetIds.has(targetId);
1401
+ if (hasCompleteTargetEvidence) {
1592
1402
  Log.warn(
1593
- `Protocol V2 target ${target} has no observable final version; completed target status is authoritative`
1403
+ `Protocol V2 target ${target} has no observable final version; install completion is authoritative`
1594
1404
  );
1595
1405
  } else {
1596
1406
  throw ERRORS.TypedError(
@@ -1607,37 +1417,23 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1607
1417
  }
1608
1418
  }
1609
1419
 
1610
- private getProtocolV2RequestedTargets(): FirmwareUpdateV4Target[] {
1611
- if (this.params.targetsToUpdate?.length) {
1612
- return [...new Set(this.params.targetsToUpdate)];
1420
+ private async getProtocolV2DeviceFeatures(): Promise<Features> {
1421
+ let { features } = this.device;
1422
+ if (typeof this.device.getFeatures === 'function') {
1423
+ features ??= await this.device.getFeatures();
1613
1424
  }
1614
- if (this.params.preparedPlan?.targetsToUpdate.length) {
1615
- return [...new Set(this.params.preparedPlan.targetsToUpdate)] as FirmwareUpdateV4Target[];
1425
+ if (!features) {
1426
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Device features not available');
1616
1427
  }
1617
- const targets = new Set<FirmwareUpdateV4Target>();
1618
- Object.keys(this.params.componentArtifacts ?? {}).forEach(target =>
1619
- targets.add(target as FirmwareUpdateV4Target)
1620
- );
1621
- if (this.params.bootloaderBinary) targets.add('boot');
1622
- if (this.params.applicationP1Binary) targets.add('app_v1');
1623
- if (this.params.applicationP2Binary) targets.add('app_v2');
1624
- if (this.params.coprocessorBinary) targets.add('coprocessor');
1625
- if (this.params.se01Binary) targets.add('se01');
1626
- if (this.params.se02Binary) targets.add('se02');
1627
- if (this.params.se03Binary) targets.add('se03');
1628
- if (this.params.se04Binary) targets.add('se04');
1629
- return Array.from(targets);
1630
- }
1631
-
1632
- private async getProtocolV2DeviceFeatures(): Promise<Features> {
1633
- if (this.device.features) {
1634
- return this.device.features;
1635
- }
1636
- if (typeof this.device.getFeatures === 'function') {
1637
- const features = await this.device.getFeatures();
1638
- if (features) return features;
1428
+ const deviceType = this.device.getCurrentDeviceType();
1429
+ if (deviceType !== EDeviceType.Pro2 && deviceType !== EDeviceType.Neo) {
1430
+ return features;
1639
1431
  }
1640
- throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Device features not available');
1432
+ return {
1433
+ ...features,
1434
+ deviceType,
1435
+ firmwareType: this.device.getCurrentFirmwareType(),
1436
+ };
1641
1437
  }
1642
1438
 
1643
1439
  private getProtocolV2SerialNumber(deviceInfo: ProtocolV2DeviceInfo) {
@@ -1840,22 +1636,54 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1840
1636
  `Protocol V2 firmware fingerprint mismatch: ${key}/${component.target}`
1841
1637
  );
1842
1638
  }
1843
- const expectedPayloadHash = normalizeProtocolV2Hex(component.payloadHash);
1844
- if (expectedPayloadHash) {
1845
- const header = parseProtocolV2OkppHeader(toProtocolV2Bytes(binary));
1846
- if (!header || header.payloadHash !== expectedPayloadHash) {
1847
- throw ERRORS.TypedError(
1848
- HardwareErrorCode.RuntimeError,
1849
- `Protocol V2 firmware payload hash mismatch: ${key}/${component.target}`
1850
- );
1851
- }
1852
- }
1853
1639
  return {
1854
1640
  ...target,
1855
1641
  binary,
1856
1642
  };
1857
1643
  }
1858
1644
 
1645
+ private async downloadRemoteProtocolV2ResourceArchive(
1646
+ features: Features,
1647
+ firmwareType: EFirmwareType
1648
+ ): Promise<ArrayBuffer> {
1649
+ const deviceType = getDeviceType(features);
1650
+ if (deviceType !== EDeviceType.Pro2 && deviceType !== EDeviceType.Neo) {
1651
+ throw ERRORS.TypedError(
1652
+ HardwareErrorCode.RuntimeError,
1653
+ 'Protocol V2 resource archive requires a Pro2 or Neo device'
1654
+ );
1655
+ }
1656
+ const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
1657
+ const source = DataManager.getProtocolV2ResourceSource(release);
1658
+ const expectedSha256 = normalizeProtocolV2Hex(source?.archiveSha256);
1659
+ if (
1660
+ !source?.archiveUrl ||
1661
+ !Number.isSafeInteger(source.archiveSize) ||
1662
+ source.archiveSize <= 0 ||
1663
+ source.archiveSize > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES ||
1664
+ !expectedSha256 ||
1665
+ !/^[0-9a-f]{64}$/u.test(expectedSha256)
1666
+ ) {
1667
+ throw ERRORS.TypedError(
1668
+ HardwareErrorCode.RuntimeError,
1669
+ 'Protocol V2 resource archive integrity metadata is invalid',
1670
+ { firmwareUpdateCode: 'FirmwarePlanInvalid' }
1671
+ );
1672
+ }
1673
+ const { binary } = await getSysResourceBinary(source.archiveUrl);
1674
+ if (
1675
+ binary.byteLength !== source.archiveSize ||
1676
+ bytesToHex(sha256(new Uint8Array(binary))) !== expectedSha256
1677
+ ) {
1678
+ throw ERRORS.TypedError(
1679
+ HardwareErrorCode.RuntimeError,
1680
+ 'Protocol V2 resource archive does not match the remote config',
1681
+ { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
1682
+ );
1683
+ }
1684
+ return binary;
1685
+ }
1686
+
1859
1687
  private async prepareRemoteProtocolV2Binaries(
1860
1688
  firmwareType: EFirmwareType,
1861
1689
  features: Features,
@@ -1899,8 +1727,14 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1899
1727
  : undefined;
1900
1728
  if (updateTarget && targetsToUpdate.has(updateTarget)) {
1901
1729
  const explicitInstallItem = explicitInstallItemByTargetId.get(target.targetId);
1902
- const installItem =
1903
- explicitInstallItem ?? (await this.downloadRemoteProtocolV2Component(key, component));
1730
+ let installItem = explicitInstallItem;
1731
+ if (!installItem) {
1732
+ installItem = await this.downloadRemoteProtocolV2Component(key, component);
1733
+ if (component.version) {
1734
+ this.params.expectedTargetVersions ??= {};
1735
+ this.params.expectedTargetVersions[updateTarget] = component.version.join('.');
1736
+ }
1737
+ }
1904
1738
  if (installItem.kind === 'bootloader') {
1905
1739
  bootloaderBinary = installItem.binary;
1906
1740
  } else {
@@ -1939,6 +1773,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1939
1773
  return `vol0:/${path}`;
1940
1774
  }
1941
1775
 
1776
+ private getProtocolV2ResourceComparePath(devicePath: string) {
1777
+ return isProtocolV2BootResourcePackagePath(devicePath)
1778
+ ? PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH
1779
+ : devicePath;
1780
+ }
1781
+
1942
1782
  private async readProtocolV2DeviceFileHeader(path: string, expectedSize?: number) {
1943
1783
  const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
1944
1784
  const filePath = this.getProtocolV2ResourceFilePath(path);
@@ -1950,7 +1790,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1950
1790
  !pathInfoRes.message?.exist ||
1951
1791
  pathInfoRes.message?.directory ||
1952
1792
  fileSize === undefined ||
1953
- fileSize < PROTOCOL_V2_OKPP_HEADER_SIZE ||
1793
+ fileSize < PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_SIZE ||
1954
1794
  (expectedSize !== undefined && fileSize !== expectedSize)
1955
1795
  ) {
1956
1796
  return null;
@@ -1959,8 +1799,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1959
1799
  const chunkSize = this.getProtocolV2FirmwareChunkSize('read');
1960
1800
  const chunks: Uint8Array[] = [];
1961
1801
  let offset = 0;
1962
- while (offset < PROTOCOL_V2_OKPP_HEADER_SIZE) {
1963
- const readLen = Math.min(chunkSize, PROTOCOL_V2_OKPP_HEADER_SIZE - offset);
1802
+ while (offset < PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_SIZE) {
1803
+ const readLen = Math.min(chunkSize, PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_SIZE - offset);
1964
1804
  const res = await typedCall('FilesystemFileRead', 'FilesystemFile', {
1965
1805
  file: {
1966
1806
  path: filePath,
@@ -1982,7 +1822,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1982
1822
  headerBytes.set(chunk, cursor);
1983
1823
  cursor += chunk.byteLength;
1984
1824
  });
1985
- return parseProtocolV2OkppHeader(headerBytes);
1825
+ try {
1826
+ return parseProtocolV2ResourcePackageHeader(headerBytes, fileSize);
1827
+ } catch {
1828
+ return null;
1829
+ }
1986
1830
  }
1987
1831
 
1988
1832
  /** Compare the downloaded and installed okpkg headers before transferring a resource. */
@@ -1997,7 +1841,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1997
1841
 
1998
1842
  try {
1999
1843
  const header = await this.readProtocolV2DeviceFileHeader(
2000
- bundle.devicePath,
1844
+ this.getProtocolV2ResourceComparePath(bundle.devicePath),
2001
1845
  bundle.source.size
2002
1846
  );
2003
1847
  if (!header) return false;
@@ -2012,6 +1856,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2012
1856
  if (!expectedHeaderHash || header.headerHash !== expectedHeaderHash) return false;
2013
1857
  return true;
2014
1858
  } catch (error) {
1859
+ if (isBleStaleBondHardwareError(error)) {
1860
+ throw error;
1861
+ }
2015
1862
  Log.log(`[FirmwareUpdateV4] RESC bundle ${bundle.name} header check failed: `, error);
2016
1863
  return false;
2017
1864
  }
@@ -2041,21 +1888,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2041
1888
  return this.device.features?.mode === 'romloader';
2042
1889
  }
2043
1890
 
2044
- async enterProtocolV2BootloaderMode() {
2045
- // romloader is the first update environment and forwards targets to bootloader.
2046
- // It rejects DeviceRebootType.Bootloader, so reuse the current connection.
2047
- if (this.isProtocolV2RomloaderMode()) {
2048
- Log.debug('Protocol V2 device is in romloader mode; start firmware update directly');
2049
- this.protocolV2ExecutionInLoader = true;
2050
- return false;
2051
- }
2052
- if (this.isProtocolV2BootloaderMode()) {
2053
- Log.debug('Protocol V2 device is already in bootloader mode, skip reboot');
2054
- this.protocolV2ExecutionInLoader = true;
2055
- this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
2056
- return false;
2057
- }
2058
-
1891
+ private async rebootProtocolV2ToBootloader() {
2059
1892
  try {
2060
1893
  this.postTipMessage(FirmwareUpdateTipMessage.AutoRebootToBootloader);
2061
1894
  await this.protocolV2Reboot(DeviceRebootType.Bootloader);
@@ -2073,6 +1906,24 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2073
1906
  }
2074
1907
  }
2075
1908
 
1909
+ async enterProtocolV2BootloaderMode() {
1910
+ // romloader is the first update environment and forwards targets to bootloader.
1911
+ // It rejects DeviceRebootType.Bootloader, so reuse the current connection.
1912
+ if (this.isProtocolV2RomloaderMode()) {
1913
+ Log.debug('Protocol V2 device is in romloader mode; start firmware update directly');
1914
+ this.protocolV2ExecutionInLoader = true;
1915
+ return false;
1916
+ }
1917
+ if (this.isProtocolV2BootloaderMode()) {
1918
+ Log.debug('Protocol V2 device is already in bootloader mode, skip reboot');
1919
+ this.protocolV2ExecutionInLoader = true;
1920
+ this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
1921
+ return false;
1922
+ }
1923
+
1924
+ return this.rebootProtocolV2ToBootloader();
1925
+ }
1926
+
2076
1927
  private async waitForProtocolV2BootloaderMode(
2077
1928
  timeout = PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT,
2078
1929
  retryInterval = 1000
@@ -2101,7 +1952,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2101
1952
  }
2102
1953
  lastError = new Error('Protocol V2 device is reachable but is not in bootloader mode');
2103
1954
  } catch (error) {
2104
- if (this.isProtocolV2ReconnectIdentityError(error)) {
1955
+ if (isBleStaleBondHardwareError(error) || this.isProtocolV2ReconnectIdentityError(error)) {
2105
1956
  throw error;
2106
1957
  }
2107
1958
  shouldReconnect = true;
@@ -2157,68 +2008,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2157
2008
  return entries;
2158
2009
  }
2159
2010
 
2160
- private buildProtocolV2ExecutionPhases({
2161
- installSources,
2162
- resourceSources,
2163
- }: {
2164
- installSources: ProtocolV2InstallSource[];
2165
- resourceSources: ProtocolV2ResourceBundleSource[];
2166
- }): ProtocolV2ExecutionPhase[] {
2167
- const phases: ProtocolV2ExecutionPhase[] = [];
2168
- const bootResourceSources = resourceSources.filter(source =>
2169
- isProtocolV2BootResourcePackagePath(source.devicePath)
2170
- );
2171
- const remainingResourceSources = resourceSources.filter(
2172
- source => !isProtocolV2BootResourcePackagePath(source.devicePath)
2173
- );
2174
- // A previous interrupted run may have left a promotable staging file. Replace
2175
- // it with the current approved package before any firmware-triggered reboot.
2176
- if (bootResourceSources.length > 0) {
2177
- phases.push({
2178
- kind: 'resource-sync',
2179
- installSources: [],
2180
- resourceSources: bootResourceSources,
2181
- });
2182
- }
2183
- const bootloaderSources = installSources.filter(source => source.kind === 'bootloader');
2184
- if (bootloaderSources.length > 0) {
2185
- phases.push(
2186
- {
2187
- kind: 'bootloader-install',
2188
- installSources: bootloaderSources,
2189
- resourceSources: [],
2190
- },
2191
- {
2192
- kind: 'bootloader-verify',
2193
- installSources: [],
2194
- resourceSources: [],
2195
- }
2196
- );
2197
- }
2198
- if (remainingResourceSources.length > 0) {
2199
- phases.push({
2200
- kind: 'resource-sync',
2201
- installSources: [],
2202
- resourceSources: remainingResourceSources,
2203
- });
2204
- }
2205
- const componentSources = installSources.filter(source => source.kind !== 'bootloader');
2206
- if (componentSources.length > 0) {
2207
- phases.push({
2208
- kind: 'component-install',
2209
- installSources: componentSources,
2210
- resourceSources: [],
2211
- });
2212
- }
2213
- phases.push({
2214
- kind: 'final-verify',
2215
- installSources: [],
2216
- resourceSources: [],
2217
- });
2218
- return phases;
2219
- }
2220
-
2221
- private async executeProtocolV2Phases({
2011
+ private async executeProtocolV2SourceUpdate({
2222
2012
  installSources,
2223
2013
  resourceSources,
2224
2014
  }: {
@@ -2226,38 +2016,22 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2226
2016
  resourceSources: ProtocolV2ResourceBundleSource[];
2227
2017
  }) {
2228
2018
  this.protocolV2BootResourceStagingSafe = false;
2229
- const phases = this.buildProtocolV2ExecutionPhases({
2230
- installSources,
2231
- resourceSources,
2232
- });
2233
- for (const phase of phases) {
2234
- if (phase.kind === 'final-verify') {
2235
- return this.completeProtocolV2FinalVerification();
2236
- }
2237
- if (phase.kind === 'resource-sync') {
2238
- await this.enterProtocolV2BootloaderMode();
2239
- if (
2240
- !phase.resourceSources.some(source =>
2241
- isProtocolV2BootResourcePackagePath(source.devicePath)
2242
- )
2243
- ) {
2244
- await this.ensureProtocolV2BootResourceStagingIsEmpty();
2245
- }
2246
- await this.executeProtocolV2TransferPhase(phase);
2247
- } else if (phase.kind === 'bootloader-install' || phase.kind === 'component-install') {
2248
- await this.enterProtocolV2BootloaderMode();
2249
- await this.ensureProtocolV2BootResourceStagingIsEmpty();
2250
- await this.executeProtocolV2TransferPhase(phase);
2251
- await this.exitProtocolV2BootloaderToNormal();
2252
- } else if (phase.kind === 'bootloader-verify') {
2253
- await this.waitForProtocolV2FinalFeatures();
2254
- this.assertExpectedProtocolV2Versions(['boot']);
2255
- }
2019
+ if (installSources.length > 0 || resourceSources.length > 0) {
2020
+ await this.enterProtocolV2BootloaderMode();
2021
+ // Clear stale boot-resource staging before writing any artifacts. The new
2022
+ // boot resource, resources and firmware then share one transfer session,
2023
+ // one global progress range and one multi-target install request.
2024
+ // Preserve the approved source order instead of synthesizing a boot-first
2025
+ // phase. Loader firmware owns the cross-stage handoff: bootloader runs its
2026
+ // component targets and leaves boot pending for romloader, while pending
2027
+ // runner-backed records always resume before the application boots.
2028
+ await this.ensureProtocolV2BootResourceStagingIsEmpty();
2029
+ await this.executeProtocolV2TransferPhase({
2030
+ installSources,
2031
+ resourceSources,
2032
+ });
2256
2033
  }
2257
- throw ERRORS.TypedError(
2258
- HardwareErrorCode.RuntimeError,
2259
- 'Protocol V2 execution has no final verification phase'
2260
- );
2034
+ return this.completeProtocolV2FinalVerification();
2261
2035
  }
2262
2036
 
2263
2037
  private async ensureProtocolV2BootResourceStagingIsEmpty() {
@@ -2289,19 +2063,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2289
2063
  this.protocolV2BootResourceStagingSafe = true;
2290
2064
  }
2291
2065
 
2292
- private async executeProtocolV2SourceUpdate({
2293
- installSources,
2294
- resourceSources,
2295
- }: {
2296
- installSources: ProtocolV2InstallSource[];
2297
- resourceSources: ProtocolV2ResourceBundleSource[];
2298
- }) {
2299
- return this.executeProtocolV2Phases({
2300
- installSources,
2301
- resourceSources,
2302
- });
2303
- }
2304
-
2305
2066
  private async executeProtocolV2Update({
2306
2067
  fwBinaryMap,
2307
2068
  bootloaderBinary,
@@ -2326,7 +2087,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2326
2087
  kind: item.kind,
2327
2088
  }))
2328
2089
  );
2329
- return await this.executeProtocolV2Phases({
2090
+ return await this.executeProtocolV2SourceUpdate({
2330
2091
  installSources,
2331
2092
  resourceSources: [],
2332
2093
  });
@@ -2338,15 +2099,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2338
2099
  private async executeProtocolV2TransferPhase({
2339
2100
  installSources,
2340
2101
  resourceSources,
2341
- }: ProtocolV2ExecutionPhase) {
2102
+ }: ProtocolV2TransferBatch) {
2342
2103
  let totalSize = installSources.reduce((total, item) => total + item.source.size, 0);
2343
2104
  const resourcesToSync: ProtocolV2ResourceBundleSource[] = [];
2344
2105
  for (const resource of resourceSources) {
2345
- // Early boot promotes this mounted package's staging file. Always replace any
2346
- // stale staging bytes with the artifact approved by the current PreparedPlan,
2347
- // even when the mounted final file itself is already current.
2348
- const requiresFreshStaging = isProtocolV2BootResourcePackagePath(resource.devicePath);
2349
- if (!requiresFreshStaging && (await this.isProtocolV2ResourceBundleUpToDate(resource))) {
2106
+ if (await this.isProtocolV2ResourceBundleUpToDate(resource)) {
2350
2107
  Log.log(`[FirmwareUpdateV4] skip RESC bundle ${resource.name}; already up to date`);
2351
2108
  } else {
2352
2109
  resourcesToSync.push(resource);
@@ -2355,11 +2112,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2355
2112
  }
2356
2113
 
2357
2114
  this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
2115
+ this.protocolV2LastTransferProgress = undefined;
2116
+ this.protocolV2LastTransferProgressAt = 0;
2358
2117
  let processedSize = 0;
2359
2118
  for (const resource of resourcesToSync) {
2360
2119
  // The bootloader keeps its live resource package mounted. FatFs rejects
2361
2120
  // replacing an open file, so early boot promotes this staging file before mounting it.
2362
- const writePath = resolveProtocolV2ResourceWritePath(resource.devicePath);
2121
+ const writePath = resource.devicePath;
2363
2122
  processedSize = await this.protocolV2SourceUpdateProcess({
2364
2123
  source: resource.source,
2365
2124
  filePath: writePath,
@@ -2400,8 +2159,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2400
2159
  target_id: item.targetId,
2401
2160
  path: item.path,
2402
2161
  }));
2403
- await this.protocolV2StartFirmwareUpdate({ targets });
2404
- await this.waitForProtocolV2FirmwareUpdateComplete(targets);
2162
+ try {
2163
+ await this.protocolV2StartFirmwareUpdate({ targets });
2164
+ await wait(PROTOCOL_V2_INSTALL_STATUS_INITIAL_DELAY);
2165
+ await this.waitForProtocolV2FirmwareUpdateComplete(targets, true);
2166
+ } finally {
2167
+ this.device?.clearCancelableAction();
2168
+ }
2405
2169
  }
2406
2170
 
2407
2171
  private async protocolV2SourceUpdateProcess({
@@ -2446,23 +2210,33 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2446
2210
  );
2447
2211
  }
2448
2212
  const transferredBytes = processedSize + chunkEnd;
2449
- const elapsedMs = Math.max(Date.now() - transferStartedAt, 0);
2450
- this.postProgressMessage(
2451
- Math.min(Math.ceil((transferredBytes / totalSize) * 100), 99),
2452
- 'transferData',
2453
- {
2213
+ const now = Date.now();
2214
+ const elapsedMs = Math.max(now - transferStartedAt, 0);
2215
+ const progress = Math.min(Math.ceil((transferredBytes / totalSize) * 100), 99);
2216
+ const shouldPostProgress =
2217
+ progress !== this.protocolV2LastTransferProgress ||
2218
+ now - this.protocolV2LastTransferProgressAt >=
2219
+ PROTOCOL_V2_TRANSFER_PROGRESS_HEARTBEAT_MS ||
2220
+ chunkEnd === source.size;
2221
+ if (shouldPostProgress) {
2222
+ this.protocolV2LastTransferProgress = progress;
2223
+ this.protocolV2LastTransferProgressAt = now;
2224
+ this.postProgressMessage(progress, 'transferData', {
2454
2225
  transferredBytes,
2455
2226
  totalBytes: totalSize,
2456
2227
  rateBytesPerSecond:
2457
2228
  elapsedMs > 0 ? Math.round((chunkEnd / elapsedMs) * 1000) : undefined,
2458
2229
  elapsedMs,
2459
- }
2460
- );
2230
+ });
2231
+ }
2461
2232
  return length;
2462
2233
  },
2463
2234
  });
2464
2235
  return processedSize + source.size;
2465
2236
  } catch (error) {
2237
+ if (isBleStaleBondHardwareError(error)) {
2238
+ throw error;
2239
+ }
2466
2240
  lastError = error;
2467
2241
  if (attempt < PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT) {
2468
2242
  await this.recoverProtocolV2FileTransfer();
@@ -2492,19 +2266,41 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2492
2266
  }
2493
2267
  }
2494
2268
 
2269
+ private collectProtocolV2LiveTargetIds(
2270
+ statusTargets: ProtocolV2FirmwareUpdateStatusTarget[],
2271
+ expectedTargetIds: Set<number>,
2272
+ liveTargetIds: Set<number>
2273
+ ) {
2274
+ statusTargets.forEach(target => {
2275
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2276
+ if (
2277
+ targetId !== undefined &&
2278
+ expectedTargetIds.has(targetId) &&
2279
+ isProtocolV2TargetStatusInProgress(target.status)
2280
+ ) {
2281
+ liveTargetIds.add(targetId);
2282
+ }
2283
+ });
2284
+ }
2285
+
2495
2286
  private assertProtocolV2TargetStatus(
2496
2287
  statusTargets: ProtocolV2FirmwareUpdateStatusTarget[],
2497
2288
  expectedTargetIds: Set<number>,
2498
- expectedPaths = new Map<number, string>()
2289
+ expectedPaths?: Map<number, string>,
2290
+ liveTargetIds?: Set<number>
2499
2291
  ) {
2292
+ const resolvedExpectedPaths = expectedPaths ?? new Map<number, string>();
2500
2293
  Log.log(
2501
2294
  `[FirmwareUpdateV4] DeviceFirmwareUpdateStatus records=${JSON.stringify(statusTargets)}`
2502
2295
  );
2503
- const failedTarget = statusTargets.find(
2504
- target =>
2505
- expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1) &&
2506
- isProtocolV2TargetStatusFailed(target.status)
2507
- );
2296
+ const failedTarget = statusTargets.find(target => {
2297
+ const targetId = normalizeProtocolV2TargetId(target.target_id) ?? -1;
2298
+ return (
2299
+ expectedTargetIds.has(targetId) &&
2300
+ isProtocolV2TargetStatusFailed(target.status) &&
2301
+ (!liveTargetIds || liveTargetIds.has(targetId))
2302
+ );
2303
+ });
2508
2304
  if (failedTarget) {
2509
2305
  const failedTargetDetails = JSON.stringify({
2510
2306
  targetId: failedTarget.target_id,
@@ -2515,13 +2311,33 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2515
2311
  Log.error(`[FirmwareUpdateV4] firmware install failed target=${failedTargetDetails}`);
2516
2312
  throw ERRORS.TypedError(
2517
2313
  HardwareErrorCode.FirmwareError,
2518
- `Protocol V2 firmware target failed: target=${failedTarget.target_id} status=${
2519
- failedTarget.status ?? 'unknown'
2520
- } payloadVersion=${failedTarget.payload_version ?? 'unknown'} path=${
2521
- failedTarget.path ?? 'unknown'
2522
- }`
2314
+ 'Protocol V2 firmware install failed',
2315
+ {
2316
+ firmwareUpdateCode: PROTOCOL_V2_INSTALL_FAILED_CODE,
2317
+ }
2523
2318
  );
2524
2319
  }
2320
+ if (liveTargetIds) {
2321
+ const staleFailedTargets = statusTargets.filter(target => {
2322
+ const targetId = normalizeProtocolV2TargetId(target.target_id) ?? -1;
2323
+ return (
2324
+ expectedTargetIds.has(targetId) &&
2325
+ isProtocolV2TargetStatusFailed(target.status) &&
2326
+ !liveTargetIds.has(targetId)
2327
+ );
2328
+ });
2329
+ if (staleFailedTargets.length > 0) {
2330
+ Log.log(
2331
+ `[FirmwareUpdateV4] ignoring stale failed firmware status until those targets start: ${JSON.stringify(
2332
+ staleFailedTargets.map(target => ({
2333
+ targetId: target.target_id,
2334
+ status: target.status,
2335
+ path: target.path,
2336
+ }))
2337
+ )}`
2338
+ );
2339
+ }
2340
+ }
2525
2341
 
2526
2342
  const matchingTargets = statusTargets.filter(target =>
2527
2343
  expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1)
@@ -2529,14 +2345,21 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2529
2345
  const seenTargetIds = new Set<number>();
2530
2346
  for (const target of matchingTargets) {
2531
2347
  const targetId = normalizeProtocolV2TargetId(target.target_id);
2348
+ const pathConflictsWithCurrentInstall =
2349
+ targetId !== undefined &&
2350
+ Boolean(target.path) &&
2351
+ Boolean(resolvedExpectedPaths.get(targetId)) &&
2352
+ target.path !== resolvedExpectedPaths.get(targetId) &&
2353
+ (!liveTargetIds || liveTargetIds.has(targetId));
2532
2354
  if (
2533
2355
  targetId === undefined ||
2534
2356
  seenTargetIds.has(targetId) ||
2535
- (target.path && expectedPaths.get(targetId) && target.path !== expectedPaths.get(targetId))
2357
+ pathConflictsWithCurrentInstall
2536
2358
  ) {
2359
+ Log.error(`[FirmwareUpdateV4] install status conflicts with target=${target.target_id}`);
2537
2360
  throw ERRORS.TypedError(
2538
- HardwareErrorCode.RuntimeError,
2539
- `Protocol V2 install status conflicts with target ${target.target_id}`,
2361
+ HardwareErrorCode.FirmwareError,
2362
+ 'Protocol V2 firmware install status conflict',
2540
2363
  {
2541
2364
  firmwareUpdateCode: PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE,
2542
2365
  }
@@ -2544,14 +2367,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2544
2367
  }
2545
2368
  seenTargetIds.add(targetId);
2546
2369
  }
2547
- const completedTargets = matchingTargets.filter(target =>
2548
- isProtocolV2TargetStatusFinished(target.status)
2549
- );
2370
+ const completedTargets = matchingTargets.filter(target => {
2371
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2372
+ return (
2373
+ isProtocolV2TargetStatusFinished(target.status) &&
2374
+ (!liveTargetIds || (targetId !== undefined && liveTargetIds.has(targetId)))
2375
+ );
2376
+ });
2550
2377
  const completedTargetIds = new Set<number>();
2551
2378
  completedTargets.forEach(target => {
2552
2379
  const targetId = normalizeProtocolV2TargetId(target.target_id);
2553
2380
  if (targetId !== undefined) {
2554
2381
  completedTargetIds.add(targetId);
2382
+ this.protocolV2CompletedTargetIds.add(targetId);
2555
2383
  }
2556
2384
  });
2557
2385
  if (completedTargetIds.size === expectedTargetIds.size && expectedTargetIds.size > 0) {
@@ -2582,16 +2410,54 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2582
2410
  }
2583
2411
 
2584
2412
  if (expectedTargetIds.size > 0 && matchingTargets.length > 0) {
2585
- const hasInProgressTarget = matchingTargets.some(target =>
2586
- isProtocolV2TargetStatusInProgress(target.status)
2587
- );
2413
+ let detailedProgress = completedTargetIds.size * 100;
2414
+ let installProgressMetadata:
2415
+ | {
2416
+ installTargetId: number;
2417
+ installPhase: 'prepare' | 'install' | 'verify';
2418
+ installPhaseProgress: number;
2419
+ }
2420
+ | undefined;
2421
+ let hasInProgressTarget = false;
2422
+ matchingTargets.forEach(target => {
2423
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2424
+ if (
2425
+ targetId === undefined ||
2426
+ completedTargetIds.has(targetId) ||
2427
+ !isProtocolV2TargetStatusInProgress(target.status) ||
2428
+ (liveTargetIds && !liveTargetIds.has(targetId))
2429
+ ) {
2430
+ return;
2431
+ }
2432
+ hasInProgressTarget = true;
2433
+ detailedProgress += normalizeProtocolV2ProgressPercent(target.progress_percent) ?? 0;
2434
+ const installPhase = target.phase_info
2435
+ ? PROTOCOL_V2_INSTALL_PHASE_BY_DECODED_VALUE.get(target.phase_info.phase)
2436
+ : undefined;
2437
+ const installPhaseProgress = normalizeProtocolV2ProgressPercent(
2438
+ target.phase_info?.progress_percent
2439
+ );
2440
+ if (installPhase && installPhaseProgress !== undefined && !installProgressMetadata) {
2441
+ installProgressMetadata = {
2442
+ installTargetId: targetId,
2443
+ installPhase,
2444
+ installPhaseProgress,
2445
+ };
2446
+ }
2447
+ });
2588
2448
  const completedProgress = Math.floor(
2589
2449
  (completedTargetIds.size / expectedTargetIds.size) * 100
2590
2450
  );
2591
- // The protocol exposes no per-target percentage, so report coarse progress by
2592
- // completed targets and use 1% once work starts to keep the UI responsive.
2593
- const progress = Math.min(99, Math.max(completedProgress, hasInProgressTarget ? 1 : 0));
2594
- this.postProgressMessage(progress, 'installingFirmware');
2451
+ const reportedProgress = Math.floor(detailedProgress / expectedTargetIds.size);
2452
+ const progress = Math.min(
2453
+ 99,
2454
+ Math.max(completedProgress, reportedProgress, hasInProgressTarget ? 1 : 0)
2455
+ );
2456
+ if (installProgressMetadata) {
2457
+ this.postProgressMessage(progress, 'installingFirmware', installProgressMetadata);
2458
+ } else {
2459
+ this.postProgressMessage(progress, 'installingFirmware');
2460
+ }
2595
2461
  }
2596
2462
 
2597
2463
  return false;
@@ -2611,31 +2477,107 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2611
2477
  return Array.from(expectedTargetIds).filter(targetId => !reportedTargetIds.has(targetId));
2612
2478
  }
2613
2479
 
2480
+ private getProtocolV2ObservableTargetVersions(features: Features) {
2481
+ const versions = new Map<number, string>();
2482
+ versions.set(
2483
+ ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER,
2484
+ getDeviceBootloaderVersion(features).join('.')
2485
+ );
2486
+ const applicationVersion = getDeviceFirmwareVersion(features).join('.');
2487
+ versions.set(ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1, applicationVersion);
2488
+ versions.set(ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2, applicationVersion);
2489
+ versions.set(
2490
+ ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_COPROCESSOR,
2491
+ getDeviceBLEFirmwareVersion(features).join('.')
2492
+ );
2493
+ const secureElementVersions: Array<[number, string | null | undefined]> = [
2494
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE01, features.se01Version],
2495
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE02, features.se02Version],
2496
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE03, features.se03Version],
2497
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE04, features.se04Version],
2498
+ ];
2499
+ secureElementVersions.forEach(([targetId, version]) => {
2500
+ if (version) versions.set(targetId, version);
2501
+ });
2502
+ return versions;
2503
+ }
2504
+
2505
+ private hasProtocolV2InstallVersionChanged(expectedTargetIds: Set<number>) {
2506
+ if (!this.protocolV2LastRuntimeProbeFeatures) return false;
2507
+ const currentVersions = this.getProtocolV2ObservableTargetVersions(
2508
+ this.protocolV2LastRuntimeProbeFeatures
2509
+ );
2510
+ return Array.from(expectedTargetIds).some(targetId => {
2511
+ const previousVersion = this.protocolV2InstallBaselineVersions.get(targetId);
2512
+ const currentVersion = currentVersions.get(targetId);
2513
+ return (
2514
+ previousVersion !== undefined &&
2515
+ currentVersion !== undefined &&
2516
+ previousVersion !== currentVersion
2517
+ );
2518
+ });
2519
+ }
2520
+
2521
+ private recordProtocolV2AuthoritativeInstallCompletion(expectedTargetIds: Set<number>) {
2522
+ expectedTargetIds.forEach(targetId => this.protocolV2CompletedTargetIds.add(targetId));
2523
+ }
2524
+
2614
2525
  private async waitForProtocolV2FirmwareUpdateComplete(
2615
- targets: Array<{ target_id: number; path: string }>
2526
+ targets: Array<{ target_id: number; path: string }>,
2527
+ requireCurrentInstallStatus = false
2616
2528
  ) {
2617
- this.protocolV2FinalStatusVerified = false;
2618
2529
  const expectedTargetIds = new Set(targets.map(target => target.target_id));
2619
2530
  const expectedPaths = new Map(targets.map(target => [target.target_id, target.path]));
2531
+ const isBleInstall = this.isBleReconnect();
2620
2532
  const startTime = Date.now();
2621
2533
  let lastError: unknown;
2622
- let shouldReconnect = true;
2534
+ // The loader may release either USB or BLE as installation starts. Recovery reconnects
2535
+ // directly to status polling without generic Ping or DeviceInfo probes.
2536
+ let shouldReconnect = this.protocolV2InstallNeedsReconnect;
2537
+ this.protocolV2InstallNeedsReconnect = false;
2623
2538
  let deviceInfo: ProtocolV2DeviceInfo | undefined;
2624
- let missingTargetStatusSince: number | undefined;
2625
- let missingTargetStatusKey: string | undefined;
2626
- const resetMissingTargetStatusGrace = () => {
2627
- missingTargetStatusSince = undefined;
2628
- missingTargetStatusKey = undefined;
2539
+ let bleInstallLinkReady = false;
2540
+ let installStatusDisconnectObserved = isBleInstall && this.protocolV2InstallDisconnectObserved;
2541
+ this.protocolV2InstallDisconnectObserved = false;
2542
+ let installReconnectObserved = false;
2543
+ let finishedStatusSnapshotKey: string | undefined;
2544
+ let finishedStatusSnapshotPolls = 0;
2545
+ let fastFinishedInstallProgressReported = false;
2546
+ let installEvidenceObserved = this.protocolV2InstallTerminalSuccessObserved;
2547
+ let currentInstallStatusObserved = false;
2548
+ const liveTargetIds = new Set<number>();
2549
+ const recordBleInstallDisconnect = (error: unknown) => {
2550
+ if (
2551
+ !isBleInstall ||
2552
+ !isProtocolV2DeviceDisconnectedError(error) ||
2553
+ isProtocolV2InstallResponseTimeout(error)
2554
+ ) {
2555
+ return;
2556
+ }
2557
+ installStatusDisconnectObserved = true;
2558
+ installReconnectObserved = false;
2559
+ finishedStatusSnapshotKey = undefined;
2560
+ finishedStatusSnapshotPolls = 0;
2629
2561
  };
2630
2562
 
2631
2563
  while (Date.now() - startTime < PROTOCOL_V2_INSTALL_TIMEOUT) {
2564
+ // A transport release caused by an explicit workflow cancellation must not
2565
+ // be mistaken for the expected device reboot during installation.
2566
+ this.throwIfAborted();
2632
2567
  try {
2633
2568
  if (shouldReconnect) {
2634
- await this.reconnectProtocolV2Device();
2635
- deviceInfo = await this.verifyProtocolV2ReconnectIdentity();
2569
+ const isBleInstallReconnect = this.isBleReconnect();
2570
+ await this.reconnectProtocolV2Device({ skipProtocolProbe: true });
2571
+ bleInstallLinkReady = isBleInstallReconnect;
2572
+ deviceInfo = isBleInstallReconnect
2573
+ ? undefined
2574
+ : await this.verifyProtocolV2ReconnectIdentity();
2575
+ if (isBleInstallReconnect && installStatusDisconnectObserved) {
2576
+ installReconnectObserved = true;
2577
+ }
2636
2578
  shouldReconnect = false;
2637
2579
  }
2638
- const currentDeviceInfo = deviceInfo;
2580
+ let currentDeviceInfo = deviceInfo;
2639
2581
  try {
2640
2582
  const statusResponse = await this.device.getCommands().typedCall(
2641
2583
  'DeviceFirmwareUpdateStatusGet',
@@ -2643,91 +2585,235 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2643
2585
  {
2644
2586
  fields: {
2645
2587
  status: true,
2588
+ progress_percent: true,
2589
+ phase_info: true,
2646
2590
  payload_version: true,
2647
2591
  path: true,
2648
2592
  },
2649
2593
  },
2650
2594
  { timeoutMs: PROTOCOL_V2_FIRMWARE_STATUS_RESPONSE_TIMEOUT }
2651
2595
  );
2596
+ if (this.protocolV2InstallTerminalSuccessObserved) {
2597
+ installEvidenceObserved = true;
2598
+ }
2652
2599
  const statusTargets = (statusResponse.message.records ??
2653
2600
  []) as ProtocolV2FirmwareUpdateStatusTarget[];
2654
- if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds, expectedPaths)) {
2655
- this.protocolV2FinalStatusVerified = true;
2601
+ this.collectProtocolV2LiveTargetIds(statusTargets, expectedTargetIds, liveTargetIds);
2602
+ if (liveTargetIds.size > 0) {
2603
+ currentInstallStatusObserved = true;
2604
+ }
2605
+ const hasMatchingTargetStatus = statusTargets.some(target => {
2606
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2607
+ return targetId !== undefined && expectedTargetIds.has(targetId);
2608
+ });
2609
+ if (
2610
+ hasMatchingTargetStatus &&
2611
+ (!requireCurrentInstallStatus || currentInstallStatusObserved)
2612
+ ) {
2613
+ installEvidenceObserved = true;
2614
+ }
2615
+ const matchingStatusTargets = statusTargets.filter(target => {
2616
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2617
+ return targetId !== undefined && expectedTargetIds.has(targetId);
2618
+ });
2619
+ const matchingFinishedTargetIds = new Set<number>();
2620
+ const matchingFinishedStatus = matchingStatusTargets
2621
+ .map(target => {
2622
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2623
+ const expectedPath = targetId === undefined ? undefined : expectedPaths.get(targetId);
2624
+ const expectedVersion =
2625
+ targetId === undefined
2626
+ ? undefined
2627
+ : this.getExpectedProtocolV2TargetVersion(targetId);
2628
+ const payloadVersion = toProtocolV2FiniteNumber(target.payload_version);
2629
+ if (
2630
+ targetId === undefined ||
2631
+ matchingFinishedTargetIds.has(targetId) ||
2632
+ !isProtocolV2TargetStatusFinished(target.status) ||
2633
+ (Boolean(target.path) && Boolean(expectedPath) && target.path !== expectedPath) ||
2634
+ (expectedVersion !== undefined &&
2635
+ payloadVersion !== undefined &&
2636
+ payloadVersion !== expectedVersion)
2637
+ ) {
2638
+ return undefined;
2639
+ }
2640
+ matchingFinishedTargetIds.add(targetId);
2641
+ return [targetId, target.path ?? null, payloadVersion ?? null] as const;
2642
+ })
2643
+ .filter((target): target is readonly [number, string | null, number | null] => !!target)
2644
+ .sort(([leftTargetId], [rightTargetId]) => leftTargetId - rightTargetId);
2645
+ const hasCompleteMatchingFinishedStatus =
2646
+ matchingFinishedStatus.length === expectedTargetIds.size &&
2647
+ matchingFinishedTargetIds.size === expectedTargetIds.size &&
2648
+ expectedTargetIds.size > 0;
2649
+ if (
2650
+ requireCurrentInstallStatus &&
2651
+ installReconnectObserved &&
2652
+ hasCompleteMatchingFinishedStatus
2653
+ ) {
2654
+ const nextSnapshotKey = JSON.stringify(matchingFinishedStatus);
2655
+ if (nextSnapshotKey === finishedStatusSnapshotKey) {
2656
+ finishedStatusSnapshotPolls += 1;
2657
+ } else {
2658
+ finishedStatusSnapshotKey = nextSnapshotKey;
2659
+ finishedStatusSnapshotPolls = 1;
2660
+ }
2661
+ } else {
2662
+ finishedStatusSnapshotKey = undefined;
2663
+ finishedStatusSnapshotPolls = 0;
2664
+ }
2665
+ if (
2666
+ requireCurrentInstallStatus &&
2667
+ installReconnectObserved &&
2668
+ hasCompleteMatchingFinishedStatus &&
2669
+ !currentInstallStatusObserved &&
2670
+ !this.protocolV2InstallTerminalSuccessObserved &&
2671
+ !fastFinishedInstallProgressReported
2672
+ ) {
2673
+ // A fast BLE install may finish while disconnected, so IN_PROGRESS can be hidden.
2674
+ // A status response timeout alone is not evidence that the install was accepted.
2675
+ this.postProgressMessage(1, 'installingFirmware');
2676
+ fastFinishedInstallProgressReported = true;
2677
+ }
2678
+ const finishedAfterDisconnectObserved =
2679
+ finishedStatusSnapshotPolls >= PROTOCOL_V2_INSTALL_FINISHED_AFTER_DISCONNECT_POLLS;
2680
+ const hasCurrentInstallEvidence =
2681
+ currentInstallStatusObserved ||
2682
+ this.protocolV2InstallTerminalSuccessObserved ||
2683
+ finishedAfterDisconnectObserved;
2684
+ if (hasCurrentInstallEvidence) {
2685
+ installEvidenceObserved = true;
2686
+ }
2687
+ const shouldVerifyTargetCompletion =
2688
+ !requireCurrentInstallStatus || hasCurrentInstallEvidence;
2689
+ if (
2690
+ shouldVerifyTargetCompletion &&
2691
+ this.assertProtocolV2TargetStatus(
2692
+ statusTargets,
2693
+ expectedTargetIds,
2694
+ expectedPaths,
2695
+ requireCurrentInstallStatus &&
2696
+ !this.protocolV2InstallTerminalSuccessObserved &&
2697
+ !finishedAfterDisconnectObserved
2698
+ ? liveTargetIds
2699
+ : undefined
2700
+ )
2701
+ ) {
2656
2702
  return;
2657
2703
  }
2658
2704
 
2705
+ if (!hasCurrentInstallEvidence) {
2706
+ // A successful status poll clears DeviceCommands' cancel action. Keep cancellation
2707
+ // available while UpdateRequest is still waiting for confirmation on the device.
2708
+ this.device.setCancelableAction(() => this.device.getCommands().cancelDevice());
2709
+ }
2710
+
2659
2711
  if (
2660
- statusTargets.length === 0 &&
2661
- currentDeviceInfo &&
2662
- (await this.probeProtocolV2NormalMode(currentDeviceInfo))
2712
+ requireCurrentInstallStatus &&
2713
+ !hasCurrentInstallEvidence &&
2714
+ matchingStatusTargets.length > 0
2663
2715
  ) {
2664
- Log.log(
2665
- '[FirmwareUpdateV4] empty firmware status after confirmed App reboot; update complete'
2716
+ lastError = new Error(
2717
+ 'Protocol V2 firmware status is stale; waiting for the current install to start'
2666
2718
  );
2667
- this.postProgressMessage(100, 'installingFirmware');
2668
- return;
2669
2719
  }
2670
2720
 
2671
- const missingTargetIds = this.getProtocolV2MissingTargetIds(
2672
- statusTargets,
2673
- expectedTargetIds
2674
- );
2675
- if (missingTargetIds.length > 0) {
2676
- const now = Date.now();
2677
- const missingKey = missingTargetIds.join(',');
2678
- if (missingTargetStatusSince === undefined || missingTargetStatusKey !== missingKey) {
2679
- missingTargetStatusSince = now;
2680
- missingTargetStatusKey = missingKey;
2681
- } else if (
2682
- now - missingTargetStatusSince >=
2683
- PROTOCOL_V2_MISSING_TARGET_STATUS_GRACE_TIMEOUT
2721
+ if (
2722
+ statusTargets.length === 0 &&
2723
+ !currentDeviceInfo &&
2724
+ bleInstallLinkReady &&
2725
+ installEvidenceObserved
2726
+ ) {
2727
+ // BLE install reconnect skips generic probes because loaders may not answer Ping.
2728
+ // Restore the verified identity only after current-install evidence disappears.
2729
+ currentDeviceInfo = await this.verifyProtocolV2ReconnectIdentity();
2730
+ deviceInfo = currentDeviceInfo;
2731
+ }
2732
+
2733
+ if (statusTargets.length === 0 && currentDeviceInfo) {
2734
+ const isNormalMode = await this.probeProtocolV2NormalMode(currentDeviceInfo);
2735
+ if (
2736
+ isNormalMode &&
2737
+ (installEvidenceObserved ||
2738
+ this.hasProtocolV2InstallVersionChanged(expectedTargetIds))
2684
2739
  ) {
2685
- const reportedTargetIds = statusTargets
2686
- .map(target => normalizeProtocolV2TargetId(target.target_id))
2687
- .filter((targetId): targetId is number => targetId !== undefined);
2688
- throw ERRORS.TypedError(
2689
- HardwareErrorCode.FirmwareError,
2690
- `Protocol V2 firmware status is missing requested records: targetIds=${missingKey} reportedTargetIds=${reportedTargetIds.join(
2691
- ','
2692
- )}`
2740
+ Log.log(
2741
+ '[FirmwareUpdateV4] empty firmware status after confirmed App reboot; update complete'
2693
2742
  );
2743
+ this.recordProtocolV2AuthoritativeInstallCompletion(expectedTargetIds);
2744
+ this.postProgressMessage(100, 'installingFirmware');
2745
+ return;
2694
2746
  }
2747
+ }
2748
+
2749
+ if (statusTargets.length === 0) {
2695
2750
  lastError = new Error(
2696
- `Protocol V2 firmware status is temporarily missing targetIds=${missingKey}`
2751
+ 'Protocol V2 firmware update is waiting for user confirmation or target status'
2697
2752
  );
2698
2753
  } else {
2699
- resetMissingTargetStatusGrace();
2700
- lastError = new Error('Protocol V2 firmware targets are still installing');
2754
+ const missingTargetIds = this.getProtocolV2MissingTargetIds(
2755
+ statusTargets,
2756
+ expectedTargetIds
2757
+ );
2758
+ if (missingTargetIds.length > 0) {
2759
+ const missingKey = missingTargetIds.join(',');
2760
+ lastError = new Error(
2761
+ `Protocol V2 firmware status is temporarily missing targetIds=${missingKey}`
2762
+ );
2763
+ } else {
2764
+ lastError = new Error('Protocol V2 firmware targets are still installing');
2765
+ }
2701
2766
  }
2702
2767
  } catch (error) {
2703
2768
  if (isProtocolV2TerminalInstallStatusError(error)) {
2704
2769
  throw error;
2705
2770
  }
2706
- // Only consecutive full status dumps can prove that a record is absent.
2707
- // A reboot or transport interruption starts a fresh grace window.
2708
- resetMissingTargetStatusGrace();
2709
2771
  // App firmware does not register DeviceFirmwareUpdateStatusGet. Treat the
2710
2772
  // missing endpoint as completion only after the runtime probe confirms App mode.
2711
2773
  if (isProtocolV2FirmwareStatusEndpointUnavailable(error)) {
2712
2774
  if (!currentDeviceInfo) {
2775
+ if (!bleInstallLinkReady) {
2776
+ throw ERRORS.TypedError(
2777
+ HardwareErrorCode.RuntimeError,
2778
+ 'Protocol V2 device identity is unavailable during install polling'
2779
+ );
2780
+ }
2781
+ deviceInfo = await this.verifyProtocolV2ReconnectIdentity();
2782
+ }
2783
+ const reconnectDeviceInfo = currentDeviceInfo ?? deviceInfo;
2784
+ if (!reconnectDeviceInfo) {
2713
2785
  throw ERRORS.TypedError(
2714
2786
  HardwareErrorCode.RuntimeError,
2715
2787
  'Protocol V2 device identity is unavailable during install polling'
2716
2788
  );
2717
2789
  }
2718
- if (await this.probeProtocolV2NormalMode(currentDeviceInfo)) {
2790
+ const isNormalMode = await this.probeProtocolV2NormalMode(reconnectDeviceInfo);
2791
+ if (
2792
+ isNormalMode &&
2793
+ (installEvidenceObserved ||
2794
+ this.hasProtocolV2InstallVersionChanged(expectedTargetIds))
2795
+ ) {
2719
2796
  Log.log(
2720
2797
  '[FirmwareUpdateV4] firmware status endpoint unavailable after confirmed App reboot'
2721
2798
  );
2799
+ this.recordProtocolV2AuthoritativeInstallCompletion(expectedTargetIds);
2722
2800
  this.postProgressMessage(100, 'installingFirmware');
2723
2801
  return;
2724
2802
  }
2725
- lastError = new Error(
2726
- 'Protocol V2 firmware status endpoint is unavailable while the device remains in loader mode'
2727
- );
2803
+ if (isNormalMode) {
2804
+ lastError = new Error(
2805
+ 'Protocol V2 device is in normal mode but installation is not yet confirmed'
2806
+ );
2807
+ } else {
2808
+ lastError = new Error(
2809
+ 'Protocol V2 firmware status endpoint is unavailable while the device remains in loader mode'
2810
+ );
2811
+ }
2728
2812
  } else {
2813
+ recordBleInstallDisconnect(error);
2729
2814
  shouldReconnect = true;
2730
2815
  deviceInfo = undefined;
2816
+ bleInstallLinkReady = false;
2731
2817
  lastError = error;
2732
2818
  Log.log(
2733
2819
  '[FirmwareUpdateV4] DeviceFirmwareUpdateStatusGet unavailable during install: ',
@@ -2749,20 +2835,27 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2749
2835
  ) {
2750
2836
  throw error;
2751
2837
  }
2838
+ recordBleInstallDisconnect(error);
2752
2839
  shouldReconnect = true;
2753
2840
  deviceInfo = undefined;
2754
- resetMissingTargetStatusGrace();
2841
+ bleInstallLinkReady = false;
2755
2842
  Log.log('Protocol V2 firmware install device readiness probe failed: ', error);
2756
2843
  }
2757
2844
  await wait(1000);
2758
2845
  }
2759
2846
 
2760
- throw ERRORS.TypedError(
2761
- HardwareErrorCode.RuntimeError,
2762
- `Protocol V2 firmware update status not complete within ${
2847
+ Log.error(
2848
+ `[FirmwareUpdateV4] install timed out after ${
2763
2849
  PROTOCOL_V2_INSTALL_TIMEOUT / 1000
2764
2850
  }s: ${this.normalizeErrorMessage(lastError)}`
2765
2851
  );
2852
+ throw ERRORS.TypedError(
2853
+ HardwareErrorCode.FirmwareError,
2854
+ 'Protocol V2 firmware install timed out',
2855
+ {
2856
+ firmwareUpdateCode: PROTOCOL_V2_INSTALL_TIMEOUT_CODE,
2857
+ }
2858
+ );
2766
2859
  }
2767
2860
 
2768
2861
  private async exitProtocolV2BootloaderToNormal() {
@@ -2774,6 +2867,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2774
2867
  return;
2775
2868
  }
2776
2869
  } catch (error) {
2870
+ if (isBleStaleBondHardwareError(error)) {
2871
+ throw error;
2872
+ }
2777
2873
  Log.log('[FirmwareUpdateV4] unable to confirm App mode before Normal reboot: ', error);
2778
2874
  }
2779
2875
  await this.protocolV2Reboot(DeviceRebootType.Normal);
@@ -2785,7 +2881,14 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2785
2881
  deviceInfo,
2786
2882
  PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT
2787
2883
  );
2788
- return features.mode === 'normal' && !features.bootloaderMode;
2884
+ this.protocolV2LastRuntimeProbeFeatures = features;
2885
+ return this.isProtocolV2ApplicationMode(features);
2886
+ }
2887
+
2888
+ private isProtocolV2ApplicationMode(features: Features) {
2889
+ return (
2890
+ (features.mode === 'normal' || features.mode === 'notInitialized') && !features.bootloaderMode
2891
+ );
2789
2892
  }
2790
2893
 
2791
2894
  private async waitForProtocolV2FinalFeatures() {
@@ -2829,6 +2932,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2829
2932
  const startTime = Date.now();
2830
2933
  let lastError: unknown;
2831
2934
  let shouldReconnect = true;
2935
+ this.protocolV2LatestFinalDeviceInfo = undefined;
2832
2936
 
2833
2937
  while (Date.now() - startTime < timeout) {
2834
2938
  try {
@@ -2845,9 +2949,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2845
2949
  this.assertProtocolV2DeviceInfoIdentity(deviceInfo);
2846
2950
  const features = await this.device.probeProtocolV2RuntimeState(
2847
2951
  deviceInfo,
2848
- PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT
2952
+ PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
2953
+ { forceRuntimeContextRefresh: true }
2849
2954
  );
2850
- if (features.mode === 'normal' && !features.bootloaderMode) {
2955
+ if (this.isProtocolV2ApplicationMode(features)) {
2956
+ this.protocolV2LatestFinalDeviceInfo = deviceInfo;
2851
2957
  return features;
2852
2958
  }
2853
2959
  lastError = ERRORS.TypedError(
@@ -2855,7 +2961,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2855
2961
  'Protocol V2 device is still in bootloader mode'
2856
2962
  );
2857
2963
  } catch (error) {
2858
- if (this.isProtocolV2ReconnectIdentityError(error)) {
2964
+ if (isBleStaleBondHardwareError(error) || this.isProtocolV2ReconnectIdentityError(error)) {
2859
2965
  throw error;
2860
2966
  }
2861
2967
  shouldReconnect = true;
@@ -2873,19 +2979,53 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2873
2979
  );
2874
2980
  }
2875
2981
 
2876
- private async reconnectProtocolV2Device() {
2982
+ private async reconnectProtocolV2Device(options?: { skipProtocolProbe?: boolean }) {
2877
2983
  if (this.isBleReconnect()) {
2878
- await this.acquireProtocolV2BleDevice();
2984
+ await this.acquireProtocolV2BleDevice(options?.skipProtocolProbe);
2879
2985
  return;
2880
2986
  }
2881
2987
 
2882
2988
  const deviceDiff = await this.device.deviceConnector?.enumerate();
2883
2989
  const devicesDescriptor = deviceDiff?.descriptors ?? [];
2990
+ const env = DataManager.getSettings('env');
2991
+ const isWebUsb = DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env);
2884
2992
 
2885
- if (
2886
- DataManager.isBrowserWebUsb(DataManager.getSettings('env')) &&
2887
- devicesDescriptor.length === 1
2888
- ) {
2993
+ if (isWebUsb && options?.skipProtocolProbe) {
2994
+ const expectedSerialNumber = this.protocolV2ExpectedSerialNumber?.trim();
2995
+ const expectedPath = this.protocolV2ExpectedPath?.trim();
2996
+ const reconnectDescriptor = devicesDescriptor.find(descriptor => {
2997
+ const descriptorPath = descriptor.path?.trim();
2998
+ return (
2999
+ (!!expectedSerialNumber && descriptorPath === expectedSerialNumber) ||
3000
+ (!!expectedPath && descriptorPath === expectedPath)
3001
+ );
3002
+ });
3003
+ if (!reconnectDescriptor?.path) {
3004
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
3005
+ }
3006
+ this.device.updateDescriptor(
3007
+ {
3008
+ ...reconnectDescriptor,
3009
+ protocolType: PROTOCOL_V2_CONNECT_PROTOCOL,
3010
+ },
3011
+ true
3012
+ );
3013
+ const mainId = await this.device.deviceConnector?.acquire(
3014
+ reconnectDescriptor.path,
3015
+ null,
3016
+ undefined,
3017
+ PROTOCOL_V2_CONNECT_PROTOCOL,
3018
+ undefined,
3019
+ undefined,
3020
+ true
3021
+ );
3022
+ this.device.mainId = typeof mainId === 'string' ? mainId : reconnectDescriptor.path;
3023
+ this.device.commands.disposed = false;
3024
+ this.device.getCommands().mainId = this.device.mainId;
3025
+ return;
3026
+ }
3027
+
3028
+ if (isWebUsb && devicesDescriptor.length === 1) {
2889
3029
  const descriptor = devicesDescriptor[0];
2890
3030
  if (!this.protocolV2ExpectedSerialNumber && descriptor.path !== this.protocolV2ExpectedPath) {
2891
3031
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
@@ -3003,13 +3143,24 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
3003
3143
  await wait(2000);
3004
3144
  }
3005
3145
 
3006
- private async acquireProtocolV2BleDevice() {
3007
- await this.device.deviceConnector?.acquire(
3008
- this.device.originalDescriptor.id,
3146
+ private async acquireProtocolV2BleDevice(skipProtocolProbe = false) {
3147
+ const connector = this.device.deviceConnector;
3148
+ const expectedId = this.device.originalDescriptor.id;
3149
+ if (!skipProtocolProbe) {
3150
+ await connector?.acquire(expectedId, null, true, PROTOCOL_V2_CONNECT_PROTOCOL);
3151
+ return;
3152
+ }
3153
+ await connector?.acquire(
3154
+ expectedId,
3009
3155
  null,
3010
3156
  true,
3011
- PROTOCOL_V2_CONNECT_PROTOCOL
3157
+ PROTOCOL_V2_CONNECT_PROTOCOL,
3158
+ undefined,
3159
+ undefined,
3160
+ skipProtocolProbe
3012
3161
  );
3162
+ this.device.commands.disposed = false;
3163
+ this.device.getCommands().mainId = expectedId;
3013
3164
  }
3014
3165
 
3015
3166
  private async protocolV2StartFirmwareUpdate({
@@ -3017,30 +3168,74 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
3017
3168
  }: {
3018
3169
  targets: Array<{ target_id: number; path: string }>;
3019
3170
  }) {
3171
+ this.protocolV2LastRuntimeProbeFeatures = undefined;
3172
+ this.protocolV2InstallNeedsReconnect = false;
3173
+ this.protocolV2InstallDisconnectObserved = false;
3174
+ this.protocolV2InstallTerminalSuccessObserved = false;
3020
3175
  const commands = this.device.getCommands();
3021
- const response: ProtocolV2FirmwareUpdateStartResponse = await commands.typedCall(
3022
- 'DeviceFirmwareUpdateRequest',
3023
- 'Success',
3024
- { targets },
3025
- { timeoutMs: PROTOCOL_V2_START_UPDATE_TIMEOUT }
3176
+ await commands.typedCall('DeviceFirmwareUpdateStage', 'Success', { targets });
3177
+ this.device.setCancelableAction(() => this.device.getCommands().cancelDevice());
3178
+ const interaction = this.device.createProtocolV2UiPhaseMetadata('button', 'start');
3179
+ this.postMessage(
3180
+ createUiMessage(UI_REQUEST.REQUEST_BUTTON, {
3181
+ device: this.device.toMessageObject() as KnownDevice,
3182
+ source: 'method-lifecycle',
3183
+ reason: 'firmware-update',
3184
+ completion: 'operation-completed',
3185
+ deviceOnly: true,
3186
+ operation: this.name,
3187
+ ...(interaction ? { interaction } : {}),
3188
+ })
3026
3189
  );
3027
- // Success acknowledges that the device accepted installation. End confirmation
3028
- // and begin status polling only after receiving this ACK.
3029
- this.postTipMessage(FirmwareUpdateTipMessage.FirmwareUpdating);
3030
- this.postProgressMessage(0, 'installingFirmware');
3031
- return response;
3190
+
3191
+ try {
3192
+ await commands.call(
3193
+ 'DeviceFirmwareUpdateRequest',
3194
+ {},
3195
+ {
3196
+ returnAfterWrite: true,
3197
+ expectedTypes: ['Success'],
3198
+ onResponseAfterWrite: response => {
3199
+ if (response.type !== 'Success') return;
3200
+ this.protocolV2InstallTerminalSuccessObserved = true;
3201
+ this.device.clearCancelableAction();
3202
+ Log.log('[FirmwareUpdateV4] firmware install completed by device response');
3203
+ },
3204
+ }
3205
+ );
3206
+ Log.log('[FirmwareUpdateV4] firmware install request written; continue with status polling');
3207
+ } catch (error) {
3208
+ this.throwIfAborted();
3209
+ if (!isProtocolV2DeviceDisconnectedError(error)) {
3210
+ throw error;
3211
+ }
3212
+ Log.log(
3213
+ '[FirmwareUpdateV4] transport released while writing install request; continue status polling'
3214
+ );
3215
+ this.protocolV2InstallNeedsReconnect = true;
3216
+ this.protocolV2InstallDisconnectObserved = !isProtocolV2InstallResponseTimeout(error);
3217
+ }
3032
3218
  }
3033
3219
 
3034
3220
  private async protocolV2Reboot(rebootType: DeviceRebootType) {
3035
3221
  const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
3036
3222
  try {
3037
- const res = await typedCall('DeviceReboot', 'Success', {
3038
- reboot_type: rebootType,
3039
- });
3223
+ const res = await typedCall(
3224
+ 'DeviceReboot',
3225
+ 'Success',
3226
+ {
3227
+ reboot_type: rebootType,
3228
+ },
3229
+ { timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT }
3230
+ );
3040
3231
  this.device.markProtocolV2Reboot(rebootType);
3041
3232
  return res.message;
3042
3233
  } catch (error) {
3043
- if (isProtocolV2DeviceDisconnectedError(error) || isProtocolV2ReconnectProbeError(error)) {
3234
+ if (
3235
+ isProtocolV2DeviceDisconnectedError(error) ||
3236
+ isProtocolV2ReconnectProbeError(error) ||
3237
+ isProtocolV2ResponseTimeout(error)
3238
+ ) {
3044
3239
  this.device.markProtocolV2Reboot(rebootType);
3045
3240
  return { message: 'Device rebooted successfully' };
3046
3241
  }