@onekeyfe/hd-core 1.2.0-alpha.99 → 1.2.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 +678 -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 +2198 -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 +19 -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 +2135 -1470
  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 +918 -741
  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,
@@ -382,10 +375,12 @@ const isProtocolV2FirmwareStatusEndpointUnavailable = (error: unknown) => {
382
375
  };
383
376
 
384
377
  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);
378
+ isBleStaleBondHardwareError(error) ||
379
+ (error instanceof HardwareError &&
380
+ (error.errorCode === HardwareErrorCode.ActionCancelled ||
381
+ error.errorCode === HardwareErrorCode.FirmwareError ||
382
+ error.errorCode === HardwareErrorCode.FirmwareVerificationFailed ||
383
+ error.params?.firmwareUpdateCode === PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE));
389
384
 
390
385
  const isProtocolV2TargetStatusFinished = (status: ProtocolV2FirmwareUpdateStatusTarget['status']) =>
391
386
  normalizeProtocolV2TargetStatus(status) === PROTOCOL_V2_TARGET_STATUS_FINISHED;
@@ -422,6 +417,18 @@ const normalizeProtocolV2TargetStatus = (
422
417
  return undefined;
423
418
  };
424
419
 
420
+ const PROTOCOL_V2_INSTALL_PHASE_BY_DECODED_VALUE = new Map<
421
+ number | string,
422
+ 'prepare' | 'install' | 'verify'
423
+ >([
424
+ [0, 'prepare'],
425
+ [1, 'install'],
426
+ [2, 'verify'],
427
+ ['FW_MGMT_UPDATER_PHASE_PREPARE', 'prepare'],
428
+ ['FW_MGMT_UPDATER_PHASE_INSTALL', 'install'],
429
+ ['FW_MGMT_UPDATER_PHASE_VERIFY', 'verify'],
430
+ ]);
431
+
425
432
  const normalizeProtocolV2Hex = (value?: string) => value?.replace(/^0x/i, '').toLowerCase();
426
433
 
427
434
  const versionArrayToNumber = (version?: IVersionArray) => {
@@ -479,40 +486,9 @@ const toProtocolV2FiniteNumber = (value: unknown) => {
479
486
  return undefined;
480
487
  };
481
488
 
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
- };
489
+ const normalizeProtocolV2ProgressPercent = (value: unknown) => {
490
+ const progress = toProtocolV2FiniteNumber(value);
491
+ return progress === undefined ? undefined : Math.min(100, Math.max(0, progress));
516
492
  };
517
493
 
