@onekeyfe/hd-core 1.2.0-alpha.98 → 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 (204) 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 +89 -38
  5. package/__tests__/check-firmware-release-protocol-v2.test.ts +83 -14
  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 +14 -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 +128 -2
  21. package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +80 -0
  22. package/__tests__/firmware-update/firmware-update-plan.test.ts +114 -50
  23. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +19 -5
  24. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +116 -0
  25. package/__tests__/firmware-update/firmware-update-v4-install-poll.test.ts +678 -0
  26. package/__tests__/firmware-update/firmware-update-v4-parameters.test.ts +85 -0
  27. package/__tests__/get-device-state.test.ts +60 -21
  28. package/__tests__/logBlockEvent.test.ts +13 -1
  29. package/__tests__/open-wallet-session.test.ts +127 -1
  30. package/__tests__/protocol-v2-firmware-targets.test.ts +39 -0
  31. package/__tests__/protocol-v2-resources.test.ts +185 -88
  32. package/__tests__/protocol-v2-unlock-policy.test.ts +235 -2
  33. package/__tests__/protocol-v2.test.ts +2274 -466
  34. package/__tests__/refresh-device-state.test.ts +3 -1
  35. package/__tests__/resourceBase64Boundary.test.ts +48 -0
  36. package/__tests__/search-devices.test.ts +79 -32
  37. package/__tests__/ton-sign-message.test.ts +84 -0
  38. package/dist/api/BaseMethod.d.ts +2 -1
  39. package/dist/api/BaseMethod.d.ts.map +1 -1
  40. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  41. package/dist/api/CheckBootloaderRelease.d.ts.map +1 -1
  42. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  43. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  44. package/dist/api/FirmwareUpdateV4.d.ts +22 -7
  45. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  46. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  47. package/dist/api/SearchDevices.d.ts.map +1 -1
  48. package/dist/api/UploadPortfolio.d.ts +1 -1
  49. package/dist/api/UploadPortfolio.d.ts.map +1 -1
  50. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  51. package/dist/api/device/DeviceChangePin.d.ts.map +1 -1
  52. package/dist/api/device/DeviceRebootToBoardloader.d.ts.map +1 -1
  53. package/dist/api/device/DeviceRebootToBootloader.d.ts.map +1 -1
  54. package/dist/api/device/DeviceSettings.d.ts.map +1 -1
  55. package/dist/api/device/DeviceVerify.d.ts +1 -0
  56. package/dist/api/device/DeviceVerify.d.ts.map +1 -1
  57. package/dist/api/device/DeviceWipe.d.ts.map +1 -1
  58. package/dist/api/firmware/FirmwareArtifactSource.d.ts +5 -1
  59. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -1
  60. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -1
  61. package/dist/api/firmware/FirmwarePreparedResourceArchive.d.ts.map +1 -1
  62. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +2 -2
  63. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  64. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +0 -15
  65. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -1
  66. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +1 -0
  67. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -1
  68. package/dist/api/firmware/getBinary.d.ts +3 -2
  69. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  70. package/dist/api/firmware/protocolV2Release.d.ts.map +1 -1
  71. package/dist/api/helpers/base64Data.d.ts +16 -0
  72. package/dist/api/helpers/base64Data.d.ts.map +1 -0
  73. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts +3 -1
  74. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts.map +1 -1
  75. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts.map +1 -1
  76. package/dist/api/protocol-v2/DeviceInfoGet.d.ts +1 -1
  77. package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -1
  78. package/dist/api/protocol-v2/DeviceReboot.d.ts +1 -1
  79. package/dist/api/protocol-v2/DeviceReboot.d.ts.map +1 -1
  80. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +2 -3
  81. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -1
  82. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts +2 -3
  83. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -1
  84. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  85. package/dist/api/ton/TonSignMessage.d.ts.map +1 -1
  86. package/dist/core/index.d.ts +6 -1
  87. package/dist/core/index.d.ts.map +1 -1
  88. package/dist/data-manager/DataManager.d.ts +4 -3
  89. package/dist/data-manager/DataManager.d.ts.map +1 -1
  90. package/dist/data-manager/TransportManager.d.ts.map +1 -1
  91. package/dist/device/Device.d.ts +16 -3
  92. package/dist/device/Device.d.ts.map +1 -1
  93. package/dist/device/DeviceCommands.d.ts +4 -4
  94. package/dist/device/DeviceCommands.d.ts.map +1 -1
  95. package/dist/device/DeviceConnector.d.ts +1 -1
  96. package/dist/device/DeviceConnector.d.ts.map +1 -1
  97. package/dist/device/DevicePool.d.ts.map +1 -1
  98. package/dist/device/DeviceStateMapper.d.ts.map +1 -1
  99. package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
  100. package/dist/events/logBlockEvent.d.ts.map +1 -1
  101. package/dist/events/ui-request.d.ts +3 -0
  102. package/dist/events/ui-request.d.ts.map +1 -1
  103. package/dist/index.d.ts +54 -84
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js +2333 -1515
  106. package/dist/protocols/protocol-v2/features.d.ts +13 -5
  107. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  108. package/dist/protocols/protocol-v2/index.d.ts +2 -2
  109. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  110. package/dist/protocols/protocol-v2/resources.d.ts +12 -7
  111. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  112. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +3 -1
  113. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -1
  114. package/dist/protocols/protocol-v2/walletSession.d.ts +3 -0
  115. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  116. package/dist/types/api/firmwareUpdatePlan.d.ts +2 -2
  117. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -1
  118. package/dist/types/api/protocolV2.d.ts +1 -1
  119. package/dist/types/api/protocolV2.d.ts.map +1 -1
  120. package/dist/types/device.d.ts.map +1 -1
  121. package/dist/types/settings.d.ts +3 -31
  122. package/dist/types/settings.d.ts.map +1 -1
  123. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  124. package/dist/utils/deviceFeaturesUtils.d.ts +3 -0
  125. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  126. package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
  127. package/dist/utils/deviceSettings.d.ts +1 -0
  128. package/dist/utils/deviceSettings.d.ts.map +1 -1
  129. package/dist/utils/index.d.ts +1 -1
  130. package/dist/utils/index.d.ts.map +1 -1
  131. package/dist/utils/patch.d.ts +1 -1
  132. package/dist/utils/patch.d.ts.map +1 -1
  133. package/dist/utils/pro2Nft.d.ts +7 -0
  134. package/dist/utils/pro2Nft.d.ts.map +1 -1
  135. package/package.json +6 -4
  136. package/src/api/BaseMethod.ts +8 -1
  137. package/src/api/CheckAllFirmwareRelease.ts +8 -5
  138. package/src/api/CheckBLEFirmwareRelease.ts +2 -2
  139. package/src/api/CheckBootloaderRelease.ts +3 -2
  140. package/src/api/CheckFirmwareRelease.ts +2 -2
  141. package/src/api/FirmwareUpdateV2.ts +130 -69
  142. package/src/api/FirmwareUpdateV3.ts +29 -19
  143. package/src/api/FirmwareUpdateV4.ts +966 -743
  144. package/src/api/OpenWalletSession.ts +39 -9
  145. package/src/api/PromptWebDeviceAccess.ts +2 -2
  146. package/src/api/SearchDevices.ts +6 -3
  147. package/src/api/UploadPortfolio.ts +9 -2
  148. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +2 -1
  149. package/src/api/device/DeviceChangePin.ts +4 -1
  150. package/src/api/device/DeviceRebootToBoardloader.ts +3 -1
  151. package/src/api/device/DeviceRebootToBootloader.ts +3 -1
  152. package/src/api/device/DeviceSettings.ts +34 -20
  153. package/src/api/device/DeviceVerify.ts +25 -0
  154. package/src/api/device/DeviceWipe.ts +4 -1
  155. package/src/api/firmware/FirmwareArtifactSource.ts +41 -14
  156. package/src/api/firmware/FirmwareHostBinding.ts +10 -0
  157. package/src/api/firmware/FirmwarePreparedResourceArchive.ts +16 -27
  158. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +131 -76
  159. package/src/api/firmware/FirmwareUpdatePlan.ts +55 -82
  160. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +32 -15
  161. package/src/api/firmware/protocolV2Release.ts +7 -2
  162. package/src/api/helpers/base64Data.ts +85 -0
  163. package/src/api/protocol-v2/DeviceFactoryInfoSet.ts +1 -1
  164. package/src/api/protocol-v2/DeviceFirmwareUpdate.ts +6 -28
  165. package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +7 -1
  166. package/src/api/protocol-v2/DeviceInfoGet.ts +3 -3
  167. package/src/api/protocol-v2/DeviceReboot.ts +5 -0
  168. package/src/api/protocol-v2/DeviceUploadNft.ts +61 -9
  169. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +53 -19
  170. package/src/api/protocol-v2/helpers.ts +8 -0
  171. package/src/api/ton/TonSignMessage.ts +5 -3
  172. package/src/core/index.ts +264 -55
  173. package/src/data/messages/messages-protocol-v2.json +140 -13
  174. package/src/data-manager/DataManager.ts +46 -39
  175. package/src/data-manager/TransportManager.ts +3 -1
  176. package/src/device/Device.ts +210 -44
  177. package/src/device/DeviceCommands.ts +27 -18
  178. package/src/device/DeviceConnector.ts +17 -3
  179. package/src/device/DevicePool.ts +9 -2
  180. package/src/device/DeviceStateMapper.ts +30 -24
  181. package/src/deviceProfile/buildDeviceFeatures.ts +10 -5
  182. package/src/events/logBlockEvent.ts +14 -1
  183. package/src/events/ui-request.ts +3 -0
  184. package/src/index.ts +0 -10
  185. package/src/protocols/protocol-v2/features.ts +22 -5
  186. package/src/protocols/protocol-v2/index.ts +2 -0
  187. package/src/protocols/protocol-v2/resources.ts +110 -147
  188. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +13 -1
  189. package/src/protocols/protocol-v2/walletSession.ts +65 -22
  190. package/src/types/api/firmwareUpdate.ts +1 -1
  191. package/src/types/api/firmwareUpdatePlan.ts +2 -2
  192. package/src/types/api/protocolV2.ts +1 -1
  193. package/src/types/device.ts +2 -1
  194. package/src/types/settings.ts +6 -34
  195. package/src/utils/deviceFeaturesCompat.ts +9 -4
  196. package/src/utils/deviceFeaturesUtils.ts +4 -0
  197. package/src/utils/deviceInfoUtils.ts +9 -2
  198. package/src/utils/deviceSettings.ts +11 -4
  199. package/src/utils/index.ts +1 -0
  200. package/src/utils/pro2Nft.ts +31 -8
  201. package/__tests__/firmware-memory-host.test.ts +0 -112
  202. package/dist/api/firmware/FirmwareMemoryHost.d.ts +0 -22
  203. package/dist/api/firmware/FirmwareMemoryHost.d.ts.map +0 -1
  204. package/src/api/firmware/FirmwareMemoryHost.ts +0 -142
@@ -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
 