518
494
  export const isProtocolV2FirmwareFingerprintValid = (
@@ -557,7 +533,7 @@ export const assertProtocolV2ReconnectIdentity = (
557
533
  *
558
534
  * It intentionally does not fall back to FirmwareUpdateV3/V1 behavior:
559
535
  * - upload uses FilesystemFileWrite
560
- * - install uses DeviceFirmwareUpdateRequest
536
+ * - install uses DeviceFirmwareUpdateStage followed by an empty DeviceFirmwareUpdateRequest
561
537
  * - completion waits for target status to finish, reboots to normal, then polls DeviceInfo
562
538
  */
563
539
  export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareUpdateV4Params> {
@@ -579,19 +555,54 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
579
555
 
580
556
  private protocolV2CompletedTargetVersions = new Map<number, number>();
581
557
 
558
+ private protocolV2CompletedTargetIds = new Set<number>();
559
+
582
560
  private protocolV2LatestFinalFeatures?: Features;
583
561
 
584
- private protocolV2FinalStatusVerified = false;
562
+ private protocolV2LatestFinalDeviceInfo?: ProtocolV2DeviceInfo;
563
+
564
+ private protocolV2InstallBaselineVersions = new Map<number, string>();
565
+
566
+ private protocolV2InstallNeedsReconnect = false;
567
+
568
+ private protocolV2InstallTerminalSuccessObserved = false;
569
+
570
+ private protocolV2LastRuntimeProbeFeatures?: Features;
571
+
572
+ private protocolV2LastTransferProgress?: number;
573
+
574
+ private protocolV2LastTransferProgressAt = 0;
585
575
 
586
576
  init() {
587
577
  this.allowDeviceMode = [UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE];
588
578
  this.requireDeviceMode = [];
589
579
  this.unlockPolicy = 'unlock-before-run';
580
+ // Protocol V2 device-management actions are not wallet-scoped, so either
581
+ // the main PIN or an Attach PIN may authorize them.
582
+ this.protocolV2PreUnlockPinType = DeviceSessionPinType.Any;
590
583
  this.useDevicePassphraseState = false;
591
584
  this.skipForceUpdateCheck = true;
592
585
 
593
586
  const { payload } = this;
594
587
 
588
+ const removedResourceInputs = ['resourceFiles', 'resourceBundleArtifacts'].filter(input =>
589
+ Object.prototype.hasOwnProperty.call(payload, input)
590
+ );
591
+ if (removedResourceInputs.length > 0) {
592
+ throw ERRORS.TypedError(
593
+ HardwareErrorCode.CallMethodInvalidParameter,
594
+ `Protocol V2 ${removedResourceInputs.join(
595
+ ' and '
596
+ )} inputs are no longer supported; use resourceArchiveBinary for local updates or preparedPlan with hostBindingGeneration for remote updates`
597
+ );
598
+ }
599
+ if (payload.hostBindingGeneration !== undefined && !payload.preparedPlan) {
600
+ throw ERRORS.TypedError(
601
+ HardwareErrorCode.CallMethodInvalidParameter,
602
+ 'Protocol V2 hostBindingGeneration requires preparedPlan for remote updates; use direct binaries or resourceArchiveBinary for local updates'
603
+ );
604
+ }
605
+
595
606
  this.protocolV2HasExplicitTargetSelection = payload.targetsToUpdate !== undefined;
596
607
 
597
608
  if (typeof payload.retryCount !== 'number') {
@@ -661,10 +672,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
661
672
  payload.hostBindingGeneration,
662
673
  preparedPlan.preparedPlanDigest
663
674
  ).artifactReader;
664
- } else if (payload.hostBindingGeneration !== undefined) {
665
- artifactReader = resolveFirmwareUpdateHostBinding(
666
- payload.hostBindingGeneration
667
- ).artifactReader;
668
675
  }
669
676
  if (preparedPlan) {
670
677
  assertFirmwareUpdatePreparedPlanBinding({
@@ -787,6 +794,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
787
794
  private async runProtocolV2() {
788
795
  await this.captureProtocolV2PhysicalIdentity();
789
796
  const deviceFeatures = await this.getProtocolV2DeviceFeatures();
797
+ this.protocolV2InstallBaselineVersions =
798
+ this.getProtocolV2ObservableTargetVersions(deviceFeatures);
799
+ this.protocolV2LastRuntimeProbeFeatures = undefined;
790
800
  const currentDeviceType = this.device.getCurrentDeviceType();
791
801
  const capabilityDeviceType =
792
802
  currentDeviceType === EDeviceType.Pro2 || currentDeviceType === EDeviceType.Neo
@@ -800,21 +810,14 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
800
810
  const deviceFirmwareType = getFirmwareType(deviceFeatures);
801
811
  const firmwareType = this.params.firmwareType ?? deviceFirmwareType;
802
812
  this.validateExpectedTargetVersions();
813
+ const wantsResources = !!this.params.targetsToUpdate?.includes('resource');
803
814
 
804
815
  if (
805
816
  !this.params.preparedPlan &&
806
817
  this.params.resourceArchiveBinary &&
807
818
  this.params.targetsToUpdate?.includes('resource')
808
819
  ) {
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
- }
820
+ return this.runProtocolV2DirectArtifacts({});
818
821
  }
819
822
 
820
823
  const hasPreparedComponentArtifacts = Object.values(this.params.componentArtifacts ?? {}).some(
@@ -823,16 +826,16 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
823
826
  if (this.params.preparedPlan || hasPreparedComponentArtifacts) {
824
827
  return this.runProtocolV2PreparedArtifacts(deviceFeatures, firmwareType);
825
828
  }
826
- const wantsResources = !!this.params.targetsToUpdate?.includes('resource');
827
829
 
828
830
  let fwBinaryMap: ProtocolV2TargetBinary[] = [];
829
831
  let bootloaderBinary: ArrayBuffer | null = null;
830
832
  let installItems: ProtocolV2InstallItem[] | undefined;
833
+ let explicitInstallItems: ProtocolV2InstallItem[] | undefined;
831
834
  try {
832
835
  this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
833
836
  fwBinaryMap = this.collectExplicitTargetBinaries();
834
837
  bootloaderBinary = this.prepareBootloaderBinary();
835
- const explicitInstallItems = this.buildProtocolV2InstallItems({
838
+ explicitInstallItems = this.buildProtocolV2InstallItems({
836
839
  bootloaderBinary,
837
840
  fwBinaryMap,
838
841
  });
@@ -840,17 +843,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
840
843
  const needsRemoteFirmware = this.params.targetsToUpdate?.length
841
844
  ? missingFirmwareTargets.length > 0
842
845
  : 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
- }
846
+ const needsSdkManagedArtifacts = needsRemoteFirmware || wantsResources;
852
847
  if (
853
- needsRemoteFirmware &&
848
+ needsSdkManagedArtifacts &&
854
849
  (this.params.artifactReader ||
855
850
  DataManager.getSettings('firmwareManifestMode') === 'external-only')
856
851
  ) {
@@ -862,9 +857,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
862
857
  }
863
858
  );
864
859
  }
865
- if (needsRemoteFirmware) {
860
+ if (needsSdkManagedArtifacts) {
866
861
  // Remote updates must use a freshly fetched config before any reboot or file write.
867
- await DataManager.forceReloadData();
862
+ await DataManager.forceReloadData({
863
+ requireResources: wantsResources,
864
+ resourceDeviceType:
865
+ capabilityDeviceType === EDeviceType.Neo ? EDeviceType.Neo : EDeviceType.Pro2,
866
+ });
868
867
  }
869
868
  if (needsRemoteFirmware) {
870
869
  const remoteBinaries = await this.prepareRemoteProtocolV2Binaries(
@@ -887,6 +886,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
887
886
  targetId: item.targetId,
888
887
  }));
889
888
  }
889
+ if (wantsResources) {
890
+ this.params.resourceArchiveBinary = await this.downloadRemoteProtocolV2ResourceArchive(
891
+ deviceFeatures,
892
+ firmwareType
893
+ );
894
+ }
890
895
  this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
891
896
  } catch (err) {
892
897
  if (
@@ -903,6 +908,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
903
908
  throw normalizeFirmwarePreparationError(err);
904
909
  }
905
910
 
911
+ if (wantsResources && this.params.resourceArchiveBinary) {
912
+ return this.runProtocolV2DirectArtifacts({
913
+ availableInstallItems: installItems ?? explicitInstallItems,
914
+ announceDownload: false,
915
+ });
916
+ }
917
+
906
918
  if (!bootloaderBinary && fwBinaryMap.length === 0 && !installItems?.length) {
907
919
  throw ERRORS.TypedError(
908
920
  HardwareErrorCode.FirmwareUpdateDownloadFailed,
@@ -1053,132 +1065,69 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1053
1065
  return installSources;
1054
1066
  }
1055
1067
 
1056
- private async prepareProtocolV2LocalMemoryHost({
1057
- features,
1058
- firmwareType,
1068
+ private async runProtocolV2DirectArtifacts({
1069
+ availableInstallItems,
1070
+ announceDownload = true,
1059
1071
  }: {
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
- }
1072
+ availableInstallItems?: ProtocolV2InstallItem[];
1073
+ announceDownload?: boolean;
1074
+ }) {
1075
+ try {
1076
+ if (announceDownload) {
1077
+ this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
1078
+ }
1079
+ const installItems = this.filterProtocolV2LocalInstallItems(
1080
+ availableInstallItems ??
1081
+ this.buildProtocolV2InstallItems({
1082
+ bootloaderBinary: this.prepareBootloaderBinary(),
1083
+ fwBinaryMap: this.collectExplicitTargetBinaries(),
1084
+ })
1090
1085
  );
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) {
1086
+ const installSources = await Promise.all(
1087
+ installItems.map(async item => ({
1088
+ fileName: item.fileName,
1089
+ source: await this.openProtocolV2MemorySource(item.binary),
1090
+ targetId: item.targetId,
1091
+ kind: item.kind,
1092
+ }))
1093
+ );
1094
+ const resourceSources = this.params.resourceArchiveBinary
1095
+ ? await this.createProtocolV2ResourceSourcesFromArchive(this.params.resourceArchiveBinary)
1096
+ : [];
1097
+ if (installSources.length === 0 && resourceSources.length === 0) {
1100
1098
  throw ERRORS.TypedError(
1101
- HardwareErrorCode.RuntimeError,
1102
- `Protocol V2 local firmware artifact is invalid: ${item.fileName}`,
1103
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1099
+ HardwareErrorCode.FirmwareUpdateDownloadFailed,
1100
+ 'No firmware to update'
1104
1101
  );
1105
1102
  }
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
- : {}),
1103
+ if (announceDownload) {
1104
+ this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
1105
+ }
1106
+ return await this.executeProtocolV2SourceUpdate({
1107
+ installSources,
1108
+ resourceSources,
1117
1109
  });
1118
- memoryArtifacts.push({ artifactId, binary: item.binary });
1110
+ } finally {
1111
+ await this.closeProtocolV2PreparedSources();
1119
1112
  }
1113
+ }
1120
1114
 
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),
1115
+ private async createProtocolV2ResourceSourcesFromArchive(
1116
+ binary: ArrayBuffer
1117
+ ): Promise<ProtocolV2ResourceBundleSource[]> {
1118
+ const archive = await this.prepareProtocolV2LocalResourceArchive(binary);
1119
+ const sources: ProtocolV2ResourceBundleSource[] = [];
1120
+ for (const resource of archive.resources) {
1121
+ sources.push({
1122
+ name: resource.entryName.split('/').pop() ?? resource.entryName,
1123
+ source: await this.openProtocolV2MemorySource(resource.binary),
1124
+ devicePath: resource.header.devicePath,
1125
+ version: resource.header.version,
1126
+ payloadHash: resource.header.payloadHash,
1127
+ headerHash: resource.header.headerHash,
1168
1128
  });
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
1129
  }
1130
+ return sources;
1182
1131
  }
1183
1132
 
1184
1133
  private async prepareProtocolV2LocalResourceArchive(
@@ -1202,130 +1151,67 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1202
1151
  );
1203
1152
  }
1204
1153
  const zipEntries = Object.values(zip.files);
1154
+ const resourceEntries = zipEntries.filter(
1155
+ entry => !entry.dir && isProtocolV2ResourceArchiveEntryName(entry.name)
1156
+ );
1205
1157
  if (
1206
- zipEntries.some(entry => entry.unsafeOriginalName && entry.unsafeOriginalName !== entry.name)
1158
+ resourceEntries.length === 0 ||
1159
+ resourceEntries.length > PROTOCOL_V2_RESOURCE_FILE_MAX_COUNT
1207
1160
  ) {
1208
1161
  throw ERRORS.TypedError(
1209
1162
  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',
1163
+ 'Protocol V2 resource ZIP has no valid resource package set',
1219
1164
  { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1220
1165
  );
1221
1166
  }
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;
1167
+
1168
+ let totalSize = 0;
1169
+ const resources: ProtocolV2LocalResourceArchive['resources'] = [];
1170
+ const devicePaths = new Set<string>();
1171
+ for (const entry of resourceEntries) {
1172
+ const { compressedSize, uncompressedSize } = getProtocolV2ZipEntrySizes(entry);
1173
+ totalSize += uncompressedSize;
1232
1174
  if (
1233
- sizes.uncompressedSize > entryLimit ||
1234
- declaredCompressedSize > binary.byteLength ||
1235
- declaredUncompressedSize >
1236
- PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES + PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
1175
+ compressedSize > binary.byteLength ||
1176
+ uncompressedSize > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES ||
1177
+ totalSize > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES
1237
1178
  ) {
1238
1179
  throw ERRORS.TypedError(
1239
1180
  HardwareErrorCode.RuntimeError,
1240
- 'Protocol V2 local resource ZIP declared size exceeds the allowed limit',
1181
+ `Protocol V2 resource package size is invalid: ${entry.name}`,
1241
1182
  { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1242
1183
  );
1243
1184
  }
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) {
1185
+ const fileBinary = await entry.async('arraybuffer');
1186
+ if (fileBinary.byteLength !== uncompressedSize) {
1302
1187
  throw ERRORS.TypedError(
1303
1188
  HardwareErrorCode.RuntimeError,
1304
- `Protocol V2 local resource ZIP is missing ${file.archive_path}`,
1305
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1189
+ `Protocol V2 resource package is incomplete: ${entry.name}`,
1190
+ { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
1306
1191
  );
1307
1192
  }
1308
- const { uncompressedSize } = getProtocolV2ZipEntrySizes(entry);
1309
- totalSize += uncompressedSize;
1310
- if (uncompressedSize !== file.size || totalSize > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES) {
1193
+ let header: ProtocolV2ResourcePackageHeader;
1194
+ try {
1195
+ header = parseProtocolV2ResourcePackage(fileBinary);
1196
+ } catch (error) {
1311
1197
  throw ERRORS.TypedError(
1312
1198
  HardwareErrorCode.RuntimeError,
1313
- `Protocol V2 local resource file declared size is invalid: ${file.archive_path}`,
1199
+ `Protocol V2 resource package is invalid: ${entry.name}: ${String(error)}`,
1314
1200
  { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1315
1201
  );
1316
1202
  }
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()) {
1203
+ const canonicalDevicePath = header.devicePath.toLowerCase();
1204
+ if (devicePaths.has(canonicalDevicePath)) {
1320
1205
  throw ERRORS.TypedError(
1321
1206
  HardwareErrorCode.RuntimeError,
1322
- `Protocol V2 local resource file does not match manifest: ${file.archive_path}`,
1323
- { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
1207
+ `Protocol V2 resource ZIP contains duplicate device path: ${header.devicePath}`,
1208
+ { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1324
1209
  );
1325
1210
  }
1326
- materializedEntries.push({ entryName: file.archive_path, binary: fileBinary });
1211
+ devicePaths.add(canonicalDevicePath);
1212
+ resources.push({ entryName: entry.name, binary: fileBinary, header });
1327
1213
  }
1328
- return { binary, materializedEntries };
1214
+ return { binary, resources };
1329
1215
  }
1330
1216
 
1331
1217
  private async prepareProtocolV2ResourceSources(): Promise<ProtocolV2ResourceBundleSource[]> {
@@ -1390,110 +1276,20 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1390
1276
  );
1391
1277
  }
1392
1278
 
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;
1279
+ // The verified ZIP bytes are authoritative. Non-okpkg entries are release
1280
+ // metadata only; each RESOURCE package carries its own device path.
1281
+ return this.createProtocolV2ResourceSourcesFromArchive(archiveBinary);
1492
1282
  }
1493
1283
 
1494
- private async runProtocolV2PreparedArtifacts(features: Features, firmwareType: EFirmwareType) {
1284
+ private async runProtocolV2PreparedArtifacts(
1285
+ features: Features,
1286
+ firmwareType: EFirmwareType,
1287
+ announceDownload = true
1288
+ ) {
1495
1289
  try {
1496
- this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
1290
+ if (announceDownload) {
1291
+ this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
1292
+ }
1497
1293
  const installSources = await this.prepareProtocolV2InstallSources(firmwareType, features);
1498
1294
  const resourceSources = await this.prepareProtocolV2ResourceSources();
1499
1295
  if (installSources.length === 0 && resourceSources.length === 0) {
@@ -1502,7 +1298,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1502
1298
  'No firmware to update'
1503
1299
  );
1504
1300
  }
1505
- this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
1301
+ if (announceDownload) {
1302
+ this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
1303
+ }
1506
1304
 
1507
1305
  return await this.executeProtocolV2SourceUpdate({
1508
1306
  installSources,
@@ -1557,21 +1355,20 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1557
1355
  const expected = this.params?.expectedTargetVersions;
1558
1356
  if (!expected) return;
1559
1357
  const features = this.protocolV2LatestFinalFeatures;
1560
- const requestedTargets = this.getProtocolV2RequestedTargets();
1561
- const applicationTargets = requestedTargets.filter(
1562
- target => target === 'app_v1' || target === 'app_v2'
1563
- );
1358
+ const deviceInfo = this.protocolV2LatestFinalDeviceInfo;
1564
1359
  const visibleVersions: Partial<Record<FirmwareUpdateV4Target, string>> = {
1565
1360
  boot: features ? getDeviceBootloaderVersion(features).join('.') : undefined,
1361
+ // Protocol V2 firmware exposes P1 as the device firmware version after reboot.
1362
+ app_v1: features
1363
+ ? getDeviceFirmwareVersion(features).join('.')
1364
+ : deviceInfo?.main_mcu?.application?.version,
1365
+ app_v2: deviceInfo?.main_mcu?.application_data?.version,
1566
1366
  coprocessor: features ? getDeviceBLEFirmwareVersion(features).join('.') : undefined,
1567
1367
  se01: features?.se01Version ?? undefined,
1568
1368
  se02: features?.se02Version ?? undefined,
1569
1369
  se03: features?.se03Version ?? undefined,
1570
1370
  se04: features?.se04Version ?? undefined,
1571
1371
  };
1572
- if (features && applicationTargets.length === 1) {
1573
- visibleVersions[applicationTargets[0]] = getDeviceFirmwareVersion(features).join('.');
1574
- }
1575
1372
  const expectedEntries = (
1576
1373
  Object.entries(expected) as Array<[FirmwareUpdateV4Target, string]>
1577
1374
  ).filter(([target]) => !targets || targets.includes(target));
@@ -1588,9 +1385,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1588
1385
  Math.floor(statusVersion / 0x100) % 0x100
1589
1386
  }.${statusVersion % 0x100}`;
1590
1387
  if (!observedVersion) {
1591
- if (this.protocolV2FinalStatusVerified) {
1388
+ const hasCompleteTargetEvidence =
1389
+ targetId !== undefined && this.protocolV2CompletedTargetIds.has(targetId);
1390
+ if (hasCompleteTargetEvidence) {
1592
1391
  Log.warn(
1593
- `Protocol V2 target ${target} has no observable final version; completed target status is authoritative`
1392
+ `Protocol V2 target ${target} has no observable final version; install completion is authoritative`
1594
1393
  );
1595
1394
  } else {
1596
1395
  throw ERRORS.TypedError(
@@ -1607,37 +1406,23 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1607
1406
  }
1608
1407
  }
1609
1408
 
1610
- private getProtocolV2RequestedTargets(): FirmwareUpdateV4Target[] {
1611
- if (this.params.targetsToUpdate?.length) {
1612
- return [...new Set(this.params.targetsToUpdate)];
1409
+ private async getProtocolV2DeviceFeatures(): Promise<Features> {
1410
+ let { features } = this.device;
1411
+ if (typeof this.device.getFeatures === 'function') {
1412
+ features ??= await this.device.getFeatures();
1613
1413
  }
1614
- if (this.params.preparedPlan?.targetsToUpdate.length) {
1615
- return [...new Set(this.params.preparedPlan.targetsToUpdate)] as FirmwareUpdateV4Target[];
1414
+ if (!features) {
1415
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Device features not available');
1616
1416
  }
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;
1639
- }
1640
- throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Device features not available');
1417
+ const deviceType = this.device.getCurrentDeviceType();
1418
+ if (deviceType !== EDeviceType.Pro2 && deviceType !== EDeviceType.Neo) {
1419
+ return features;
1420
+ }
1421
+ return {
1422
+ ...features,
1423
+ deviceType,
1424
+ firmwareType: this.device.getCurrentFirmwareType(),
1425
+ };
1641
1426
  }
1642
1427
 
1643
1428
  private getProtocolV2SerialNumber(deviceInfo: ProtocolV2DeviceInfo) {
@@ -1840,22 +1625,54 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1840
1625
  `Protocol V2 firmware fingerprint mismatch: ${key}/${component.target}`
1841
1626
  );
1842
1627
  }
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
1628
  return {
1854
1629
  ...target,
1855
1630
  binary,
1856
1631
  };
1857
1632
  }
1858
1633
 
1634
+ private async downloadRemoteProtocolV2ResourceArchive(
1635
+ features: Features,
1636
+ firmwareType: EFirmwareType
1637
+ ): Promise<ArrayBuffer> {
1638
+ const deviceType = getDeviceType(features);
1639
+ if (deviceType !== EDeviceType.Pro2 && deviceType !== EDeviceType.Neo) {
1640
+ throw ERRORS.TypedError(
1641
+ HardwareErrorCode.RuntimeError,
1642
+ 'Protocol V2 resource archive requires a Pro2 or Neo device'
1643
+ );
1644
+ }
1645
+ const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
1646
+ const source = DataManager.getProtocolV2ResourceSource(release);
1647
+ const expectedSha256 = normalizeProtocolV2Hex(source?.archiveSha256);
1648
+ if (
1649
+ !source?.archiveUrl ||
1650
+ !Number.isSafeInteger(source.archiveSize) ||
1651
+ source.archiveSize <= 0 ||
1652
+ source.archiveSize > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES ||
1653
+ !expectedSha256 ||
1654
+ !/^[0-9a-f]{64}$/u.test(expectedSha256)
1655
+ ) {
1656
+ throw ERRORS.TypedError(
1657
+ HardwareErrorCode.RuntimeError,
1658
+ 'Protocol V2 resource archive integrity metadata is invalid',
1659
+ { firmwareUpdateCode: 'FirmwarePlanInvalid' }
1660
+ );
1661
+ }
1662
+ const { binary } = await getSysResourceBinary(source.archiveUrl);
1663
+ if (
1664
+ binary.byteLength !== source.archiveSize ||
1665
+ bytesToHex(sha256(new Uint8Array(binary))) !== expectedSha256
1666
+ ) {
1667
+ throw ERRORS.TypedError(
1668
+ HardwareErrorCode.RuntimeError,
1669
+ 'Protocol V2 resource archive does not match the remote config',
1670
+ { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
1671
+ );
1672
+ }
1673
+ return binary;
1674
+ }
1675
+
1859
1676
  private async prepareRemoteProtocolV2Binaries(
1860
1677
  firmwareType: EFirmwareType,
1861
1678
  features: Features,
@@ -1899,8 +1716,14 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1899
1716
  : undefined;
1900
1717
  if (updateTarget && targetsToUpdate.has(updateTarget)) {
1901
1718
  const explicitInstallItem = explicitInstallItemByTargetId.get(target.targetId);
1902
- const installItem =
1903
- explicitInstallItem ?? (await this.downloadRemoteProtocolV2Component(key, component));
1719
+ let installItem = explicitInstallItem;
1720
+ if (!installItem) {
1721
+ installItem = await this.downloadRemoteProtocolV2Component(key, component);
1722
+ if (component.version) {
1723
+ this.params.expectedTargetVersions ??= {};
1724
+ this.params.expectedTargetVersions[updateTarget] = component.version.join('.');
1725
+ }
1726
+ }
1904
1727
  if (installItem.kind === 'bootloader') {
1905
1728
  bootloaderBinary = installItem.binary;
1906
1729
  } else {
@@ -1939,6 +1762,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1939
1762
  return `vol0:/${path}`;
1940
1763
  }
1941
1764
 
1765
+ private getProtocolV2ResourceComparePath(devicePath: string) {
1766
+ return isProtocolV2BootResourcePackagePath(devicePath)
1767
+ ? PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH
1768
+ : devicePath;
1769
+ }
1770
+
1942
1771
  private async readProtocolV2DeviceFileHeader(path: string, expectedSize?: number) {
1943
1772
  const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
1944
1773
  const filePath = this.getProtocolV2ResourceFilePath(path);
@@ -1950,7 +1779,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1950
1779
  !pathInfoRes.message?.exist ||
1951
1780
  pathInfoRes.message?.directory ||
1952
1781
  fileSize === undefined ||
1953
- fileSize < PROTOCOL_V2_OKPP_HEADER_SIZE ||
1782
+ fileSize < PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_SIZE ||
1954
1783
  (expectedSize !== undefined && fileSize !== expectedSize)
1955
1784
  ) {
1956
1785
  return null;
@@ -1959,8 +1788,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1959
1788
  const chunkSize = this.getProtocolV2FirmwareChunkSize('read');
1960
1789
  const chunks: Uint8Array[] = [];
1961
1790
  let offset = 0;
1962
- while (offset < PROTOCOL_V2_OKPP_HEADER_SIZE) {
1963
- const readLen = Math.min(chunkSize, PROTOCOL_V2_OKPP_HEADER_SIZE - offset);
1791
+ while (offset < PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_SIZE) {
1792
+ const readLen = Math.min(chunkSize, PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_SIZE - offset);
1964
1793
  const res = await typedCall('FilesystemFileRead', 'FilesystemFile', {
1965
1794
  file: {
1966
1795
  path: filePath,
@@ -1982,7 +1811,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1982
1811
  headerBytes.set(chunk, cursor);
1983
1812
  cursor += chunk.byteLength;
1984
1813
  });
1985
- return parseProtocolV2OkppHeader(headerBytes);
1814
+ try {
1815
+ return parseProtocolV2ResourcePackageHeader(headerBytes, fileSize);
1816
+ } catch {
1817
+ return null;
1818
+ }
1986
1819
  }
1987
1820
 
1988
1821
  /** Compare the downloaded and installed okpkg headers before transferring a resource. */
@@ -1997,7 +1830,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1997
1830
 
1998
1831
  try {
1999
1832
  const header = await this.readProtocolV2DeviceFileHeader(
2000
- bundle.devicePath,
1833
+ this.getProtocolV2ResourceComparePath(bundle.devicePath),
2001
1834
  bundle.source.size
2002
1835
  );
2003
1836
  if (!header) return false;
@@ -2012,6 +1845,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2012
1845
  if (!expectedHeaderHash || header.headerHash !== expectedHeaderHash) return false;
2013
1846
  return true;
2014
1847
  } catch (error) {
1848
+ if (isBleStaleBondHardwareError(error)) {
1849
+ throw error;
1850
+ }
2015
1851
  Log.log(`[FirmwareUpdateV4] RESC bundle ${bundle.name} header check failed: `, error);
2016
1852
  return false;
2017
1853
  }
@@ -2041,21 +1877,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2041
1877
  return this.device.features?.mode === 'romloader';
2042
1878
  }
2043
1879
 
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
-
1880
+ private async rebootProtocolV2ToBootloader() {
2059
1881
  try {
2060
1882
  this.postTipMessage(FirmwareUpdateTipMessage.AutoRebootToBootloader);
2061
1883
  await this.protocolV2Reboot(DeviceRebootType.Bootloader);
@@ -2073,6 +1895,24 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2073
1895
  }
2074
1896
  }
2075
1897
 
1898
+ async enterProtocolV2BootloaderMode() {
1899
+ // romloader is the first update environment and forwards targets to bootloader.
1900
+ // It rejects DeviceRebootType.Bootloader, so reuse the current connection.
1901
+ if (this.isProtocolV2RomloaderMode()) {
1902
+ Log.debug('Protocol V2 device is in romloader mode; start firmware update directly');
1903
+ this.protocolV2ExecutionInLoader = true;
1904
+ return false;
1905
+ }
1906
+ if (this.isProtocolV2BootloaderMode()) {
1907
+ Log.debug('Protocol V2 device is already in bootloader mode, skip reboot');
1908
+ this.protocolV2ExecutionInLoader = true;
1909
+ this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
1910
+ return false;
1911
+ }
1912
+
1913
+ return this.rebootProtocolV2ToBootloader();
1914
+ }
1915
+
2076
1916
  private async waitForProtocolV2BootloaderMode(
2077
1917
  timeout = PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT,
2078
1918
  retryInterval = 1000
@@ -2101,7 +1941,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2101
1941
  }
2102
1942
  lastError = new Error('Protocol V2 device is reachable but is not in bootloader mode');
2103
1943
  } catch (error) {
2104
- if (this.isProtocolV2ReconnectIdentityError(error)) {
1944
+ if (isBleStaleBondHardwareError(error) || this.isProtocolV2ReconnectIdentityError(error)) {
2105
1945
  throw error;
2106
1946
  }
2107
1947
  shouldReconnect = true;
@@ -2157,68 +1997,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2157
1997
  return entries;
2158
1998
  }
2159
1999
 
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({
2000
+ private async executeProtocolV2SourceUpdate({
2222
2001
  installSources,
2223
2002
  resourceSources,
2224
2003
  }: {
@@ -2226,38 +2005,22 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2226
2005
  resourceSources: ProtocolV2ResourceBundleSource[];
2227
2006
  }) {
2228
2007
  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
- }
2008
+ if (installSources.length > 0 || resourceSources.length > 0) {
2009
+ await this.enterProtocolV2BootloaderMode();
2010
+ // Clear stale boot-resource staging before writing any artifacts. The new
2011
+ // boot resource, resources and firmware then share one transfer session,
2012
+ // one global progress range and one multi-target install request.
2013
+ // Preserve the approved source order instead of synthesizing a boot-first
2014
+ // phase. Loader firmware owns the cross-stage handoff: bootloader runs its
2015
+ // component targets and leaves boot pending for romloader, while pending
2016
+ // runner-backed records always resume before the application boots.
2017
+ await this.ensureProtocolV2BootResourceStagingIsEmpty();
2018
+ await this.executeProtocolV2TransferPhase({
2019
+ installSources,
2020
+ resourceSources,
2021
+ });
2256
2022
  }
2257
- throw ERRORS.TypedError(
2258
- HardwareErrorCode.RuntimeError,
2259
- 'Protocol V2 execution has no final verification phase'
2260
- );
2023
+ return this.completeProtocolV2FinalVerification();
2261
2024
  }
2262
2025
 
2263
2026
  private async ensureProtocolV2BootResourceStagingIsEmpty() {
@@ -2289,19 +2052,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2289
2052
  this.protocolV2BootResourceStagingSafe = true;
2290
2053
  }
2291
2054
 
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
2055
  private async executeProtocolV2Update({
2306
2056
  fwBinaryMap,
2307
2057
  bootloaderBinary,
@@ -2326,7 +2076,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2326
2076
  kind: item.kind,
2327
2077
  }))
2328
2078
  );
2329
- return await this.executeProtocolV2Phases({
2079
+ return await this.executeProtocolV2SourceUpdate({
2330
2080
  installSources,
2331
2081
  resourceSources: [],
2332
2082
  });
@@ -2338,15 +2088,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2338
2088
  private async executeProtocolV2TransferPhase({
2339
2089
  installSources,
2340
2090
  resourceSources,
2341
- }: ProtocolV2ExecutionPhase) {
2091
+ }: ProtocolV2TransferBatch) {
2342
2092
  let totalSize = installSources.reduce((total, item) => total + item.source.size, 0);
2343
2093
  const resourcesToSync: ProtocolV2ResourceBundleSource[] = [];
2344
2094
  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))) {
2095
+ if (await this.isProtocolV2ResourceBundleUpToDate(resource)) {
2350
2096
  Log.log(`[FirmwareUpdateV4] skip RESC bundle ${resource.name}; already up to date`);
2351
2097
  } else {
2352
2098
  resourcesToSync.push(resource);
@@ -2355,11 +2101,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2355
2101
  }
2356
2102
 
2357
2103
  this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
2104
+ this.protocolV2LastTransferProgress = undefined;
2105
+ this.protocolV2LastTransferProgressAt = 0;
2358
2106
  let processedSize = 0;
2359
2107
  for (const resource of resourcesToSync) {
2360
2108
  // The bootloader keeps its live resource package mounted. FatFs rejects
2361
2109
  // replacing an open file, so early boot promotes this staging file before mounting it.
2362
- const writePath = resolveProtocolV2ResourceWritePath(resource.devicePath);
2110
+ const writePath = resource.devicePath;
2363
2111
  processedSize = await this.protocolV2SourceUpdateProcess({
2364
2112
  source: resource.source,
2365
2113
  filePath: writePath,
@@ -2400,8 +2148,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2400
2148
  target_id: item.targetId,
2401
2149
  path: item.path,
2402
2150
  }));
2403
- await this.protocolV2StartFirmwareUpdate({ targets });
2404
- await this.waitForProtocolV2FirmwareUpdateComplete(targets);
2151
+ try {
2152
+ await this.protocolV2StartFirmwareUpdate({ targets });
2153
+ await wait(PROTOCOL_V2_INSTALL_STATUS_INITIAL_DELAY);
2154
+ await this.waitForProtocolV2FirmwareUpdateComplete(targets, true);
2155
+ } finally {
2156
+ this.device?.clearCancelableAction();
2157
+ }
2405
2158
  }
2406
2159
 
2407
2160
  private async protocolV2SourceUpdateProcess({
@@ -2446,23 +2199,33 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2446
2199
  );
2447
2200
  }
2448
2201
  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
- {
2202
+ const now = Date.now();
2203
+ const elapsedMs = Math.max(now - transferStartedAt, 0);
2204
+ const progress = Math.min(Math.ceil((transferredBytes / totalSize) * 100), 99);
2205
+ const shouldPostProgress =
2206
+ progress !== this.protocolV2LastTransferProgress ||
2207
+ now - this.protocolV2LastTransferProgressAt >=
2208
+ PROTOCOL_V2_TRANSFER_PROGRESS_HEARTBEAT_MS ||
2209
+ chunkEnd === source.size;
2210
+ if (shouldPostProgress) {
2211
+ this.protocolV2LastTransferProgress = progress;
2212
+ this.protocolV2LastTransferProgressAt = now;
2213
+ this.postProgressMessage(progress, 'transferData', {
2454
2214
  transferredBytes,
2455
2215
  totalBytes: totalSize,
2456
2216
  rateBytesPerSecond:
2457
2217
  elapsedMs > 0 ? Math.round((chunkEnd / elapsedMs) * 1000) : undefined,
2458
2218
  elapsedMs,
2459
- }
2460
- );
2219
+ });
2220
+ }
2461
2221
  return length;
2462
2222
  },
2463
2223
  });
2464
2224
  return processedSize + source.size;
2465
2225
  } catch (error) {
2226
+ if (isBleStaleBondHardwareError(error)) {
2227
+ throw error;
2228
+ }
2466
2229
  lastError = error;
2467
2230
  if (attempt < PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT) {
2468
2231
  await this.recoverProtocolV2FileTransfer();
@@ -2492,19 +2255,41 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2492
2255
  }
2493
2256
  }
2494
2257
 
2258
+ private collectProtocolV2LiveTargetIds(
2259
+ statusTargets: ProtocolV2FirmwareUpdateStatusTarget[],
2260
+ expectedTargetIds: Set<number>,
2261
+ liveTargetIds: Set<number>
2262
+ ) {
2263
+ statusTargets.forEach(target => {
2264
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2265
+ if (
2266
+ targetId !== undefined &&
2267
+ expectedTargetIds.has(targetId) &&
2268
+ isProtocolV2TargetStatusInProgress(target.status)
2269
+ ) {
2270
+ liveTargetIds.add(targetId);
2271
+ }
2272
+ });
2273
+ }
2274
+
2495
2275
  private assertProtocolV2TargetStatus(
2496
2276
  statusTargets: ProtocolV2FirmwareUpdateStatusTarget[],
2497
2277
  expectedTargetIds: Set<number>,
2498
- expectedPaths = new Map<number, string>()
2278
+ expectedPaths?: Map<number, string>,
2279
+ liveTargetIds?: Set<number>
2499
2280
  ) {
2281
+ const resolvedExpectedPaths = expectedPaths ?? new Map<number, string>();
2500
2282
  Log.log(
2501
2283
  `[FirmwareUpdateV4] DeviceFirmwareUpdateStatus records=${JSON.stringify(statusTargets)}`
2502
2284
  );
2503
- const failedTarget = statusTargets.find(
2504
- target =>
2505
- expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1) &&
2506
- isProtocolV2TargetStatusFailed(target.status)
2507
- );
2285
+ const failedTarget = statusTargets.find(target => {
2286
+ const targetId = normalizeProtocolV2TargetId(target.target_id) ?? -1;
2287
+ return (
2288
+ expectedTargetIds.has(targetId) &&
2289
+ isProtocolV2TargetStatusFailed(target.status) &&
2290
+ (!liveTargetIds || liveTargetIds.has(targetId))
2291
+ );
2292
+ });
2508
2293
  if (failedTarget) {
2509
2294
  const failedTargetDetails = JSON.stringify({
2510
2295
  targetId: failedTarget.target_id,
@@ -2515,13 +2300,33 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2515
2300
  Log.error(`[FirmwareUpdateV4] firmware install failed target=${failedTargetDetails}`);
2516
2301
  throw ERRORS.TypedError(
2517
2302
  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
- }`
2303
+ 'Protocol V2 firmware install failed',
2304
+ {
2305
+ firmwareUpdateCode: PROTOCOL_V2_INSTALL_FAILED_CODE,
2306
+ }
2523
2307
  );
2524
2308
  }
2309
+ if (liveTargetIds) {
2310
+ const staleFailedTargets = statusTargets.filter(target => {
2311
+ const targetId = normalizeProtocolV2TargetId(target.target_id) ?? -1;
2312
+ return (
2313
+ expectedTargetIds.has(targetId) &&
2314
+ isProtocolV2TargetStatusFailed(target.status) &&
2315
+ !liveTargetIds.has(targetId)
2316
+ );
2317
+ });
2318
+ if (staleFailedTargets.length > 0) {
2319
+ Log.log(
2320
+ `[FirmwareUpdateV4] ignoring stale failed firmware status until those targets start: ${JSON.stringify(
2321
+ staleFailedTargets.map(target => ({
2322
+ targetId: target.target_id,
2323
+ status: target.status,
2324
+ path: target.path,
2325
+ }))
2326
+ )}`
2327
+ );
2328
+ }
2329
+ }
2525
2330
 
2526
2331
  const matchingTargets = statusTargets.filter(target =>
2527
2332
  expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1)
@@ -2529,14 +2334,21 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2529
2334
  const seenTargetIds = new Set<number>();
2530
2335
  for (const target of matchingTargets) {
2531
2336
  const targetId = normalizeProtocolV2TargetId(target.target_id);
2337
+ const pathConflictsWithCurrentInstall =
2338
+ targetId !== undefined &&
2339
+ Boolean(target.path) &&
2340
+ Boolean(resolvedExpectedPaths.get(targetId)) &&
2341
+ target.path !== resolvedExpectedPaths.get(targetId) &&
2342
+ (!liveTargetIds || liveTargetIds.has(targetId));
2532
2343
  if (
2533
2344
  targetId === undefined ||
2534
2345
  seenTargetIds.has(targetId) ||
2535
- (target.path && expectedPaths.get(targetId) && target.path !== expectedPaths.get(targetId))
2346
+ pathConflictsWithCurrentInstall
2536
2347
  ) {
2348
+ Log.error(`[FirmwareUpdateV4] install status conflicts with target=${target.target_id}`);
2537
2349
  throw ERRORS.TypedError(
2538
- HardwareErrorCode.RuntimeError,
2539
- `Protocol V2 install status conflicts with target ${target.target_id}`,
2350
+ HardwareErrorCode.FirmwareError,
2351
+ 'Protocol V2 firmware install status conflict',
2540
2352
  {
2541
2353
  firmwareUpdateCode: PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE,
2542
2354
  }
@@ -2544,14 +2356,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2544
2356
  }
2545
2357
  seenTargetIds.add(targetId);
2546
2358
  }
2547
- const completedTargets = matchingTargets.filter(target =>
2548
- isProtocolV2TargetStatusFinished(target.status)
2549
- );
2359
+ const completedTargets = matchingTargets.filter(target => {
2360
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2361
+ return (
2362
+ isProtocolV2TargetStatusFinished(target.status) &&
2363
+ (!liveTargetIds || (targetId !== undefined && liveTargetIds.has(targetId)))
2364
+ );
2365
+ });
2550
2366
  const completedTargetIds = new Set<number>();
2551
2367
  completedTargets.forEach(target => {
2552
2368
  const targetId = normalizeProtocolV2TargetId(target.target_id);
2553
2369
  if (targetId !== undefined) {
2554
2370
  completedTargetIds.add(targetId);
2371
+ this.protocolV2CompletedTargetIds.add(targetId);
2555
2372
  }
2556
2373
  });
2557
2374
  if (completedTargetIds.size === expectedTargetIds.size && expectedTargetIds.size > 0) {
@@ -2582,16 +2399,54 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2582
2399
  }
2583
2400
 
2584
2401
  if (expectedTargetIds.size > 0 && matchingTargets.length > 0) {
2585
- const hasInProgressTarget = matchingTargets.some(target =>
2586
- isProtocolV2TargetStatusInProgress(target.status)
2587
- );
2402
+ let detailedProgress = completedTargetIds.size * 100;
2403
+ let installProgressMetadata:
2404
+ | {
2405
+ installTargetId: number;
2406
+ installPhase: 'prepare' | 'install' | 'verify';
2407
+ installPhaseProgress: number;
2408
+ }
2409
+ | undefined;
2410
+ let hasInProgressTarget = false;
2411
+ matchingTargets.forEach(target => {
2412
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2413
+ if (
2414
+ targetId === undefined ||
2415
+ completedTargetIds.has(targetId) ||
2416
+ !isProtocolV2TargetStatusInProgress(target.status) ||
2417
+ (liveTargetIds && !liveTargetIds.has(targetId))
2418
+ ) {
2419
+ return;
2420
+ }
2421
+ hasInProgressTarget = true;
2422
+ detailedProgress += normalizeProtocolV2ProgressPercent(target.progress_percent) ?? 0;
2423
+ const installPhase = target.phase_info
2424
+ ? PROTOCOL_V2_INSTALL_PHASE_BY_DECODED_VALUE.get(target.phase_info.phase)
2425
+ : undefined;
2426
+ const installPhaseProgress = normalizeProtocolV2ProgressPercent(
2427
+ target.phase_info?.progress_percent
2428
+ );
2429
+ if (installPhase && installPhaseProgress !== undefined && !installProgressMetadata) {
2430
+ installProgressMetadata = {
2431
+ installTargetId: targetId,
2432
+ installPhase,
2433
+ installPhaseProgress,
2434
+ };
2435
+ }
2436
+ });
2588
2437
  const completedProgress = Math.floor(
2589
2438
  (completedTargetIds.size / expectedTargetIds.size) * 100
2590
2439
  );
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');
2440
+ const reportedProgress = Math.floor(detailedProgress / expectedTargetIds.size);
2441
+ const progress = Math.min(
2442
+ 99,
2443
+ Math.max(completedProgress, reportedProgress, hasInProgressTarget ? 1 : 0)
2444
+ );
2445
+ if (installProgressMetadata) {
2446
+ this.postProgressMessage(progress, 'installingFirmware', installProgressMetadata);
2447
+ } else {
2448
+ this.postProgressMessage(progress, 'installingFirmware');
2449
+ }
2595
2450
  }
2596
2451
 
2597
2452
  return false;
@@ -2611,31 +2466,95 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2611
2466
  return Array.from(expectedTargetIds).filter(targetId => !reportedTargetIds.has(targetId));
2612
2467
  }
2613
2468
 
2469
+ private getProtocolV2ObservableTargetVersions(features: Features) {
2470
+ const versions = new Map<number, string>();
2471
+ versions.set(
2472
+ ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER,
2473
+ getDeviceBootloaderVersion(features).join('.')
2474
+ );
2475
+ const applicationVersion = getDeviceFirmwareVersion(features).join('.');
2476
+ versions.set(ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1, applicationVersion);
2477
+ versions.set(ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2, applicationVersion);
2478
+ versions.set(
2479
+ ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_COPROCESSOR,
2480
+ getDeviceBLEFirmwareVersion(features).join('.')
2481
+ );
2482
+ const secureElementVersions: Array<[number, string | null | undefined]> = [
2483
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE01, features.se01Version],
2484
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE02, features.se02Version],
2485
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE03, features.se03Version],
2486
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE04, features.se04Version],
2487
+ ];
2488
+ secureElementVersions.forEach(([targetId, version]) => {
2489
+ if (version) versions.set(targetId, version);
2490
+ });
2491
+ return versions;
2492
+ }
2493
+
2494
+ private hasProtocolV2InstallVersionChanged(expectedTargetIds: Set<number>) {
2495
+ if (!this.protocolV2LastRuntimeProbeFeatures) return false;
2496
+ const currentVersions = this.getProtocolV2ObservableTargetVersions(
2497
+ this.protocolV2LastRuntimeProbeFeatures
2498
+ );
2499
+ return Array.from(expectedTargetIds).some(targetId => {
2500
+ const previousVersion = this.protocolV2InstallBaselineVersions.get(targetId);
2501
+ const currentVersion = currentVersions.get(targetId);
2502
+ return (
2503
+ previousVersion !== undefined &&
2504
+ currentVersion !== undefined &&
2505
+ previousVersion !== currentVersion
2506
+ );
2507
+ });
2508
+ }
2509
+
2510
+ private recordProtocolV2AuthoritativeInstallCompletion(expectedTargetIds: Set<number>) {
2511
+ expectedTargetIds.forEach(targetId => this.protocolV2CompletedTargetIds.add(targetId));
2512
+ }
2513
+
2614
2514
  private async waitForProtocolV2FirmwareUpdateComplete(
2615
- targets: Array<{ target_id: number; path: string }>
2515
+ targets: Array<{ target_id: number; path: string }>,
2516
+ requireCurrentInstallStatus = false
2616
2517
  ) {
2617
- this.protocolV2FinalStatusVerified = false;
2618
2518
  const expectedTargetIds = new Set(targets.map(target => target.target_id));
2619
2519
  const expectedPaths = new Map(targets.map(target => [target.target_id, target.path]));
2520
+ const isBleCoprocessorInstall =
2521
+ this.isBleReconnect() &&
2522
+ expectedTargetIds.has(ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_COPROCESSOR);
2620
2523
  const startTime = Date.now();
2621
2524
  let lastError: unknown;
2622
- let shouldReconnect = true;
2525
+ // The loader may release either USB or BLE as installation starts. Recovery reconnects
2526
+ // directly to status polling without generic Ping or DeviceInfo probes.
2527
+ let shouldReconnect = this.protocolV2InstallNeedsReconnect;
2528
+ this.protocolV2InstallNeedsReconnect = false;
2623
2529
  let deviceInfo: ProtocolV2DeviceInfo | undefined;
2624
- let missingTargetStatusSince: number | undefined;
2625
- let missingTargetStatusKey: string | undefined;
2626
- const resetMissingTargetStatusGrace = () => {
2627
- missingTargetStatusSince = undefined;
2628
- missingTargetStatusKey = undefined;
2629
- };
2530
+ let bleInstallLinkReady = false;
2531
+ let installStatusDisconnectObserved = false;
2532
+ let installReconnectObserved = false;
2533
+ let finishedStatusSnapshotKey: string | undefined;
2534
+ let finishedStatusSnapshotPolls = 0;
2535
+ let fastFinishedInstallProgressReported = false;
2536
+ let installEvidenceObserved = this.protocolV2InstallTerminalSuccessObserved;
2537
+ let currentInstallStatusObserved = false;
2538
+ const liveTargetIds = new Set<number>();
2630
2539
 
2631
2540
  while (Date.now() - startTime < PROTOCOL_V2_INSTALL_TIMEOUT) {
2541
+ // A transport release caused by an explicit workflow cancellation must not
2542
+ // be mistaken for the expected device reboot during installation.
2543
+ this.throwIfAborted();
2632
2544
  try {
2633
2545
  if (shouldReconnect) {
2634
- await this.reconnectProtocolV2Device();
2635
- deviceInfo = await this.verifyProtocolV2ReconnectIdentity();
2546
+ const isBleInstallReconnect = this.isBleReconnect();
2547
+ await this.reconnectProtocolV2Device({ skipProtocolProbe: true });
2548
+ bleInstallLinkReady = isBleInstallReconnect;
2549
+ deviceInfo = isBleInstallReconnect
2550
+ ? undefined
2551
+ : await this.verifyProtocolV2ReconnectIdentity();
2552
+ if (isBleInstallReconnect && installStatusDisconnectObserved) {
2553
+ installReconnectObserved = true;
2554
+ }
2636
2555
  shouldReconnect = false;
2637
2556
  }
2638
- const currentDeviceInfo = deviceInfo;
2557
+ let currentDeviceInfo = deviceInfo;
2639
2558
  try {
2640
2559
  const statusResponse = await this.device.getCommands().typedCall(
2641
2560
  'DeviceFirmwareUpdateStatusGet',
@@ -2643,91 +2562,243 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2643
2562
  {
2644
2563
  fields: {
2645
2564
  status: true,
2565
+ progress_percent: true,
2566
+ phase_info: true,
2646
2567
  payload_version: true,
2647
2568
  path: true,
2648
2569
  },
2649
2570
  },
2650
2571
  { timeoutMs: PROTOCOL_V2_FIRMWARE_STATUS_RESPONSE_TIMEOUT }
2651
2572
  );
2573
+ if (this.protocolV2InstallTerminalSuccessObserved) {
2574
+ installEvidenceObserved = true;
2575
+ }
2652
2576
  const statusTargets = (statusResponse.message.records ??
2653
2577
  []) as ProtocolV2FirmwareUpdateStatusTarget[];
2654
- if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds, expectedPaths)) {
2655
- this.protocolV2FinalStatusVerified = true;
2578
+ this.collectProtocolV2LiveTargetIds(statusTargets, expectedTargetIds, liveTargetIds);
2579
+ if (liveTargetIds.size > 0) {
2580
+ currentInstallStatusObserved = true;
2581
+ }
2582
+ const hasMatchingTargetStatus = statusTargets.some(target => {
2583
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2584
+ return targetId !== undefined && expectedTargetIds.has(targetId);
2585
+ });
2586
+ if (
2587
+ hasMatchingTargetStatus &&
2588
+ (!requireCurrentInstallStatus || currentInstallStatusObserved)
2589
+ ) {
2590
+ installEvidenceObserved = true;
2591
+ }
2592
+ const matchingStatusTargets = statusTargets.filter(target => {
2593
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2594
+ return targetId !== undefined && expectedTargetIds.has(targetId);
2595
+ });
2596
+ const matchingFinishedTargetIds = new Set<number>();
2597
+ const matchingFinishedStatus = matchingStatusTargets
2598
+ .map(target => {
2599
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
2600
+ const expectedPath = targetId === undefined ? undefined : expectedPaths.get(targetId);
2601
+ const expectedVersion =
2602
+ targetId === undefined
2603
+ ? undefined
2604
+ : this.getExpectedProtocolV2TargetVersion(targetId);
2605
+ const payloadVersion = toProtocolV2FiniteNumber(target.payload_version);
2606
+ if (
2607
+ targetId === undefined ||
2608
+ matchingFinishedTargetIds.has(targetId) ||
2609
+ !isProtocolV2TargetStatusFinished(target.status) ||
2610
+ (Boolean(target.path) && Boolean(expectedPath) && target.path !== expectedPath) ||
2611
+ (expectedVersion !== undefined &&
2612
+ payloadVersion !== undefined &&
2613
+ payloadVersion !== expectedVersion)
2614
+ ) {
2615
+ return undefined;
2616
+ }
2617
+ matchingFinishedTargetIds.add(targetId);
2618
+ return [targetId, target.path ?? null, payloadVersion ?? null] as const;
2619
+ })
2620
+ .filter((target): target is readonly [number, string | null, number | null] => !!target)
2621
+ .sort(([leftTargetId], [rightTargetId]) => leftTargetId - rightTargetId);
2622
+ const hasCompleteMatchingFinishedStatus =
2623
+ matchingFinishedStatus.length === expectedTargetIds.size &&
2624
+ matchingFinishedTargetIds.size === expectedTargetIds.size &&
2625
+ expectedTargetIds.size > 0;
2626
+ if (
2627
+ requireCurrentInstallStatus &&
2628
+ installReconnectObserved &&
2629
+ hasCompleteMatchingFinishedStatus
2630
+ ) {
2631
+ const nextSnapshotKey = JSON.stringify(matchingFinishedStatus);
2632
+ if (nextSnapshotKey === finishedStatusSnapshotKey) {
2633
+ finishedStatusSnapshotPolls += 1;
2634
+ } else {
2635
+ finishedStatusSnapshotKey = nextSnapshotKey;
2636
+ finishedStatusSnapshotPolls = 1;
2637
+ }
2638
+ } else {
2639
+ finishedStatusSnapshotKey = undefined;
2640
+ finishedStatusSnapshotPolls = 0;
2641
+ }
2642
+ if (
2643
+ requireCurrentInstallStatus &&
2644
+ installReconnectObserved &&
2645
+ hasCompleteMatchingFinishedStatus &&
2646
+ !currentInstallStatusObserved &&
2647
+ !this.protocolV2InstallTerminalSuccessObserved &&
2648
+ !fastFinishedInstallProgressReported
2649
+ ) {
2650
+ // A fast BLE install may finish while disconnected, so IN_PROGRESS can be hidden.
2651
+ // A status response timeout alone is not evidence that the install was accepted.
2652
+ this.postProgressMessage(1, 'installingFirmware');
2653
+ fastFinishedInstallProgressReported = true;
2654
+ }
2655
+ const finishedAfterDisconnectObserved =
2656
+ finishedStatusSnapshotPolls >= PROTOCOL_V2_INSTALL_FINISHED_AFTER_DISCONNECT_POLLS;
2657
+ const hasCurrentInstallEvidence =
2658
+ currentInstallStatusObserved ||
2659
+ this.protocolV2InstallTerminalSuccessObserved ||
2660
+ finishedAfterDisconnectObserved;
2661
+ if (hasCurrentInstallEvidence) {
2662
+ installEvidenceObserved = true;
2663
+ }
2664
+ const shouldVerifyTargetCompletion =
2665
+ !requireCurrentInstallStatus || hasCurrentInstallEvidence;
2666
+ if (
2667
+ shouldVerifyTargetCompletion &&
2668
+ this.assertProtocolV2TargetStatus(
2669
+ statusTargets,
2670
+ expectedTargetIds,
2671
+ expectedPaths,
2672
+ requireCurrentInstallStatus &&
2673
+ !this.protocolV2InstallTerminalSuccessObserved &&
2674
+ !finishedAfterDisconnectObserved
2675
+ ? liveTargetIds
2676
+ : undefined
2677
+ )
2678
+ ) {
2656
2679
  return;
2657
2680
  }
2658
2681
 
2682
+ if (!hasCurrentInstallEvidence) {
2683
+ // A successful status poll clears DeviceCommands' cancel action. Keep cancellation
2684
+ // available while UpdateRequest is still waiting for confirmation on the device.
2685
+ this.device.setCancelableAction(() => this.device.getCommands().cancelDevice());
2686
+ }
2687
+
2659
2688
  if (
2660
- statusTargets.length === 0 &&
2661
- currentDeviceInfo &&
2662
- (await this.probeProtocolV2NormalMode(currentDeviceInfo))
2689
+ requireCurrentInstallStatus &&
2690
+ !hasCurrentInstallEvidence &&
2691
+ matchingStatusTargets.length > 0
2663
2692
  ) {
2664
- Log.log(
2665
- '[FirmwareUpdateV4] empty firmware status after confirmed App reboot; update complete'
2693
+ lastError = new Error(
2694
+ 'Protocol V2 firmware status is stale; waiting for the current install to start'
2666
2695
  );
2667
- this.postProgressMessage(100, 'installingFirmware');
2668
- return;
2669
2696
  }
2670
2697
 
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
2698
+ if (
2699
+ statusTargets.length === 0 &&
2700
+ !currentDeviceInfo &&
2701
+ bleInstallLinkReady &&
2702
+ installEvidenceObserved
2703
+ ) {
2704
+ // BLE install reconnect skips generic probes because loaders may not answer Ping.
2705
+ // Restore the verified identity only after current-install evidence disappears.
2706
+ currentDeviceInfo = await this.verifyProtocolV2ReconnectIdentity();
2707
+ deviceInfo = currentDeviceInfo;
2708
+ }
2709
+
2710
+ if (statusTargets.length === 0 && currentDeviceInfo) {
2711
+ const isNormalMode = await this.probeProtocolV2NormalMode(currentDeviceInfo);
2712
+ if (
2713
+ isNormalMode &&
2714
+ (installEvidenceObserved ||
2715
+ this.hasProtocolV2InstallVersionChanged(expectedTargetIds))
2684
2716
  ) {
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
- )}`
2717
+ Log.log(
2718
+ '[FirmwareUpdateV4] empty firmware status after confirmed App reboot; update complete'
2693
2719
  );
2720
+ this.recordProtocolV2AuthoritativeInstallCompletion(expectedTargetIds);
2721
+ this.postProgressMessage(100, 'installingFirmware');
2722
+ return;
2694
2723
  }
2724
+ }
2725
+
2726
+ if (statusTargets.length === 0) {
2695
2727
  lastError = new Error(
2696
- `Protocol V2 firmware status is temporarily missing targetIds=${missingKey}`
2728
+ 'Protocol V2 firmware update is waiting for user confirmation or target status'
2697
2729
  );
2698
2730
  } else {
2699
- resetMissingTargetStatusGrace();
2700
- lastError = new Error('Protocol V2 firmware targets are still installing');
2731
+ const missingTargetIds = this.getProtocolV2MissingTargetIds(
2732
+ statusTargets,
2733
+ expectedTargetIds
2734
+ );
2735
+ if (missingTargetIds.length > 0) {
2736
+ const missingKey = missingTargetIds.join(',');
2737
+ lastError = new Error(
2738
+ `Protocol V2 firmware status is temporarily missing targetIds=${missingKey}`
2739
+ );
2740
+ } else {
2741
+ lastError = new Error('Protocol V2 firmware targets are still installing');
2742
+ }
2701
2743
  }
2702
2744
  } catch (error) {
2703
2745
  if (isProtocolV2TerminalInstallStatusError(error)) {
2704
2746
  throw error;
2705
2747
  }
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
2748
  // App firmware does not register DeviceFirmwareUpdateStatusGet. Treat the
2710
2749
  // missing endpoint as completion only after the runtime probe confirms App mode.
2711
2750
  if (isProtocolV2FirmwareStatusEndpointUnavailable(error)) {
2712
2751
  if (!currentDeviceInfo) {
2752
+ if (!bleInstallLinkReady) {
2753
+ throw ERRORS.TypedError(
2754
+ HardwareErrorCode.RuntimeError,
2755
+ 'Protocol V2 device identity is unavailable during install polling'
2756
+ );
2757
+ }
2758
+ deviceInfo = await this.verifyProtocolV2ReconnectIdentity();
2759
+ }
2760
+ const reconnectDeviceInfo = currentDeviceInfo ?? deviceInfo;
2761
+ if (!reconnectDeviceInfo) {
2713
2762
  throw ERRORS.TypedError(
2714
2763
  HardwareErrorCode.RuntimeError,
2715
2764
  'Protocol V2 device identity is unavailable during install polling'
2716
2765
  );
2717
2766
  }
2718
- if (await this.probeProtocolV2NormalMode(currentDeviceInfo)) {
2767
+ const isNormalMode = await this.probeProtocolV2NormalMode(reconnectDeviceInfo);
2768
+ if (
2769
+ isNormalMode &&
2770
+ (installEvidenceObserved ||
2771
+ this.hasProtocolV2InstallVersionChanged(expectedTargetIds))
2772
+ ) {
2719
2773
  Log.log(
2720
2774
  '[FirmwareUpdateV4] firmware status endpoint unavailable after confirmed App reboot'
2721
2775
  );
2776
+ this.recordProtocolV2AuthoritativeInstallCompletion(expectedTargetIds);
2722
2777
  this.postProgressMessage(100, 'installingFirmware');
2723
2778
  return;
2724
2779
  }
2725
- lastError = new Error(
2726
- 'Protocol V2 firmware status endpoint is unavailable while the device remains in loader mode'
2727
- );
2780
+ if (isNormalMode) {
2781
+ lastError = new Error(
2782
+ 'Protocol V2 device is in normal mode but installation is not yet confirmed'
2783
+ );
2784
+ } else {
2785
+ lastError = new Error(
2786
+ 'Protocol V2 firmware status endpoint is unavailable while the device remains in loader mode'
2787
+ );
2788
+ }
2728
2789
  } else {
2790
+ if (
2791
+ isBleCoprocessorInstall &&
2792
+ isProtocolV2DeviceDisconnectedError(error) &&
2793
+ !isProtocolV2ResponseTimeout(error)
2794
+ ) {
2795
+ installStatusDisconnectObserved = true;
2796
+ finishedStatusSnapshotKey = undefined;
2797
+ finishedStatusSnapshotPolls = 0;
2798
+ }
2729
2799
  shouldReconnect = true;
2730
2800
  deviceInfo = undefined;
2801
+ bleInstallLinkReady = false;
2731
2802
  lastError = error;
2732
2803
  Log.log(
2733
2804
  '[FirmwareUpdateV4] DeviceFirmwareUpdateStatusGet unavailable during install: ',
@@ -2751,18 +2822,24 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2751
2822
  }
2752
2823
  shouldReconnect = true;
2753
2824
  deviceInfo = undefined;
2754
- resetMissingTargetStatusGrace();
2825
+ bleInstallLinkReady = false;
2755
2826
  Log.log('Protocol V2 firmware install device readiness probe failed: ', error);
2756
2827
  }
2757
2828
  await wait(1000);
2758
2829
  }
2759
2830
 
2760
- throw ERRORS.TypedError(
2761
- HardwareErrorCode.RuntimeError,
2762
- `Protocol V2 firmware update status not complete within ${
2831
+ Log.error(
2832
+ `[FirmwareUpdateV4] install timed out after ${
2763
2833
  PROTOCOL_V2_INSTALL_TIMEOUT / 1000
2764
2834
  }s: ${this.normalizeErrorMessage(lastError)}`
2765
2835
  );
2836
+ throw ERRORS.TypedError(
2837
+ HardwareErrorCode.FirmwareError,
2838
+ 'Protocol V2 firmware install timed out',
2839
+ {
2840
+ firmwareUpdateCode: PROTOCOL_V2_INSTALL_TIMEOUT_CODE,
2841
+ }
2842
+ );
2766
2843
  }
2767
2844
 
2768
2845
  private async exitProtocolV2BootloaderToNormal() {
@@ -2774,6 +2851,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2774
2851
  return;
2775
2852
  }
2776
2853
  } catch (error) {
2854
+ if (isBleStaleBondHardwareError(error)) {
2855
+ throw error;
2856
+ }
2777
2857
  Log.log('[FirmwareUpdateV4] unable to confirm App mode before Normal reboot: ', error);
2778
2858
  }
2779
2859
  await this.protocolV2Reboot(DeviceRebootType.Normal);
@@ -2785,7 +2865,14 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2785
2865
  deviceInfo,
2786
2866
  PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT
2787
2867
  );
2788
- return features.mode === 'normal' && !features.bootloaderMode;
2868
+ this.protocolV2LastRuntimeProbeFeatures = features;
2869
+ return this.isProtocolV2ApplicationMode(features);
2870
+ }
2871
+
2872
+ private isProtocolV2ApplicationMode(features: Features) {
2873
+ return (
2874
+ (features.mode === 'normal' || features.mode === 'notInitialized') && !features.bootloaderMode
2875
+ );
2789
2876
  }
2790
2877
 
2791
2878
  private async waitForProtocolV2FinalFeatures() {
@@ -2829,6 +2916,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2829
2916
  const startTime = Date.now();
2830
2917
  let lastError: unknown;
2831
2918
  let shouldReconnect = true;
2919
+ this.protocolV2LatestFinalDeviceInfo = undefined;
2832
2920
 
2833
2921
  while (Date.now() - startTime < timeout) {
2834
2922
  try {
@@ -2845,9 +2933,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2845
2933
  this.assertProtocolV2DeviceInfoIdentity(deviceInfo);
2846
2934
  const features = await this.device.probeProtocolV2RuntimeState(
2847
2935
  deviceInfo,
2848
- PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT
2936
+ PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
2937
+ { forceRuntimeContextRefresh: true }
2849
2938
  );
2850
- if (features.mode === 'normal' && !features.bootloaderMode) {
2939
+ if (this.isProtocolV2ApplicationMode(features)) {
2940
+ this.protocolV2LatestFinalDeviceInfo = deviceInfo;
2851
2941
  return features;
2852
2942
  }
2853
2943
  lastError = ERRORS.TypedError(
@@ -2855,7 +2945,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2855
2945
  'Protocol V2 device is still in bootloader mode'
2856
2946
  );
2857
2947
  } catch (error) {
2858
- if (this.isProtocolV2ReconnectIdentityError(error)) {
2948
+ if (isBleStaleBondHardwareError(error) || this.isProtocolV2ReconnectIdentityError(error)) {
2859
2949
  throw error;
2860
2950
  }
2861
2951
  shouldReconnect = true;
@@ -2873,19 +2963,53 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2873
2963
  );
2874
2964
  }
2875
2965
 
2876
- private async reconnectProtocolV2Device() {
2966
+ private async reconnectProtocolV2Device(options?: { skipProtocolProbe?: boolean }) {
2877
2967
  if (this.isBleReconnect()) {
2878
- await this.acquireProtocolV2BleDevice();
2968
+ await this.acquireProtocolV2BleDevice(options?.skipProtocolProbe);
2879
2969
  return;
2880
2970
  }
2881
2971
 
2882
2972
  const deviceDiff = await this.device.deviceConnector?.enumerate();
2883
2973
  const devicesDescriptor = deviceDiff?.descriptors ?? [];
2974
+ const env = DataManager.getSettings('env');
2975
+ const isWebUsb = DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env);
2884
2976
 
2885
- if (
2886
- DataManager.isBrowserWebUsb(DataManager.getSettings('env')) &&
2887
- devicesDescriptor.length === 1
2888
- ) {
2977
+ if (isWebUsb && options?.skipProtocolProbe) {
2978
+ const expectedSerialNumber = this.protocolV2ExpectedSerialNumber?.trim();
2979
+ const expectedPath = this.protocolV2ExpectedPath?.trim();
2980
+ const reconnectDescriptor = devicesDescriptor.find(descriptor => {
2981
+ const descriptorPath = descriptor.path?.trim();
2982
+ return (
2983
+ (!!expectedSerialNumber && descriptorPath === expectedSerialNumber) ||
2984
+ (!!expectedPath && descriptorPath === expectedPath)
2985
+ );
2986
+ });
2987
+ if (!reconnectDescriptor?.path) {
2988
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
2989
+ }
2990
+ this.device.updateDescriptor(
2991
+ {
2992
+ ...reconnectDescriptor,
2993
+ protocolType: PROTOCOL_V2_CONNECT_PROTOCOL,
2994
+ },
2995
+ true
2996
+ );
2997
+ const mainId = await this.device.deviceConnector?.acquire(
2998
+ reconnectDescriptor.path,
2999
+ null,
3000
+ undefined,
3001
+ PROTOCOL_V2_CONNECT_PROTOCOL,
3002
+ undefined,
3003
+ undefined,
3004
+ true
3005
+ );
3006
+ this.device.mainId = typeof mainId === 'string' ? mainId : reconnectDescriptor.path;
3007
+ this.device.commands.disposed = false;
3008
+ this.device.getCommands().mainId = this.device.mainId;
3009
+ return;
3010
+ }
3011
+
3012
+ if (isWebUsb && devicesDescriptor.length === 1) {
2889
3013
  const descriptor = devicesDescriptor[0];
2890
3014
  if (!this.protocolV2ExpectedSerialNumber && descriptor.path !== this.protocolV2ExpectedPath) {
2891
3015
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
@@ -3003,13 +3127,24 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
3003
3127
  await wait(2000);
3004
3128
  }
3005
3129
 
3006
- private async acquireProtocolV2BleDevice() {
3007
- await this.device.deviceConnector?.acquire(
3008
- this.device.originalDescriptor.id,
3130
+ private async acquireProtocolV2BleDevice(skipProtocolProbe = false) {
3131
+ const connector = this.device.deviceConnector;
3132
+ const expectedId = this.device.originalDescriptor.id;
3133
+ if (!skipProtocolProbe) {
3134
+ await connector?.acquire(expectedId, null, true, PROTOCOL_V2_CONNECT_PROTOCOL);
3135
+ return;
3136
+ }
3137
+ await connector?.acquire(
3138
+ expectedId,
3009
3139
  null,
3010
3140
  true,
3011
- PROTOCOL_V2_CONNECT_PROTOCOL
3141
+ PROTOCOL_V2_CONNECT_PROTOCOL,
3142
+ undefined,
3143
+ undefined,
3144
+ skipProtocolProbe
3012
3145
  );
3146
+ this.device.commands.disposed = false;
3147
+ this.device.getCommands().mainId = expectedId;
3013
3148
  }
3014
3149
 
3015
3150
  private async protocolV2StartFirmwareUpdate({
@@ -3017,30 +3152,72 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
3017
3152
  }: {
3018
3153
  targets: Array<{ target_id: number; path: string }>;
3019
3154
  }) {
3155
+ this.protocolV2LastRuntimeProbeFeatures = undefined;
3156
+ this.protocolV2InstallNeedsReconnect = false;
3157
+ this.protocolV2InstallTerminalSuccessObserved = false;
3020
3158
  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 }
3159
+ await commands.typedCall('DeviceFirmwareUpdateStage', 'Success', { targets });
3160
+ this.device.setCancelableAction(() => this.device.getCommands().cancelDevice());
3161
+ const interaction = this.device.createProtocolV2UiPhaseMetadata('button', 'start');
3162
+ this.postMessage(
3163
+ createUiMessage(UI_REQUEST.REQUEST_BUTTON, {
3164
+ device: this.device.toMessageObject() as KnownDevice,
3165
+ source: 'method-lifecycle',
3166
+ reason: 'firmware-update',
3167
+ completion: 'operation-completed',
3168
+ deviceOnly: true,
3169
+ operation: this.name,
3170
+ ...(interaction ? { interaction } : {}),
3171
+ })
3026
3172
  );
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;
3173
+
3174
+ try {
3175
+ await commands.call(
3176
+ 'DeviceFirmwareUpdateRequest',
3177
+ {},
3178
+ {
3179
+ returnAfterWrite: true,
3180
+ expectedTypes: ['Success'],
3181
+ onResponseAfterWrite: response => {
3182
+ if (response.type !== 'Success') return;
3183
+ this.protocolV2InstallTerminalSuccessObserved = true;
3184
+ this.device.clearCancelableAction();
3185
+ Log.log('[FirmwareUpdateV4] firmware install completed by device response');
3186
+ },
3187
+ }
3188
+ );
3189
+ Log.log('[FirmwareUpdateV4] firmware install request written; continue with status polling');
3190
+ } catch (error) {
3191
+ this.throwIfAborted();
3192
+ if (!isProtocolV2DeviceDisconnectedError(error)) {
3193
+ throw error;
3194
+ }
3195
+ Log.log(
3196
+ '[FirmwareUpdateV4] transport released while writing install request; continue status polling'
3197
+ );
3198
+ this.protocolV2InstallNeedsReconnect = true;
3199
+ }
3032
3200
  }
3033
3201
 
3034
3202
  private async protocolV2Reboot(rebootType: DeviceRebootType) {
3035
3203
  const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
3036
3204
  try {
3037
- const res = await typedCall('DeviceReboot', 'Success', {
3038
- reboot_type: rebootType,
3039
- });
3205
+ const res = await typedCall(
3206
+ 'DeviceReboot',
3207
+ 'Success',
3208
+ {
3209
+ reboot_type: rebootType,
3210
+ },
3211
+ { timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT }
3212
+ );
3040
3213
  this.device.markProtocolV2Reboot(rebootType);
3041
3214
  return res.message;
3042
3215
  } catch (error) {
3043
- if (isProtocolV2DeviceDisconnectedError(error) || isProtocolV2ReconnectProbeError(error)) {
3216
+ if (
3217
+ isProtocolV2DeviceDisconnectedError(error) ||
3218
+ isProtocolV2ReconnectProbeError(error) ||
3219
+ isProtocolV2ResponseTimeout(error)
3220
+ ) {
3044
3221
  this.device.markProtocolV2Reboot(rebootType);
3045
3222
  return { message: 'Device rebooted successfully' };
3046
3223
  }