@@ -138,13 +139,31 @@ const getProtocolV2ZipEntrySizes = (entry: JSZip.JSZipObject) => {
138
139
 
139
140
  export function assertProtocolV2FirmwareTargetsSupported(
140
141
  deviceType: EDeviceType | string | undefined,
141
- params: FirmwareUpdateV4Params
142
+ params: FirmwareUpdateV4Params,
143
+ hasExplicitTargetSelection = false
142
144
  ) {
145
+ const requestedTargets = new Set(params.targetsToUpdate ?? []);
143
146
  const unsupportedTargets = new Set(
144
- (params.targetsToUpdate ?? []).filter(target => PROTOCOL_V2_NEO_UNSUPPORTED_TARGETS.has(target))
147
+ Array.from(requestedTargets).filter(target => PROTOCOL_V2_NEO_UNSUPPORTED_TARGETS.has(target))
145
148
  );
146
- if (params.se03Binary) unsupportedTargets.add('se03');
147
- if (params.se04Binary) unsupportedTargets.add('se04');
149
+ if (params.se03Binary && (!hasExplicitTargetSelection || requestedTargets.has('se03'))) {
150
+ unsupportedTargets.add('se03');
151
+ }
152
+ if (params.se04Binary && (!hasExplicitTargetSelection || requestedTargets.has('se04'))) {
153
+ unsupportedTargets.add('se04');
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
+ }
148
167
 
149
168
  if (!unsupportedTargets.size || deviceType === EDeviceType.Pro2) return;
150
169
 
@@ -162,26 +181,26 @@ const getProtocolV2DeviceTransferProgress = (
162
181
  totalBytes: number
163
182
  ) => {
164
183
  if (!Number.isFinite(totalBytes) || totalBytes <= 0) {
165
- return 100;
184
+ return 0;
166
185
  }
167
186
  if (bytesBeforeChunk <= 0 && bytesAfterChunk < totalBytes) {
168
187
  return 0;
169
188
  }
170
- if (bytesAfterChunk >= totalBytes) {
171
- return 100;
172
- }
173
189
  return Math.min(Math.max(Math.ceil((bytesAfterChunk / totalBytes) * 100), 1), 99);
174
190
  };
175
191
 
176
192
  type ProtocolV2FirmwareUpdateStatusTarget = {
177
193
  target_id: number | string;
178
194
  status?: number | string;
195
+ progress_percent?: number;
196
+ phase_info?: {
197
+ phase: number | string;
198
+ progress_percent: number;
199
+ };
179
200
  payload_version?: number;
180
201
  path?: string;
181
202
  };
182
203
 
183
- type ProtocolV2FirmwareUpdateStartResponse = TypedResponseMessage<'Success'>;
184
-
185
204
  type ProtocolV2TargetBinary = { fileName: string; binary: ArrayBuffer; targetId: number };
186
205
  type ProtocolV2InstallItem = ProtocolV2TargetBinary & {
187
206
  kind: ProtocolV2RemoteComponentTarget['kind'];
@@ -215,7 +234,11 @@ type ProtocolV2ResourceBundleSource = {
215
234
 
216
235
  type ProtocolV2LocalResourceArchive = {
217
236
  binary: ArrayBuffer;
218
- materializedEntries: FirmwareMemoryArtifactEntry[];
237
+ resources: Array<{
238
+ entryName: string;
239
+ binary: ArrayBuffer;
240
+ header: ProtocolV2ResourcePackageHeader;
241
+ }>;
219
242
  };
220
243
 
221
244
  type JSZipSizedEntry = JSZip.JSZipObject & {
@@ -225,26 +248,11 @@ type JSZipSizedEntry = JSZip.JSZipObject & {
225
248
  };
226
249
  };
227
250
 
228
- type ProtocolV2ExecutionPhaseKind =
229
- | 'resource-sync'
230
- | 'bootloader-install'
231
- | 'bootloader-verify'
232
- | 'component-install'
233
- | 'final-verify';
234
-
235
- type ProtocolV2ExecutionPhase = {
236
- kind: ProtocolV2ExecutionPhaseKind;
251
+ type ProtocolV2TransferBatch = {
237
252
  installSources: ProtocolV2InstallSource[];
238
253
  resourceSources: ProtocolV2ResourceBundleSource[];
239
254
  };
240
255
 
241
- type ProtocolV2OkppHeader = {
242
- type: string;
243
- version: IVersionArray;
244
- payloadHash: string;
245
- headerHash: string;
246
- };
247
-
248
256
  const PROTOCOL_V2_REMOTE_COMPONENT_TARGETS: Readonly<
249
257
  Record<string, ProtocolV2RemoteComponentTarget>
250
258
  > = {
@@ -296,18 +304,9 @@ const PROTOCOL_V2_FIRMWARE_STAGING_PATHS = new Set(
296
304
  )
297
305
  );
298
306
 
299
- const PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH = 'vol0:/loaders/bootloader/boot_resource.okpkg';
300
- const PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH = `${PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH}.staging`;
301
-
302
307
  const isProtocolV2BootResourcePackagePath = (devicePath: string) =>
303
308
  typeof devicePath === 'string' &&
304
- devicePath.replace(/^vol0:(?!\/)/i, 'vol0:/').toLowerCase() ===
305
- PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH;
306
-
307
- const resolveProtocolV2ResourceWritePath = (devicePath: string) =>
308
- isProtocolV2BootResourcePackagePath(devicePath)
309
- ? PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH
310
- : devicePath;
309
+ devicePath.toLowerCase() === PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_STAGING_PATH;
311
310
 
312
311
  const PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID = new Map<
313
312
  number,
@@ -376,10 +375,12 @@ const isProtocolV2FirmwareStatusEndpointUnavailable = (error: unknown) => {
376
375
  };
377
376
 
378
377
  const isProtocolV2TerminalInstallStatusError = (error: unknown) =>
379
- error instanceof HardwareError &&
380
- (error.errorCode === HardwareErrorCode.FirmwareError ||
381
- error.errorCode === HardwareErrorCode.FirmwareVerificationFailed ||
382
- 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));
383
384
 
384
385
  const isProtocolV2TargetStatusFinished = (status: ProtocolV2FirmwareUpdateStatusTarget['status']) =>
385
386
  normalizeProtocolV2TargetStatus(status) === PROTOCOL_V2_TARGET_STATUS_FINISHED;
@@ -416,6 +417,18 @@ const normalizeProtocolV2TargetStatus = (
416
417
  return undefined;
417
418
  };
418
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
+
419
432
  const normalizeProtocolV2Hex = (value?: string) => value?.replace(/^0x/i, '').toLowerCase();
420
433
 
421
434
  const versionArrayToNumber = (version?: IVersionArray) => {
@@ -473,40 +486,9 @@ const toProtocolV2FiniteNumber = (value: unknown) => {
473
486
  return undefined;
474
487
  };
475
488
 
476
- const readProtocolV2Ascii = (bytes: Uint8Array, offset: number, length: number) =>
477
- Array.from(bytes.slice(offset, offset + length))
478
- .map(byte => String.fromCharCode(byte))
479
- .join('');
480
-
481
- const parseProtocolV2OkppHeader = (bytes: Uint8Array): ProtocolV2OkppHeader | null => {
482
- if (bytes.byteLength < PROTOCOL_V2_OKPP_HEADER_SIZE) return null;
483
- if (readProtocolV2Ascii(bytes, 0, 4) !== 'OKPP') return null;
484
-
485
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
486
- const headerLen = view.getUint32(0x0c, true);
487
- if (headerLen !== PROTOCOL_V2_OKPP_HEADER_SIZE) return null;
488
-
489
- const packedVersion = view.getUint32(0x10, true);
490
- return {
491
- type: readProtocolV2Ascii(bytes, 0x08, 4),
492
- version: [
493
- Math.floor(packedVersion / 0x10000) % 0x100,
494
- Math.floor(packedVersion / 0x100) % 0x100,
495
- packedVersion % 0x100,
496
- ],
497
- payloadHash: bytesToHex(
498
- bytes.slice(
499
- PROTOCOL_V2_OKPP_PAYLOAD_HASH_OFFSET,
500
- PROTOCOL_V2_OKPP_PAYLOAD_HASH_OFFSET + PROTOCOL_V2_OKPP_HASH_SIZE
501
- )
502
- ),
503
- headerHash: bytesToHex(
504
- bytes.slice(
505
- PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET,
506
- PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET + PROTOCOL_V2_OKPP_HASH_SIZE
507
- )
508
- ),
509
- };
489
+ const normalizeProtocolV2ProgressPercent = (value: unknown) => {
490
+ const progress = toProtocolV2FiniteNumber(value);
491
+ return progress === undefined ? undefined : Math.min(100, Math.max(0, progress));
510
492
  };
511
493
 
512
494
  export const isProtocolV2FirmwareFingerprintValid = (
@@ -551,7 +533,7 @@ export const assertProtocolV2ReconnectIdentity = (
551
533
  *
552
534
  * It intentionally does not fall back to FirmwareUpdateV3/V1 behavior:
553
535
  * - upload uses FilesystemFileWrite
554
- * - install uses DeviceFirmwareUpdateRequest
536
+ * - install uses DeviceFirmwareUpdateStage followed by an empty DeviceFirmwareUpdateRequest
555
537
  * - completion waits for target status to finish, reboots to normal, then polls DeviceInfo
556
538
  */
557
539
  export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareUpdateV4Params> {
@@ -559,6 +541,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
559
541
 
560
542
  private protocolV2ExpectedPath?: string;
561
543
 
544
+ private protocolV2HasExplicitTargetSelection = false;
545
+
562
546
  getSupportedProtocols() {
563
547
  return ['V2'] as const;
564
548
  }
@@ -571,19 +555,56 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
571
555
 
572
556
  private protocolV2CompletedTargetVersions = new Map<number, number>();
573
557
 
558
+ private protocolV2CompletedTargetIds = new Set<number>();
559
+
574
560
  private protocolV2LatestFinalFeatures?: Features;
575
561
 
576
- 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;
577
575
 
578
576
  init() {
579
577
  this.allowDeviceMode = [UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE];
580
578
  this.requireDeviceMode = [];
581
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;
582
583
  this.useDevicePassphraseState = false;
583
584
  this.skipForceUpdateCheck = true;
584
585
 
585
586
  const { payload } = this;
586
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
+
606
+ this.protocolV2HasExplicitTargetSelection = payload.targetsToUpdate !== undefined;
607
+
587
608
  if (typeof payload.retryCount !== 'number') {
588
609
  payload.retryCount = PROTOCOL_V2_CONNECT_RETRY_COUNT;
589
610
  }
@@ -651,10 +672,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
651
672
  payload.hostBindingGeneration,
652
673
  preparedPlan.preparedPlanDigest
653
674
  ).artifactReader;
654
- } else if (payload.hostBindingGeneration !== undefined) {
655
- artifactReader = resolveFirmwareUpdateHostBinding(
656
- payload.hostBindingGeneration
657
- ).artifactReader;
658
675
  }
659
676
  if (preparedPlan) {
660
677
  assertFirmwareUpdatePreparedPlanBinding({
@@ -777,30 +794,30 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
777
794
  private async runProtocolV2() {
778
795
  await this.captureProtocolV2PhysicalIdentity();
779
796
  const deviceFeatures = await this.getProtocolV2DeviceFeatures();
797
+ this.protocolV2InstallBaselineVersions =
798
+ this.getProtocolV2ObservableTargetVersions(deviceFeatures);
799
+ this.protocolV2LastRuntimeProbeFeatures = undefined;
780
800
  const currentDeviceType = this.device.getCurrentDeviceType();
781
801
  const capabilityDeviceType =
782
802
  currentDeviceType === EDeviceType.Pro2 || currentDeviceType === EDeviceType.Neo
783
803
  ? currentDeviceType
784
804
  : getDeviceType(deviceFeatures);
785
- assertProtocolV2FirmwareTargetsSupported(capabilityDeviceType, this.params);
805
+ assertProtocolV2FirmwareTargetsSupported(
806
+ capabilityDeviceType,
807
+ this.params,
808
+ this.protocolV2HasExplicitTargetSelection
809
+ );
786
810
  const deviceFirmwareType = getFirmwareType(deviceFeatures);
787
811
  const firmwareType = this.params.firmwareType ?? deviceFirmwareType;
788
812
  this.validateExpectedTargetVersions();
813
+ const wantsResources = !!this.params.targetsToUpdate?.includes('resource');
789
814
 
790
815
  if (
791
816
  !this.params.preparedPlan &&
792
817
  this.params.resourceArchiveBinary &&
793
818
  this.params.targetsToUpdate?.includes('resource')
794
819
  ) {
795
- const localMemoryHost = await this.prepareProtocolV2LocalMemoryHost({
796
- features: deviceFeatures,
797
- firmwareType,
798
- });
799
- try {
800
- return await this.runProtocolV2PreparedArtifacts(deviceFeatures, firmwareType);
801
- } finally {
802
- localMemoryHost.release();
803
- }
820
+ return this.runProtocolV2DirectArtifacts({});
804
821
  }
805
822
 
806
823
  const hasPreparedComponentArtifacts = Object.values(this.params.componentArtifacts ?? {}).some(
@@ -809,16 +826,16 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
809
826
  if (this.params.preparedPlan || hasPreparedComponentArtifacts) {
810
827
  return this.runProtocolV2PreparedArtifacts(deviceFeatures, firmwareType);
811
828
  }
812
- const wantsResources = !!this.params.targetsToUpdate?.includes('resource');
813
829
 
814
830
  let fwBinaryMap: ProtocolV2TargetBinary[] = [];
815
831
  let bootloaderBinary: ArrayBuffer | null = null;
816
832
  let installItems: ProtocolV2InstallItem[] | undefined;
833
+ let explicitInstallItems: ProtocolV2InstallItem[] | undefined;
817
834
  try {
818
835
  this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
819
836
  fwBinaryMap = this.collectExplicitTargetBinaries();
820
837
  bootloaderBinary = this.prepareBootloaderBinary();
821
- const explicitInstallItems = this.buildProtocolV2InstallItems({
838
+ explicitInstallItems = this.buildProtocolV2InstallItems({
822
839
  bootloaderBinary,
823
840
  fwBinaryMap,
824
841
  });
@@ -826,17 +843,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
826
843
  const needsRemoteFirmware = this.params.targetsToUpdate?.length
827
844
  ? missingFirmwareTargets.length > 0
828
845
  : explicitInstallItems.length === 0;
829
- if (wantsResources) {
830
- throw ERRORS.TypedError(
831
- HardwareErrorCode.RuntimeError,
832
- 'Protocol V2 resource archive must be provided through a local or external PreparedPlan',
833
- {
834
- firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
835
- }
836
- );
837
- }
846
+ const needsSdkManagedArtifacts = needsRemoteFirmware || wantsResources;
838
847
  if (
839
- needsRemoteFirmware &&
848
+ needsSdkManagedArtifacts &&
840
849
  (this.params.artifactReader ||
841
850
  DataManager.getSettings('firmwareManifestMode') === 'external-only')
842
851
  ) {
@@ -848,9 +857,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
848
857
  }
849
858
  );
850
859
  }
851
- if (needsRemoteFirmware) {
860
+ if (needsSdkManagedArtifacts) {
852
861
  // Remote updates must use a freshly fetched config before any reboot or file write.
853
- await DataManager.forceReloadData();
862
+ await DataManager.forceReloadData({
863
+ requireResources: wantsResources,
864
+ resourceDeviceType:
865
+ capabilityDeviceType === EDeviceType.Neo ? EDeviceType.Neo : EDeviceType.Pro2,
866
+ });
854
867
  }
855
868
  if (needsRemoteFirmware) {
856
869
  const remoteBinaries = await this.prepareRemoteProtocolV2Binaries(
@@ -861,6 +874,23 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
861
874
  bootloaderBinary = remoteBinaries.bootloaderBinary;
862
875
  fwBinaryMap = remoteBinaries.fwBinaryMap;
863
876
  installItems = remoteBinaries.installItems;
877
+ } else {
878
+ const selectedInstallItems = this.filterProtocolV2LocalInstallItems(explicitInstallItems);
879
+ bootloaderBinary =
880
+ selectedInstallItems.find(item => item.kind === 'bootloader')?.binary ?? null;
881
+ fwBinaryMap = selectedInstallItems
882
+ .filter(item => item.kind === 'firmware')
883
+ .map(item => ({
884
+ fileName: item.fileName,
885
+ binary: item.binary,
886
+ targetId: item.targetId,
887
+ }));
888
+ }
889
+ if (wantsResources) {
890
+ this.params.resourceArchiveBinary = await this.downloadRemoteProtocolV2ResourceArchive(
891
+ deviceFeatures,
892
+ firmwareType
893
+ );
864
894
  }
865
895
  this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
866
896
  } catch (err) {
@@ -878,6 +908,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
878
908
  throw normalizeFirmwarePreparationError(err);
879
909
  }
880
910
 
911
+ if (wantsResources && this.params.resourceArchiveBinary) {
912
+ return this.runProtocolV2DirectArtifacts({
913
+ availableInstallItems: installItems ?? explicitInstallItems,
914
+ announceDownload: false,
915
+ });
916
+ }
917
+
881
918
  if (!bootloaderBinary && fwBinaryMap.length === 0 && !installItems?.length) {
882
919
  throw ERRORS.TypedError(
883
920
  HardwareErrorCode.FirmwareUpdateDownloadFailed,
@@ -1028,131 +1065,69 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1028
1065
  return installSources;
1029
1066
  }
1030
1067
 
1031
- private async prepareProtocolV2LocalMemoryHost({
1032
- features,
1033
- firmwareType,
1068
+ private async runProtocolV2DirectArtifacts({
1069
+ availableInstallItems,
1070
+ announceDownload = true,
1034
1071
  }: {
1035
- features: Features;
1036
- firmwareType: EFirmwareType;
1037
- }): Promise<FirmwareUpdateV4MemoryHost> {
1038
- const installItems = this.buildProtocolV2InstallItems({
1039
- bootloaderBinary: this.prepareBootloaderBinary(),
1040
- fwBinaryMap: this.collectExplicitTargetBinaries(),
1041
- });
1042
- const requestedComponentTargets = new Set(
1043
- (this.params.targetsToUpdate ?? []).filter(
1044
- (target): target is Exclude<FirmwareUpdateV4Target, 'resource' | 'boot_resources'> =>
1045
- target !== 'resource' && target !== 'boot_resources'
1046
- )
1047
- );
1048
- const localComponentTargets = new Set(
1049
- installItems.flatMap(item => {
1050
- const target = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(item.targetId);
1051
- return target ? [target] : [];
1052
- })
1053
- );
1054
- const missingTarget = Array.from(requestedComponentTargets).find(
1055
- target => !localComponentTargets.has(target)
1056
- );
1057
- if (missingTarget) {
1058
- throw ERRORS.TypedError(
1059
- HardwareErrorCode.RuntimeError,
1060
- `Protocol V2 local update has no binary for requested target ${missingTarget}`,
1061
- {
1062
- firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
1063
- artifactName: missingTarget,
1064
- }
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
+ })
1065
1085
  );
1066
- }
1067
-
1068
- const planArtifacts: Parameters<typeof buildProtocolV2LocalFirmwareUpdatePlan>[0]['artifacts'] =
1069
- [];
1070
- const memoryArtifacts: FirmwareMemoryArtifact[] = [];
1071
- for (const item of installItems) {
1072
- const target = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(item.targetId);
1073
- 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) {
1074
1098
  throw ERRORS.TypedError(
1075
- HardwareErrorCode.RuntimeError,
1076
- `Protocol V2 local firmware artifact is invalid: ${item.fileName}`,
1077
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1099
+ HardwareErrorCode.FirmwareUpdateDownloadFailed,
1100
+ 'No firmware to update'
1078
1101
  );
1079
1102
  }
1080
- const artifactId = `component:${target}`;
1081
- planArtifacts.push({
1082
- artifactId,
1083
- target,
1084
- container: 'raw',
1085
- logicalName: item.fileName,
1086
- expectedSize: item.binary.byteLength,
1087
- expectedSha256: bytesToHex(sha256(new Uint8Array(item.binary))),
1088
- ...(this.params.expectedTargetVersions?.[target]
1089
- ? { targetVersion: this.params.expectedTargetVersions[target] }
1090
- : {}),
1103
+ if (announceDownload) {
1104
+ this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
1105
+ }
1106
+ return await this.executeProtocolV2SourceUpdate({
1107
+ installSources,
1108
+ resourceSources,
1091
1109
  });
1092
- memoryArtifacts.push({ artifactId, binary: item.binary });
1110
+ } finally {
1111
+ await this.closeProtocolV2PreparedSources();
1093
1112
  }
1113
+ }
1094
1114
 
1095
- const resourceArchive = await this.prepareProtocolV2LocalResourceArchive(
1096
- this.params.resourceArchiveBinary as ArrayBuffer
1097
- );
1098
- const resourceArtifactId = 'resource:archive';
1099
- planArtifacts.push({
1100
- artifactId: resourceArtifactId,
1101
- target: 'resource',
1102
- container: 'zip',
1103
- logicalName: 'protocol-v2-local-resource-archive',
1104
- expectedSize: resourceArchive.binary.byteLength,
1105
- expectedSha256: bytesToHex(sha256(new Uint8Array(resourceArchive.binary))),
1106
- });
1107
- memoryArtifacts.push({
1108
- artifactId: resourceArtifactId,
1109
- binary: resourceArchive.binary,
1110
- materializedEntries: resourceArchive.materializedEntries,
1111
- });
1112
-
1113
- const plan = buildProtocolV2LocalFirmwareUpdatePlan({
1114
- features,
1115
- firmwareType,
1116
- platform: this.params.platform,
1117
- artifacts: planArtifacts,
1118
- });
1119
- let memoryHost: FirmwareUpdateV4MemoryHost | undefined;
1120
- try {
1121
- memoryHost = prepareFirmwareUpdateV4MemoryHost({
1122
- sdk: {
1123
- prepareFirmwareUpdatePlan,
1124
- registerFirmwareUpdateHostBinding,
1125
- unregisterFirmwareUpdateHostBinding,
1126
- },
1127
- plan,
1128
- artifacts: memoryArtifacts,
1129
- });
1130
- const preparedPlan = validateFirmwareUpdatePreparedPlan(memoryHost.preparedPlan);
1131
- assertFirmwareUpdatePreparedPlanBinding({
1132
- preparedPlan,
1133
- executor: 'v4',
1134
- platform: this.params.platform,
1135
- scopeTargets: [],
1136
- bindings: [],
1137
- });
1138
- assertFirmwareUpdatePreparedPlanDeviceIdentity({
1139
- preparedPlan,
1140
- deviceIdentity: this.protocolV2ExpectedSerialNumber,
1141
- 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,
1142
1128
  });
1143
- const hostBinding = resolveFirmwareUpdateHostBinding(
1144
- memoryHost.hostBindingGeneration,
1145
- preparedPlan.preparedPlanDigest
1146
- );
1147
- this.params.preparedPlan = preparedPlan;
1148
- this.params.targetsToUpdate = [...preparedPlan.targetsToUpdate] as FirmwareUpdateV4Target[];
1149
- this.params.artifactReader = hostBinding.artifactReader;
1150
- this.params.componentArtifacts = undefined;
1151
- return memoryHost;
1152
- } catch (error) {
1153
- memoryHost?.release();
1154
- throw error;
1155
1129
  }
1130
+ return sources;
1156
1131
  }
1157
1132
 
1158
1133
  private async prepareProtocolV2LocalResourceArchive(
@@ -1165,132 +1140,78 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1165
1140
  { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1166
1141
  );
1167
1142
  }
1168
- const zip = await JSZip.loadAsync(binary);
1169
- const zipEntries = Object.values(zip.files);
1170
- if (
1171
- zipEntries.some(entry => entry.unsafeOriginalName && entry.unsafeOriginalName !== entry.name)
1172
- ) {
1143
+ let zip: JSZip;
1144
+ try {
1145
+ zip = await JSZip.loadAsync(binary);
1146
+ } catch {
1173
1147
  throw ERRORS.TypedError(
1174
1148
  HardwareErrorCode.RuntimeError,
1175
- 'Protocol V2 local resource ZIP contains an unsafe entry path',
1149
+ 'Protocol V2 resource ZIP cannot be parsed',
1176
1150
  { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1177
1151
  );
1178
1152
  }
1179
- const entries = zipEntries.filter(entry => !entry.dir);
1180
- if (entries.length === 0 || entries.length > PROTOCOL_V2_RESOURCE_FILE_MAX_COUNT + 1) {
1153
+ const zipEntries = Object.values(zip.files);
1154
+ const resourceEntries = zipEntries.filter(
1155
+ entry => !entry.dir && isProtocolV2ResourceArchiveEntryName(entry.name)
1156
+ );
1157
+ if (
1158
+ resourceEntries.length === 0 ||
1159
+ resourceEntries.length > PROTOCOL_V2_RESOURCE_FILE_MAX_COUNT
1160
+ ) {
1181
1161
  throw ERRORS.TypedError(
1182
1162
  HardwareErrorCode.RuntimeError,
1183
- 'Protocol V2 local resource ZIP entry set is invalid',
1163
+ 'Protocol V2 resource ZIP has no valid resource package set',
1184
1164
  { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1185
1165
  );
1186
1166
  }
1187
- let declaredUncompressedSize = 0;
1188
- let declaredCompressedSize = 0;
1189
- for (const entry of entries) {
1190
- const sizes = getProtocolV2ZipEntrySizes(entry);
1191
- declaredCompressedSize += sizes.compressedSize;
1192
- declaredUncompressedSize += sizes.uncompressedSize;
1193
- const entryLimit =
1194
- entry.name === 'manifest.json'
1195
- ? PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
1196
- : 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;
1197
1174
  if (
1198
- sizes.uncompressedSize > entryLimit ||
1199
- declaredCompressedSize > binary.byteLength ||
1200
- declaredUncompressedSize >
1201
- 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
1202
1178
  ) {
1203
1179
  throw ERRORS.TypedError(
1204
1180
  HardwareErrorCode.RuntimeError,
1205
- 'Protocol V2 local resource ZIP declared size exceeds the allowed limit',
1181
+ `Protocol V2 resource package size is invalid: ${entry.name}`,
1206
1182
  { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1207
1183
  );
1208
1184
  }
1209
- }
1210
- const manifestEntry = zip.file('manifest.json');
1211
- if (!manifestEntry) {
1212
- throw ERRORS.TypedError(
1213
- HardwareErrorCode.RuntimeError,
1214
- 'Protocol V2 local resource ZIP has no manifest.json',
1215
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1216
- );
1217
- }
1218
- const manifestBinary = await manifestEntry.async('arraybuffer');
1219
- if (
1220
- manifestBinary.byteLength <= 0 ||
1221
- manifestBinary.byteLength > PROTOCOL_V2_RESOURCE_MANIFEST_MAX_BYTES
1222
- ) {
1223
- throw ERRORS.TypedError(
1224
- HardwareErrorCode.RuntimeError,
1225
- 'Protocol V2 local resource manifest size is invalid',
1226
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1227
- );
1228
- }
1229
-
1230
- let manifestValue: unknown;
1231
- try {
1232
- manifestValue = JSON.parse(new TextDecoder().decode(manifestBinary));
1233
- } catch (error) {
1234
- throw ERRORS.TypedError(
1235
- HardwareErrorCode.RuntimeError,
1236
- `Protocol V2 local resource manifest is invalid: ${String(error)}`,
1237
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1238
- );
1239
- }
1240
- const manifest = parseProtocolV2ResourceManifest(manifestValue);
1241
- const selectedFiles = selectProtocolV2ResourceManifestFiles({
1242
- manifest,
1243
- targetsToUpdate: this.params.targetsToUpdate ?? [],
1244
- });
1245
- const expectedEntryNames = new Set([
1246
- 'manifest.json',
1247
- ...selectedFiles.map(file => file.archive_path),
1248
- ]);
1249
- if (
1250
- entries.length !== expectedEntryNames.size ||
1251
- entries.some(entry => !expectedEntryNames.has(entry.name))
1252
- ) {
1253
- throw ERRORS.TypedError(
1254
- HardwareErrorCode.RuntimeError,
1255
- 'Protocol V2 local resource ZIP contains an unexpected entry',
1256
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1257
- );
1258
- }
1259
-
1260
- let totalSize = 0;
1261
- const materializedEntries: FirmwareMemoryArtifactEntry[] = [
1262
- { entryName: 'manifest.json', binary: manifestBinary },
1263
- ];
1264
- for (const file of selectedFiles) {
1265
- const entry = zip.file(file.archive_path);
1266
- if (!entry) {
1185
+ const fileBinary = await entry.async('arraybuffer');
1186
+ if (fileBinary.byteLength !== uncompressedSize) {
1267
1187
  throw ERRORS.TypedError(
1268
1188
  HardwareErrorCode.RuntimeError,
1269
- `Protocol V2 local resource ZIP is missing ${file.archive_path}`,
1270
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1189
+ `Protocol V2 resource package is incomplete: ${entry.name}`,
1190
+ { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
1271
1191
  );
1272
1192
  }
1273
- const { uncompressedSize } = getProtocolV2ZipEntrySizes(entry);
1274
- totalSize += uncompressedSize;
1275
- if (uncompressedSize !== file.size || totalSize > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES) {
1193
+ let header: ProtocolV2ResourcePackageHeader;
1194
+ try {
1195
+ header = parseProtocolV2ResourcePackage(fileBinary);
1196
+ } catch (error) {
1276
1197
  throw ERRORS.TypedError(
1277
1198
  HardwareErrorCode.RuntimeError,
1278
- `Protocol V2 local resource file declared size is invalid: ${file.archive_path}`,
1199
+ `Protocol V2 resource package is invalid: ${entry.name}: ${String(error)}`,
1279
1200
  { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1280
1201
  );
1281
1202
  }
1282
- const fileBinary = await entry.async('arraybuffer');
1283
- const digest = bytesToHex(sha256(new Uint8Array(fileBinary)));
1284
- if (fileBinary.byteLength !== file.size || digest !== file.sha256.toLowerCase()) {
1203
+ const canonicalDevicePath = header.devicePath.toLowerCase();
1204
+ if (devicePaths.has(canonicalDevicePath)) {
1285
1205
  throw ERRORS.TypedError(
1286
1206
  HardwareErrorCode.RuntimeError,
1287
- `Protocol V2 local resource file does not match manifest: ${file.archive_path}`,
1288
- { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
1207
+ `Protocol V2 resource ZIP contains duplicate device path: ${header.devicePath}`,
1208
+ { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1289
1209
  );
1290
1210
  }
1291
- materializedEntries.push({ entryName: file.archive_path, binary: fileBinary });
1211
+ devicePaths.add(canonicalDevicePath);
1212
+ resources.push({ entryName: entry.name, binary: fileBinary, header });
1292
1213
  }
1293
- return { binary, materializedEntries };
1214
+ return { binary, resources };
1294
1215
  }
1295
1216
 
1296
1217
  private async prepareProtocolV2ResourceSources(): Promise<ProtocolV2ResourceBundleSource[]> {
@@ -1355,110 +1276,20 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1355
1276
  );
1356
1277
  }
1357
1278
 
1358
- // materializedEntries 由宿主生成,只有与获批 ZIP 的规范字节完全一致后才能使用。
1359
- const verifiedArchive = await this.prepareProtocolV2LocalResourceArchive(archiveBinary);
1360
- const entries = archiveArtifact.materializedEntries ?? [];
1361
- const entriesByName = new Map(entries.map(entry => [entry.entryName, entry] as const));
1362
- if (
1363
- entries.length !== verifiedArchive.materializedEntries.length ||
1364
- verifiedArchive.materializedEntries.some(entry => {
1365
- const preparedEntry = entriesByName.get(entry.entryName);
1366
- const digest = bytesToHex(sha256(new Uint8Array(entry.binary)));
1367
- return (
1368
- !preparedEntry ||
1369
- preparedEntry.artifact.size !== entry.binary.byteLength ||
1370
- preparedEntry.artifact.sha256.toLowerCase() !== digest
1371
- );
1372
- })
1373
- ) {
1374
- throw ERRORS.TypedError(
1375
- HardwareErrorCode.RuntimeError,
1376
- 'Protocol V2 prepared resource entries do not match the approved archive',
1377
- { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
1378
- );
1379
- }
1380
-
1381
- const verifiedEntriesByName = new Map(
1382
- verifiedArchive.materializedEntries.map(entry => [entry.entryName, entry.binary] as const)
1383
- );
1384
- const manifestBinary = verifiedEntriesByName.get('manifest.json');
1385
- if (!manifestBinary) {
1386
- throw ERRORS.TypedError(
1387
- HardwareErrorCode.RuntimeError,
1388
- 'Protocol V2 prepared resource archive has no valid manifest.json',
1389
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1390
- );
1391
- }
1392
-
1393
- let manifestValue: unknown;
1394
- try {
1395
- manifestValue = JSON.parse(new TextDecoder().decode(manifestBinary));
1396
- } catch (error) {
1397
- throw ERRORS.TypedError(
1398
- HardwareErrorCode.RuntimeError,
1399
- `Protocol V2 prepared resource manifest is invalid: ${String(error)}`,
1400
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1401
- );
1402
- }
1403
- const manifest = parseProtocolV2ResourceManifest(manifestValue);
1404
- const selectedFiles = selectProtocolV2ResourceManifestFiles({
1405
- manifest,
1406
- targetsToUpdate: this.params.targetsToUpdate ?? [],
1407
- });
1408
- if (selectedFiles.length > PROTOCOL_V2_RESOURCE_FILE_MAX_COUNT) {
1409
- throw ERRORS.TypedError(
1410
- HardwareErrorCode.RuntimeError,
1411
- 'Protocol V2 prepared resource archive contains too many files',
1412
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1413
- );
1414
- }
1415
-
1416
- let totalSize = 0;
1417
- const sources: ProtocolV2ResourceBundleSource[] = [];
1418
- for (const [index, file] of selectedFiles.entries()) {
1419
- const binary = verifiedEntriesByName.get(file.archive_path);
1420
- if (!binary || binary.byteLength !== file.size) {
1421
- throw ERRORS.TypedError(
1422
- HardwareErrorCode.RuntimeError,
1423
- `Protocol V2 prepared resource file does not match manifest: ${file.archive_path}`,
1424
- { firmwareUpdateCode: 'FirmwareArtifactReceiptMismatch' }
1425
- );
1426
- }
1427
- totalSize += binary.byteLength;
1428
- if (totalSize > PROTOCOL_V2_RESOURCE_TOTAL_MAX_BYTES) {
1429
- throw ERRORS.TypedError(
1430
- HardwareErrorCode.RuntimeError,
1431
- 'Protocol V2 prepared resource archive exceeds the total size limit',
1432
- { firmwareUpdateCode: 'FirmwareArtifactsNotPrepared' }
1433
- );
1434
- }
1435
- // 使用从获批 ZIP 中提取的规范字节,避免宿主在校验后替换 entry reader 内容。
1436
- const source = await this.openProtocolV2MemorySource(binary);
1437
- const header =
1438
- source.size >= PROTOCOL_V2_OKPP_HEADER_SIZE
1439
- ? parseProtocolV2OkppHeader(
1440
- new Uint8Array(await source.readAt(0, PROTOCOL_V2_OKPP_HEADER_SIZE))
1441
- )
1442
- : null;
1443
- sources.push({
1444
- name: file.original_name || `resource-${index}`,
1445
- source,
1446
- devicePath: file.device_path,
1447
- ...(header
1448
- ? {
1449
- version: header.version,
1450
- payloadHash: header.payloadHash,
1451
- headerHash: header.headerHash,
1452
- }
1453
- : {}),
1454
- });
1455
- }
1456
- 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);
1457
1282
  }
1458
1283
 
1459
- private async runProtocolV2PreparedArtifacts(features: Features, firmwareType: EFirmwareType) {
1284
+ private async runProtocolV2PreparedArtifacts(
1285
+ features: Features,
1286
+ firmwareType: EFirmwareType,
1287
+ announceDownload = true
1288
+ ) {
1460
1289
  try {
1461
- this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
1290
+ if (announceDownload) {
1291
+ this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
1292
+ }
1462
1293
  const installSources = await this.prepareProtocolV2InstallSources(firmwareType, features);
1463
1294
  const resourceSources = await this.prepareProtocolV2ResourceSources();
1464
1295
  if (installSources.length === 0 && resourceSources.length === 0) {
@@ -1467,7 +1298,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1467
1298
  'No firmware to update'
1468
1299
  );
1469
1300
  }
1470
- this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
1301
+ if (announceDownload) {
1302
+ this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
1303
+ }
1471
1304
 
1472
1305
  return await this.executeProtocolV2SourceUpdate({
1473
1306
  installSources,
@@ -1522,21 +1355,20 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1522
1355
  const expected = this.params?.expectedTargetVersions;
1523
1356
  if (!expected) return;
1524
1357
  const features = this.protocolV2LatestFinalFeatures;
1525
- const requestedTargets = this.getProtocolV2RequestedTargets();
1526
- const applicationTargets = requestedTargets.filter(
1527
- target => target === 'app_v1' || target === 'app_v2'
1528
- );
1358
+ const deviceInfo = this.protocolV2LatestFinalDeviceInfo;
1529
1359
  const visibleVersions: Partial<Record<FirmwareUpdateV4Target, string>> = {
1530
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,
1531
1366
  coprocessor: features ? getDeviceBLEFirmwareVersion(features).join('.') : undefined,
1532
1367
  se01: features?.se01Version ?? undefined,
1533
1368
  se02: features?.se02Version ?? undefined,
1534
1369
  se03: features?.se03Version ?? undefined,
1535
1370
  se04: features?.se04Version ?? undefined,
1536
1371
  };
1537
- if (features && applicationTargets.length === 1) {
1538
- visibleVersions[applicationTargets[0]] = getDeviceFirmwareVersion(features).join('.');
1539
- }
1540
1372
  const expectedEntries = (
1541
1373
  Object.entries(expected) as Array<[FirmwareUpdateV4Target, string]>
1542
1374
  ).filter(([target]) => !targets || targets.includes(target));
@@ -1553,9 +1385,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1553
1385
  Math.floor(statusVersion / 0x100) % 0x100
1554
1386
  }.${statusVersion % 0x100}`;
1555
1387
  if (!observedVersion) {
1556
- if (this.protocolV2FinalStatusVerified) {
1388
+ const hasCompleteTargetEvidence =
1389
+ targetId !== undefined && this.protocolV2CompletedTargetIds.has(targetId);
1390
+ if (hasCompleteTargetEvidence) {
1557
1391
  Log.warn(
1558
- `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`
1559
1393
  );
1560
1394
  } else {
1561
1395
  throw ERRORS.TypedError(
@@ -1572,37 +1406,23 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1572
1406
  }
1573
1407
  }
1574
1408
 
1575
- private getProtocolV2RequestedTargets(): FirmwareUpdateV4Target[] {
1576
- if (this.params.targetsToUpdate?.length) {
1577
- 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();
1578
1413
  }
1579
- if (this.params.preparedPlan?.targetsToUpdate.length) {
1580
- return [...new Set(this.params.preparedPlan.targetsToUpdate)] as FirmwareUpdateV4Target[];
1414
+ if (!features) {
1415
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Device features not available');
1581
1416
  }
1582
- const targets = new Set<FirmwareUpdateV4Target>();
1583
- Object.keys(this.params.componentArtifacts ?? {}).forEach(target =>
1584
- targets.add(target as FirmwareUpdateV4Target)
1585
- );
1586
- if (this.params.bootloaderBinary) targets.add('boot');
1587
- if (this.params.applicationP1Binary) targets.add('app_v1');
1588
- if (this.params.applicationP2Binary) targets.add('app_v2');
1589
- if (this.params.coprocessorBinary) targets.add('coprocessor');
1590
- if (this.params.se01Binary) targets.add('se01');
1591
- if (this.params.se02Binary) targets.add('se02');
1592
- if (this.params.se03Binary) targets.add('se03');
1593
- if (this.params.se04Binary) targets.add('se04');
1594
- return Array.from(targets);
1595
- }
1596
-
1597
- private async getProtocolV2DeviceFeatures(): Promise<Features> {
1598
- if (this.device.features) {
1599
- return this.device.features;
1600
- }
1601
- if (typeof this.device.getFeatures === 'function') {
1602
- const features = await this.device.getFeatures();
1603
- if (features) return features;
1417
+ const deviceType = this.device.getCurrentDeviceType();
1418
+ if (deviceType !== EDeviceType.Pro2 && deviceType !== EDeviceType.Neo) {
1419
+ return features;
1604
1420
  }
1605
- throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Device features not available');
1421
+ return {
1422
+ ...features,
1423
+ deviceType,
1424
+ firmwareType: this.device.getCurrentFirmwareType(),
1425
+ };
1606
1426
  }
1607
1427
 
1608
1428
  private getProtocolV2SerialNumber(deviceInfo: ProtocolV2DeviceInfo) {
@@ -1698,6 +1518,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1698
1518
  .filter(target => !preparedTargets.has(target));
1699
1519
  }
1700
1520
 
1521
+ private filterProtocolV2LocalInstallItems(installItems: ProtocolV2InstallItem[]) {
1522
+ if (!this.protocolV2HasExplicitTargetSelection) {
1523
+ return installItems;
1524
+ }
1525
+ const requestedTargets = new Set(this.params.targetsToUpdate ?? []);
1526
+ return installItems.filter(item => {
1527
+ const target = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(item.targetId);
1528
+ return target !== undefined && requestedTargets.has(target);
1529
+ });
1530
+ }
1531
+
1701
1532
  private buildProtocolV2InstallItems({
1702
1533
  bootloaderBinary,
1703
1534
  fwBinaryMap,
@@ -1794,22 +1625,54 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1794
1625
  `Protocol V2 firmware fingerprint mismatch: ${key}/${component.target}`
1795
1626
  );
1796
1627
  }
1797
- const expectedPayloadHash = normalizeProtocolV2Hex(component.payloadHash);
1798
- if (expectedPayloadHash) {
1799
- const header = parseProtocolV2OkppHeader(toProtocolV2Bytes(binary));
1800
- if (!header || header.payloadHash !== expectedPayloadHash) {
1801
- throw ERRORS.TypedError(
1802
- HardwareErrorCode.RuntimeError,
1803
- `Protocol V2 firmware payload hash mismatch: ${key}/${component.target}`
1804
- );
1805
- }
1806
- }
1807
1628
  return {
1808
1629
  ...target,
1809
1630
  binary,
1810
1631
  };
1811
1632
  }
1812
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
+
1813
1676
  private async prepareRemoteProtocolV2Binaries(
1814
1677
  firmwareType: EFirmwareType,
1815
1678
  features: Features,
@@ -1853,8 +1716,14 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1853
1716
  : undefined;
1854
1717
  if (updateTarget && targetsToUpdate.has(updateTarget)) {
1855
1718
  const explicitInstallItem = explicitInstallItemByTargetId.get(target.targetId);
1856
- const installItem =
1857
- 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
+ }
1858
1727
  if (installItem.kind === 'bootloader') {
1859
1728
  bootloaderBinary = installItem.binary;
1860
1729
  } else {
@@ -1893,6 +1762,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1893
1762
  return `vol0:/${path}`;
1894
1763
  }
1895
1764
 
1765
+ private getProtocolV2ResourceComparePath(devicePath: string) {
1766
+ return isProtocolV2BootResourcePackagePath(devicePath)
1767
+ ? PROTOCOL_V2_BOOT_RESOURCE_PACKAGE_PATH
1768
+ : devicePath;
1769
+ }
1770
+
1896
1771
  private async readProtocolV2DeviceFileHeader(path: string, expectedSize?: number) {
1897
1772
  const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
1898
1773
  const filePath = this.getProtocolV2ResourceFilePath(path);
@@ -1904,7 +1779,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1904
1779
  !pathInfoRes.message?.exist ||
1905
1780
  pathInfoRes.message?.directory ||
1906
1781
  fileSize === undefined ||
1907
- fileSize < PROTOCOL_V2_OKPP_HEADER_SIZE ||
1782
+ fileSize < PROTOCOL_V2_RESOURCE_PACKAGE_HEADER_SIZE ||
1908
1783
  (expectedSize !== undefined && fileSize !== expectedSize)
1909
1784
  ) {
1910
1785
  return null;
@@ -1913,8 +1788,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1913
1788
  const chunkSize = this.getProtocolV2FirmwareChunkSize('read');
1914
1789
  const chunks: Uint8Array[] = [];
1915
1790
  let offset = 0;
1916
- while (offset < PROTOCOL_V2_OKPP_HEADER_SIZE) {
1917
- 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);
1918
1793
  const res = await typedCall('FilesystemFileRead', 'FilesystemFile', {
1919
1794
  file: {
1920
1795
  path: filePath,
@@ -1936,7 +1811,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1936
1811
  headerBytes.set(chunk, cursor);
1937
1812
  cursor += chunk.byteLength;
1938
1813
  });
1939
- return parseProtocolV2OkppHeader(headerBytes);
1814
+ try {
1815
+ return parseProtocolV2ResourcePackageHeader(headerBytes, fileSize);
1816
+ } catch {
1817
+ return null;
1818
+ }
1940
1819
  }
1941
1820
 
1942
1821
  /** Compare the downloaded and installed okpkg headers before transferring a resource. */
@@ -1951,7 +1830,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1951
1830
 
1952
1831
  try {
1953
1832
  const header = await this.readProtocolV2DeviceFileHeader(
1954
- bundle.devicePath,
1833
+ this.getProtocolV2ResourceComparePath(bundle.devicePath),
1955
1834
  bundle.source.size
1956
1835
  );
1957
1836
  if (!header) return false;
@@ -1966,6 +1845,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1966
1845
  if (!expectedHeaderHash || header.headerHash !== expectedHeaderHash) return false;
1967
1846
  return true;
1968
1847
  } catch (error) {
1848
+ if (isBleStaleBondHardwareError(error)) {
1849
+ throw error;
1850
+ }
1969
1851
  Log.log(`[FirmwareUpdateV4] RESC bundle ${bundle.name} header check failed: `, error);
1970
1852
  return false;
1971
1853
  }
@@ -1995,21 +1877,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1995
1877
  return this.device.features?.mode === 'romloader';
1996
1878
  }
1997
1879
 
1998
- async enterProtocolV2BootloaderMode() {
1999
- // romloader is the first update environment and forwards targets to bootloader.
2000
- // It rejects DeviceRebootType.Bootloader, so reuse the current connection.
2001
- if (this.isProtocolV2RomloaderMode()) {
2002
- Log.debug('Protocol V2 device is in romloader mode; start firmware update directly');
2003
- this.protocolV2ExecutionInLoader = true;
2004
- return false;
2005
- }
2006
- if (this.isProtocolV2BootloaderMode()) {
2007
- Log.debug('Protocol V2 device is already in bootloader mode, skip reboot');
2008
- this.protocolV2ExecutionInLoader = true;
2009
- this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
2010
- return false;
2011
- }
2012
-
1880
+ private async rebootProtocolV2ToBootloader() {
2013
1881
  try {
2014
1882
  this.postTipMessage(FirmwareUpdateTipMessage.AutoRebootToBootloader);
2015
1883
  await this.protocolV2Reboot(DeviceRebootType.Bootloader);
@@ -2027,6 +1895,24 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2027
1895
  }
2028
1896
  }
2029
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
+
2030
1916
  private async waitForProtocolV2BootloaderMode(
2031
1917
  timeout = PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT,
2032
1918
  retryInterval = 1000
@@ -2055,7 +1941,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2055
1941
  }
2056
1942
  lastError = new Error('Protocol V2 device is reachable but is not in bootloader mode');
2057
1943
  } catch (error) {
2058
- if (this.isProtocolV2ReconnectIdentityError(error)) {
1944
+ if (isBleStaleBondHardwareError(error) || this.isProtocolV2ReconnectIdentityError(error)) {
2059
1945
  throw error;
2060
1946
  }
2061
1947
  shouldReconnect = true;
@@ -2111,68 +1997,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2111
1997
  return entries;
2112
1998
  }
2113
1999
 
2114
- private buildProtocolV2ExecutionPhases({
2115
- installSources,
2116
- resourceSources,
2117
- }: {
2118
- installSources: ProtocolV2InstallSource[];
2119
- resourceSources: ProtocolV2ResourceBundleSource[];
2120
- }): ProtocolV2ExecutionPhase[] {
2121
- const phases: ProtocolV2ExecutionPhase[] = [];
2122
- const bootResourceSources = resourceSources.filter(source =>
2123
- isProtocolV2BootResourcePackagePath(source.devicePath)
2124
- );
2125
- const remainingResourceSources = resourceSources.filter(
2126
- source => !isProtocolV2BootResourcePackagePath(source.devicePath)
2127
- );
2128
- // A previous interrupted run may have left a promotable staging file. Replace
2129
- // it with the current approved package before any firmware-triggered reboot.
2130
- if (bootResourceSources.length > 0) {
2131
- phases.push({
2132
- kind: 'resource-sync',
2133
- installSources: [],
2134
- resourceSources: bootResourceSources,
2135
- });
2136
- }
2137
- const bootloaderSources = installSources.filter(source => source.kind === 'bootloader');
2138
- if (bootloaderSources.length > 0) {
2139
- phases.push(
2140
- {
2141
- kind: 'bootloader-install',
2142
- installSources: bootloaderSources,
2143
- resourceSources: [],
2144
- },
2145
- {
2146
- kind: 'bootloader-verify',
2147
- installSources: [],
2148
- resourceSources: [],
2149
- }
2150
- );
2151
- }
2152
- if (remainingResourceSources.length > 0) {
2153
- phases.push({
2154
- kind: 'resource-sync',
2155
- installSources: [],
2156
- resourceSources: remainingResourceSources,
2157
- });
2158
- }
2159
- const componentSources = installSources.filter(source => source.kind !== 'bootloader');
2160
- if (componentSources.length > 0) {
2161
- phases.push({
2162
- kind: 'component-install',
2163
- installSources: componentSources,
2164
- resourceSources: [],
2165
- });
2166
- }
2167
- phases.push({
2168
- kind: 'final-verify',
2169
- installSources: [],
2170
- resourceSources: [],
2171
- });
2172
- return phases;
2173
- }
2174
-
2175
- private async executeProtocolV2Phases({
2000
+ private async executeProtocolV2SourceUpdate({
2176
2001
  installSources,
2177
2002
  resourceSources,
2178
2003
  }: {
@@ -2180,38 +2005,22 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2180
2005
  resourceSources: ProtocolV2ResourceBundleSource[];
2181
2006
  }) {
2182
2007
  this.protocolV2BootResourceStagingSafe = false;
2183
- const phases = this.buildProtocolV2ExecutionPhases({
2184
- installSources,
2185
- resourceSources,
2186
- });
2187
- for (const phase of phases) {
2188
- if (phase.kind === 'final-verify') {
2189
- return this.completeProtocolV2FinalVerification();
2190
- }
2191
- if (phase.kind === 'resource-sync') {
2192
- await this.enterProtocolV2BootloaderMode();
2193
- if (
2194
- !phase.resourceSources.some(source =>
2195
- isProtocolV2BootResourcePackagePath(source.devicePath)
2196
- )
2197
- ) {
2198
- await this.ensureProtocolV2BootResourceStagingIsEmpty();
2199
- }
2200
- await this.executeProtocolV2TransferPhase(phase);
2201
- } else if (phase.kind === 'bootloader-install' || phase.kind === 'component-install') {
2202
- await this.enterProtocolV2BootloaderMode();
2203
- await this.ensureProtocolV2BootResourceStagingIsEmpty();
2204
- await this.executeProtocolV2TransferPhase(phase);
2205
- await this.exitProtocolV2BootloaderToNormal();
2206
- } else if (phase.kind === 'bootloader-verify') {
2207
- await this.waitForProtocolV2FinalFeatures();
2208
- this.assertExpectedProtocolV2Versions(['boot']);
2209
- }
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
+ });
2210
2022
  }
2211
- throw ERRORS.TypedError(
2212
- HardwareErrorCode.RuntimeError,
2213
- 'Protocol V2 execution has no final verification phase'
2214
- );
2023
+ return this.completeProtocolV2FinalVerification();
2215
2024
  }
2216
2025
 
2217
2026
  private async ensureProtocolV2BootResourceStagingIsEmpty() {
@@ -2243,19 +2052,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2243
2052
  this.protocolV2BootResourceStagingSafe = true;
2244
2053
  }
2245
2054
 
2246
- private async executeProtocolV2SourceUpdate({
2247
- installSources,
2248
- resourceSources,
2249
- }: {
2250
- installSources: ProtocolV2InstallSource[];
2251
- resourceSources: ProtocolV2ResourceBundleSource[];
2252
- }) {
2253
- return this.executeProtocolV2Phases({
2254
- installSources,
2255
- resourceSources,
2256
- });
2257
- }
2258
-
2259
2055
  private async executeProtocolV2Update({
2260
2056
  fwBinaryMap,
2261
2057
  bootloaderBinary,
@@ -2280,7 +2076,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2280
2076
  kind: item.kind,
2281
2077
  }))
2282
2078
  );
2283
- return await this.executeProtocolV2Phases({
2079
+ return await this.executeProtocolV2SourceUpdate({
2284
2080
  installSources,
2285
2081
  resourceSources: [],
2286
2082
  });
@@ -2292,15 +2088,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2292
2088
  private async executeProtocolV2TransferPhase({
2293
2089
  installSources,
2294
2090
  resourceSources,
2295
- }: ProtocolV2ExecutionPhase) {
2091
+ }: ProtocolV2TransferBatch) {
2296
2092
  let totalSize = installSources.reduce((total, item) => total + item.source.size, 0);
2297
2093
  const resourcesToSync: ProtocolV2ResourceBundleSource[] = [];
2298
2094
  for (const resource of resourceSources) {
2299
- // Early boot promotes this mounted package's staging file. Always replace any
2300
- // stale staging bytes with the artifact approved by the current PreparedPlan,
2301
- // even when the mounted final file itself is already current.
2302
- const requiresFreshStaging = isProtocolV2BootResourcePackagePath(resource.devicePath);
2303
- if (!requiresFreshStaging && (await this.isProtocolV2ResourceBundleUpToDate(resource))) {
2095
+ if (await this.isProtocolV2ResourceBundleUpToDate(resource)) {
2304
2096
  Log.log(`[FirmwareUpdateV4] skip RESC bundle ${resource.name}; already up to date`);
2305
2097
  } else {
2306
2098
  resourcesToSync.push(resource);
@@ -2309,11 +2101,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2309
2101
  }
2310
2102
 
2311
2103
  this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
2104
+ this.protocolV2LastTransferProgress = undefined;
2105
+ this.protocolV2LastTransferProgressAt = 0;
2312
2106
  let processedSize = 0;
2313
2107
  for (const resource of resourcesToSync) {
2314
2108
  // The bootloader keeps its live resource package mounted. FatFs rejects
2315
2109
  // replacing an open file, so early boot promotes this staging file before mounting it.
2316
- const writePath = resolveProtocolV2ResourceWritePath(resource.devicePath);
2110
+ const writePath = resource.devicePath;
2317
2111
  processedSize = await this.protocolV2SourceUpdateProcess({
2318
2112
  source: resource.source,
2319
2113
  filePath: writePath,
@@ -2354,8 +2148,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2354
2148
  target_id: item.targetId,
2355
2149
  path: item.path,
2356
2150
  }));
2357
- await this.protocolV2StartFirmwareUpdate({ targets });
2358
- 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
+ }
2359
2158
  }
2360
2159
 
2361
2160
  private async protocolV2SourceUpdateProcess({
@@ -2400,23 +2199,33 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2400
2199
  );
2401
2200
  }
2402
2201
  const transferredBytes = processedSize + chunkEnd;
2403
- const elapsedMs = Math.max(Date.now() - transferStartedAt, 0);
2404
- this.postProgressMessage(
2405
- Math.min(Math.ceil((transferredBytes / totalSize) * 100), 99),
2406
- 'transferData',
2407
- {
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', {
2408
2214
  transferredBytes,
2409
2215
  totalBytes: totalSize,
2410
2216
  rateBytesPerSecond:
2411
2217
  elapsedMs > 0 ? Math.round((chunkEnd / elapsedMs) * 1000) : undefined,
2412
2218
  elapsedMs,
2413
- }
2414
- );
2219
+ });
2220
+ }
2415
2221
  return length;
2416
2222
  },
2417
2223
  });
2418
2224
  return processedSize + source.size;
2419
2225
  } catch (error) {
2226
+ if (isBleStaleBondHardwareError(error)) {
2227
+ throw error;
2228
+ }
2420
2229
  lastError = error;
2421
2230
  if (attempt < PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT) {
2422
2231
  await this.recoverProtocolV2FileTransfer();
@@ -2446,19 +2255,41 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2446
2255
  }
2447
2256
  }
2448
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
+
2449
2275
  private assertProtocolV2TargetStatus(
2450
2276
  statusTargets: ProtocolV2FirmwareUpdateStatusTarget[],
2451
2277
  expectedTargetIds: Set<number>,
2452
- expectedPaths = new Map<number, string>()
2278
+ expectedPaths?: Map<number, string>,
2279
+ liveTargetIds?: Set<number>
2453
2280
  ) {
2281
+ const resolvedExpectedPaths = expectedPaths ?? new Map<number, string>();
2454
2282
  Log.log(
2455
2283
  `[FirmwareUpdateV4] DeviceFirmwareUpdateStatus records=${JSON.stringify(statusTargets)}`
2456
2284
  );
2457
- const failedTarget = statusTargets.find(
2458
- target =>
2459
- expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1) &&
2460
- isProtocolV2TargetStatusFailed(target.status)
2461
- );
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
+ });
2462
2293
  if (failedTarget) {
2463
2294
  const failedTargetDetails = JSON.stringify({
2464
2295
  targetId: failedTarget.target_id,
@@ -2469,13 +2300,33 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2469
2300
  Log.error(`[FirmwareUpdateV4] firmware install failed target=${failedTargetDetails}`);
2470
2301
  throw ERRORS.TypedError(
2471
2302
  HardwareErrorCode.FirmwareError,
2472
- `Protocol V2 firmware target failed: target=${failedTarget.target_id} status=${
2473
- failedTarget.status ?? 'unknown'
2474
- } payloadVersion=${failedTarget.payload_version ?? 'unknown'} path=${
2475
- failedTarget.path ?? 'unknown'
2476
- }`
2303
+ 'Protocol V2 firmware install failed',
2304
+ {
2305
+ firmwareUpdateCode: PROTOCOL_V2_INSTALL_FAILED_CODE,
2306
+ }
2477
2307
  );
2478
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
+ }
2479
2330
 
2480
2331
  const matchingTargets = statusTargets.filter(target =>
2481
2332
  expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1)
@@ -2483,14 +2334,21 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2483
2334
  const seenTargetIds = new Set<number>();
2484
2335
  for (const target of matchingTargets) {
2485
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));
2486
2343
  if (
2487
2344
  targetId === undefined ||
2488
2345
  seenTargetIds.has(targetId) ||
2489
- (target.path && expectedPaths.get(targetId) && target.path !== expectedPaths.get(targetId))
2346
+ pathConflictsWithCurrentInstall
2490
2347
  ) {
2348
+ Log.error(`[FirmwareUpdateV4] install status conflicts with target=${target.target_id}`);
2491
2349
  throw ERRORS.TypedError(
2492
- HardwareErrorCode.RuntimeError,
2493
- `Protocol V2 install status conflicts with target ${target.target_id}`,
2350
+ HardwareErrorCode.FirmwareError,
2351
+ 'Protocol V2 firmware install status conflict',
2494
2352
  {
2495
2353
  firmwareUpdateCode: PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE,
2496
2354
  }
@@ -2498,14 +2356,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2498
2356
  }
2499
2357
  seenTargetIds.add(targetId);
2500
2358
  }
2501
- const completedTargets = matchingTargets.filter(target =>
2502
- isProtocolV2TargetStatusFinished(target.status)
2503
- );
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
+ });
2504
2366
  const completedTargetIds = new Set<number>();
2505
2367
  completedTargets.forEach(target => {
2506
2368
  const targetId = normalizeProtocolV2TargetId(target.target_id);
2507
2369
  if (targetId !== undefined) {
2508
2370
  completedTargetIds.add(targetId);
2371
+ this.protocolV2CompletedTargetIds.add(targetId);
2509
2372
  }
2510
2373
  });
2511
2374
  if (completedTargetIds.size === expectedTargetIds.size && expectedTargetIds.size > 0) {
@@ -2536,16 +2399,54 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2536
2399
  }
2537
2400
 
2538
2401
  if (expectedTargetIds.size > 0 && matchingTargets.length > 0) {
2539
- const hasInProgressTarget = matchingTargets.some(target =>
2540
- isProtocolV2TargetStatusInProgress(target.status)
2541
- );
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
+ });
2542
2437
  const completedProgress = Math.floor(
2543
2438
  (completedTargetIds.size / expectedTargetIds.size) * 100
2544
2439
  );
2545
- // The protocol exposes no per-target percentage, so report coarse progress by
2546
- // completed targets and use 1% once work starts to keep the UI responsive.
2547
- const progress = Math.min(99, Math.max(completedProgress, hasInProgressTarget ? 1 : 0));
2548
- 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
+ }
2549
2450
  }
2550
2451
 
2551
2452
  return false;
@@ -2565,31 +2466,95 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2565
2466
  return Array.from(expectedTargetIds).filter(targetId => !reportedTargetIds.has(targetId));
2566
2467
  }
2567
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
+
2568
2514
  private async waitForProtocolV2FirmwareUpdateComplete(
2569
- targets: Array<{ target_id: number; path: string }>
2515
+ targets: Array<{ target_id: number; path: string }>,
2516
+ requireCurrentInstallStatus = false
2570
2517
  ) {
2571
- this.protocolV2FinalStatusVerified = false;
2572
2518
  const expectedTargetIds = new Set(targets.map(target => target.target_id));
2573
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);
2574
2523
  const startTime = Date.now();
2575
2524
  let lastError: unknown;
2576
- 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;
2577
2529
  let deviceInfo: ProtocolV2DeviceInfo | undefined;
2578
- let missingTargetStatusSince: number | undefined;
2579
- let missingTargetStatusKey: string | undefined;
2580
- const resetMissingTargetStatusGrace = () => {
2581
- missingTargetStatusSince = undefined;
2582
- missingTargetStatusKey = undefined;
2583
- };
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>();
2584
2539
 
2585
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();
2586
2544
  try {
2587
2545
  if (shouldReconnect) {
2588
- await this.reconnectProtocolV2Device();
2589
- 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
+ }
2590
2555
  shouldReconnect = false;
2591
2556
  }
2592
- const currentDeviceInfo = deviceInfo;
2557
+ let currentDeviceInfo = deviceInfo;
2593
2558
  try {
2594
2559
  const statusResponse = await this.device.getCommands().typedCall(
2595
2560
  'DeviceFirmwareUpdateStatusGet',
@@ -2597,91 +2562,243 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2597
2562
  {
2598
2563
  fields: {
2599
2564
  status: true,
2565
+ progress_percent: true,
2566
+ phase_info: true,
2600
2567
  payload_version: true,
2601
2568
  path: true,
2602
2569
  },
2603
2570
  },
2604
2571
  { timeoutMs: PROTOCOL_V2_FIRMWARE_STATUS_RESPONSE_TIMEOUT }
2605
2572
  );
2573
+ if (this.protocolV2InstallTerminalSuccessObserved) {
2574
+ installEvidenceObserved = true;
2575
+ }
2606
2576
  const statusTargets = (statusResponse.message.records ??
2607
2577
  []) as ProtocolV2FirmwareUpdateStatusTarget[];
2608
- if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds, expectedPaths)) {
2609
- 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
+ ) {
2610
2679
  return;
2611
2680
  }
2612
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
+
2613
2688
  if (
2614
- statusTargets.length === 0 &&
2615
- currentDeviceInfo &&
2616
- (await this.probeProtocolV2NormalMode(currentDeviceInfo))
2689
+ requireCurrentInstallStatus &&
2690
+ !hasCurrentInstallEvidence &&
2691
+ matchingStatusTargets.length > 0
2617
2692
  ) {
2618
- Log.log(
2619
- '[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'
2620
2695
  );
2621
- this.postProgressMessage(100, 'installingFirmware');
2622
- return;
2623
2696
  }
2624
2697
 
2625
- const missingTargetIds = this.getProtocolV2MissingTargetIds(
2626
- statusTargets,
2627
- expectedTargetIds
2628
- );
2629
- if (missingTargetIds.length > 0) {
2630
- const now = Date.now();
2631
- const missingKey = missingTargetIds.join(',');
2632
- if (missingTargetStatusSince === undefined || missingTargetStatusKey !== missingKey) {
2633
- missingTargetStatusSince = now;
2634
- missingTargetStatusKey = missingKey;
2635
- } else if (
2636
- now - missingTargetStatusSince >=
2637
- 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))
2638
2716
  ) {
2639
- const reportedTargetIds = statusTargets
2640
- .map(target => normalizeProtocolV2TargetId(target.target_id))
2641
- .filter((targetId): targetId is number => targetId !== undefined);
2642
- throw ERRORS.TypedError(
2643
- HardwareErrorCode.FirmwareError,
2644
- `Protocol V2 firmware status is missing requested records: targetIds=${missingKey} reportedTargetIds=${reportedTargetIds.join(
2645
- ','
2646
- )}`
2717
+ Log.log(
2718
+ '[FirmwareUpdateV4] empty firmware status after confirmed App reboot; update complete'
2647
2719
  );
2720
+ this.recordProtocolV2AuthoritativeInstallCompletion(expectedTargetIds);
2721
+ this.postProgressMessage(100, 'installingFirmware');
2722
+ return;
2648
2723
  }
2724
+ }
2725
+
2726
+ if (statusTargets.length === 0) {
2649
2727
  lastError = new Error(
2650
- `Protocol V2 firmware status is temporarily missing targetIds=${missingKey}`
2728
+ 'Protocol V2 firmware update is waiting for user confirmation or target status'
2651
2729
  );
2652
2730
  } else {
2653
- resetMissingTargetStatusGrace();
2654
- 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
+ }
2655
2743
  }
2656
2744
  } catch (error) {
2657
2745
  if (isProtocolV2TerminalInstallStatusError(error)) {
2658
2746
  throw error;
2659
2747
  }
2660
- // Only consecutive full status dumps can prove that a record is absent.
2661
- // A reboot or transport interruption starts a fresh grace window.
2662
- resetMissingTargetStatusGrace();
2663
2748
  // App firmware does not register DeviceFirmwareUpdateStatusGet. Treat the
2664
2749
  // missing endpoint as completion only after the runtime probe confirms App mode.
2665
2750
  if (isProtocolV2FirmwareStatusEndpointUnavailable(error)) {
2666
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) {
2667
2762
  throw ERRORS.TypedError(
2668
2763
  HardwareErrorCode.RuntimeError,
2669
2764
  'Protocol V2 device identity is unavailable during install polling'
2670
2765
  );
2671
2766
  }
2672
- if (await this.probeProtocolV2NormalMode(currentDeviceInfo)) {
2767
+ const isNormalMode = await this.probeProtocolV2NormalMode(reconnectDeviceInfo);
2768
+ if (
2769
+ isNormalMode &&
2770
+ (installEvidenceObserved ||
2771
+ this.hasProtocolV2InstallVersionChanged(expectedTargetIds))
2772
+ ) {
2673
2773
  Log.log(
2674
2774
  '[FirmwareUpdateV4] firmware status endpoint unavailable after confirmed App reboot'
2675
2775
  );
2776
+ this.recordProtocolV2AuthoritativeInstallCompletion(expectedTargetIds);
2676
2777
  this.postProgressMessage(100, 'installingFirmware');
2677
2778
  return;
2678
2779
  }
2679
- lastError = new Error(
2680
- 'Protocol V2 firmware status endpoint is unavailable while the device remains in loader mode'
2681
- );
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
+ }
2682
2789
  } else {
2790
+ if (
2791
+ isBleCoprocessorInstall &&
2792
+ isProtocolV2DeviceDisconnectedError(error) &&
2793
+ !isProtocolV2ResponseTimeout(error)
2794
+ ) {
2795
+ installStatusDisconnectObserved = true;
2796
+ finishedStatusSnapshotKey = undefined;
2797
+ finishedStatusSnapshotPolls = 0;
2798
+ }
2683
2799
  shouldReconnect = true;
2684
2800
  deviceInfo = undefined;
2801
+ bleInstallLinkReady = false;
2685
2802
  lastError = error;
2686
2803
  Log.log(
2687
2804
  '[FirmwareUpdateV4] DeviceFirmwareUpdateStatusGet unavailable during install: ',
@@ -2705,18 +2822,24 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2705
2822
  }
2706
2823
  shouldReconnect = true;
2707
2824
  deviceInfo = undefined;
2708
- resetMissingTargetStatusGrace();
2825
+ bleInstallLinkReady = false;
2709
2826
  Log.log('Protocol V2 firmware install device readiness probe failed: ', error);
2710
2827
  }
2711
2828
  await wait(1000);
2712
2829
  }
2713
2830
 
2714
- throw ERRORS.TypedError(
2715
- HardwareErrorCode.RuntimeError,
2716
- `Protocol V2 firmware update status not complete within ${
2831
+ Log.error(
2832
+ `[FirmwareUpdateV4] install timed out after ${
2717
2833
  PROTOCOL_V2_INSTALL_TIMEOUT / 1000
2718
2834
  }s: ${this.normalizeErrorMessage(lastError)}`
2719
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
+ );
2720
2843
  }
2721
2844
 
2722
2845
  private async exitProtocolV2BootloaderToNormal() {
@@ -2728,6 +2851,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2728
2851
  return;
2729
2852
  }
2730
2853
  } catch (error) {
2854
+ if (isBleStaleBondHardwareError(error)) {
2855
+ throw error;
2856
+ }
2731
2857
  Log.log('[FirmwareUpdateV4] unable to confirm App mode before Normal reboot: ', error);
2732
2858
  }
2733
2859
  await this.protocolV2Reboot(DeviceRebootType.Normal);
@@ -2739,7 +2865,14 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2739
2865
  deviceInfo,
2740
2866
  PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT
2741
2867
  );
2742
- 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
+ );
2743
2876
  }
2744
2877
 
2745
2878
  private async waitForProtocolV2FinalFeatures() {
@@ -2783,6 +2916,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2783
2916
  const startTime = Date.now();
2784
2917
  let lastError: unknown;
2785
2918
  let shouldReconnect = true;
2919
+ this.protocolV2LatestFinalDeviceInfo = undefined;
2786
2920
 
2787
2921
  while (Date.now() - startTime < timeout) {
2788
2922
  try {
@@ -2799,9 +2933,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2799
2933
  this.assertProtocolV2DeviceInfoIdentity(deviceInfo);
2800
2934
  const features = await this.device.probeProtocolV2RuntimeState(
2801
2935
  deviceInfo,
2802
- PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT
2936
+ PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
2937
+ { forceRuntimeContextRefresh: true }
2803
2938
  );
2804
- if (features.mode === 'normal' && !features.bootloaderMode) {
2939
+ if (this.isProtocolV2ApplicationMode(features)) {
2940
+ this.protocolV2LatestFinalDeviceInfo = deviceInfo;
2805
2941
  return features;
2806
2942
  }
2807
2943
  lastError = ERRORS.TypedError(
@@ -2809,7 +2945,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2809
2945
  'Protocol V2 device is still in bootloader mode'
2810
2946
  );
2811
2947
  } catch (error) {
2812
- if (this.isProtocolV2ReconnectIdentityError(error)) {
2948
+ if (isBleStaleBondHardwareError(error) || this.isProtocolV2ReconnectIdentityError(error)) {
2813
2949
  throw error;
2814
2950
  }
2815
2951
  shouldReconnect = true;
@@ -2827,19 +2963,53 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2827
2963
  );
2828
2964
  }
2829
2965
 
2830
- private async reconnectProtocolV2Device() {
2966
+ private async reconnectProtocolV2Device(options?: { skipProtocolProbe?: boolean }) {
2831
2967
  if (this.isBleReconnect()) {
2832
- await this.acquireProtocolV2BleDevice();
2968
+ await this.acquireProtocolV2BleDevice(options?.skipProtocolProbe);
2833
2969
  return;
2834
2970
  }
2835
2971
 
2836
2972
  const deviceDiff = await this.device.deviceConnector?.enumerate();
2837
2973
  const devicesDescriptor = deviceDiff?.descriptors ?? [];
2974
+ const env = DataManager.getSettings('env');
2975
+ const isWebUsb = DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env);
2838
2976
 
2839
- if (
2840
- DataManager.isBrowserWebUsb(DataManager.getSettings('env')) &&
2841
- devicesDescriptor.length === 1
2842
- ) {
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) {
2843
3013
  const descriptor = devicesDescriptor[0];
2844
3014
  if (!this.protocolV2ExpectedSerialNumber && descriptor.path !== this.protocolV2ExpectedPath) {
2845
3015
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
@@ -2957,13 +3127,24 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2957
3127
  await wait(2000);
2958
3128
  }
2959
3129
 
2960
- private async acquireProtocolV2BleDevice() {
2961
- await this.device.deviceConnector?.acquire(
2962
- 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,
2963
3139
  null,
2964
3140
  true,
2965
- PROTOCOL_V2_CONNECT_PROTOCOL
3141
+ PROTOCOL_V2_CONNECT_PROTOCOL,
3142
+ undefined,
3143
+ undefined,
3144
+ skipProtocolProbe
2966
3145
  );
3146
+ this.device.commands.disposed = false;
3147
+ this.device.getCommands().mainId = expectedId;
2967
3148
  }
2968
3149
 
2969
3150
  private async protocolV2StartFirmwareUpdate({
@@ -2971,30 +3152,72 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2971
3152
  }: {
2972
3153
  targets: Array<{ target_id: number; path: string }>;
2973
3154
  }) {
3155
+ this.protocolV2LastRuntimeProbeFeatures = undefined;
3156
+ this.protocolV2InstallNeedsReconnect = false;
3157
+ this.protocolV2InstallTerminalSuccessObserved = false;
2974
3158
  const commands = this.device.getCommands();
2975
- const response: ProtocolV2FirmwareUpdateStartResponse = await commands.typedCall(
2976
- 'DeviceFirmwareUpdateRequest',
2977
- 'Success',
2978
- { targets },
2979
- { 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
+ })
2980
3172
  );
2981
- // Success acknowledges that the device accepted installation. End confirmation
2982
- // and begin status polling only after receiving this ACK.
2983
- this.postTipMessage(FirmwareUpdateTipMessage.FirmwareUpdating);
2984
- this.postProgressMessage(0, 'installingFirmware');
2985
- 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
+ }
2986
3200
  }
2987
3201
 
2988
3202
  private async protocolV2Reboot(rebootType: DeviceRebootType) {
2989
3203
  const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
2990
3204
  try {
2991
- const res = await typedCall('DeviceReboot', 'Success', {
2992
- reboot_type: rebootType,
2993
- });
3205
+ const res = await typedCall(
3206
+ 'DeviceReboot',
3207
+ 'Success',
3208
+ {
3209
+ reboot_type: rebootType,
3210
+ },
3211
+ { timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT }
3212
+ );
2994
3213
  this.device.markProtocolV2Reboot(rebootType);
2995
3214
  return res.message;
2996
3215
  } catch (error) {
2997
- if (isProtocolV2DeviceDisconnectedError(error) || isProtocolV2ReconnectProbeError(error)) {
3216
+ if (
3217
+ isProtocolV2DeviceDisconnectedError(error) ||
3218
+ isProtocolV2ReconnectProbeError(error) ||
3219
+ isProtocolV2ResponseTimeout(error)
3220
+ ) {
2998
3221
  this.device.markProtocolV2Reboot(rebootType);
2999
3222
  return { message: 'Device rebooted successfully' };
3000
3223
  }