@onekeyfe/hd-core 1.2.0-alpha.53 → 1.2.0-alpha.54

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 (251) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +0 -78
  2. package/__tests__/DeviceCommands.test.ts +0 -45
  3. package/__tests__/DeviceEventRegistration.test.ts +0 -6
  4. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +3 -198
  5. package/__tests__/connectSettings.test.ts +47 -5
  6. package/__tests__/device-initialize-timeout.test.ts +56 -0
  7. package/__tests__/device-lifecycle-events.test.ts +3 -105
  8. package/__tests__/firmware-update/check-all-firmware-release.test.ts +175 -0
  9. package/__tests__/firmware-update/firmware-artifact-source.test.ts +119 -0
  10. package/__tests__/firmware-update/firmware-host-binding.test.ts +70 -0
  11. package/__tests__/firmware-update/firmware-preparation-error.test.ts +27 -0
  12. package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +200 -0
  13. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +13 -0
  14. package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +294 -0
  15. package/__tests__/firmware-update/firmware-update-plan.test.ts +593 -0
  16. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +213 -0
  17. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +59 -31
  18. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +58 -0
  19. package/__tests__/firmware-update/firmware-upload-source.test.ts +70 -0
  20. package/__tests__/get-device-state.test.ts +12 -132
  21. package/__tests__/homescreen.test.ts +1 -41
  22. package/__tests__/logBlockEvent.test.ts +1 -1
  23. package/__tests__/method-protocol-support.test.ts +0 -19
  24. package/__tests__/open-wallet-session.test.ts +33 -446
  25. package/__tests__/protocol-v2-unlock-policy.test.ts +35 -201
  26. package/__tests__/protocol-v2.test.ts +1034 -1378
  27. package/__tests__/protocolV2UiInteraction.test.ts +0 -63
  28. package/__tests__/public-pro2-api-boundary.test.ts +0 -1
  29. package/__tests__/search-devices.test.ts +3 -12
  30. package/dist/api/BaseMethod.d.ts +1 -3
  31. package/dist/api/BaseMethod.d.ts.map +1 -1
  32. package/dist/api/CheckAllFirmwareRelease.d.ts +1 -2
  33. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  34. package/dist/api/FirmwareUpdate.d.ts.map +1 -1
  35. package/dist/api/FirmwareUpdateV2.d.ts +11 -0
  36. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  37. package/dist/api/FirmwareUpdateV3.d.ts +7 -3
  38. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  39. package/dist/api/FirmwareUpdateV4.d.ts +25 -11
  40. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  41. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  42. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  43. package/dist/api/SearchDevices.d.ts.map +1 -1
  44. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  45. package/dist/api/device/DeviceUnlock.d.ts +2 -2
  46. package/dist/api/device/DeviceUnlock.d.ts.map +1 -1
  47. package/dist/api/device/DeviceUpdateBootloader.d.ts +5 -1
  48. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  49. package/dist/api/firmware/FirmwareArtifactSource.d.ts +25 -0
  50. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -0
  51. package/dist/api/firmware/FirmwareHostBinding.d.ts +6 -0
  52. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -0
  53. package/dist/api/firmware/FirmwarePreparationError.d.ts +3 -0
  54. package/dist/api/firmware/FirmwarePreparationError.d.ts.map +1 -0
  55. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +9 -0
  56. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  57. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +3 -0
  58. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +1 -0
  59. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +25 -0
  60. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -0
  61. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +32 -0
  62. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -0
  63. package/dist/api/firmware/getBinary.d.ts +7 -0
  64. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  65. package/dist/api/firmware/progressThrottle.d.ts +3 -0
  66. package/dist/api/firmware/progressThrottle.d.ts.map +1 -0
  67. package/dist/api/firmware/updateBootloader.d.ts +2 -0
  68. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  69. package/dist/api/firmware/uploadFirmware.d.ts +9 -1
  70. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  71. package/dist/api/helpers/protocolV2FileWrite.d.ts +0 -2
  72. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  73. package/dist/api/index.d.ts +0 -2
  74. package/dist/api/index.d.ts.map +1 -1
  75. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -1
  76. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  77. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  78. package/dist/core/deviceEventRegistration.d.ts +0 -2
  79. package/dist/core/deviceEventRegistration.d.ts.map +1 -1
  80. package/dist/core/index.d.ts.map +1 -1
  81. package/dist/data-manager/DataManager.d.ts +2 -4
  82. package/dist/data-manager/DataManager.d.ts.map +1 -1
  83. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  84. package/dist/device/Device.d.ts +6 -29
  85. package/dist/device/Device.d.ts.map +1 -1
  86. package/dist/device/DeviceCommands.d.ts +4 -4
  87. package/dist/device/DeviceCommands.d.ts.map +1 -1
  88. package/dist/device/DevicePool.d.ts.map +1 -1
  89. package/dist/events/device.d.ts +0 -4
  90. package/dist/events/device.d.ts.map +1 -1
  91. package/dist/events/logBlockEvent.d.ts.map +1 -1
  92. package/dist/events/ui-promise.d.ts +2 -4
  93. package/dist/events/ui-promise.d.ts.map +1 -1
  94. package/dist/events/ui-request.d.ts +2 -31
  95. package/dist/events/ui-request.d.ts.map +1 -1
  96. package/dist/events/ui-response.d.ts +2 -10
  97. package/dist/events/ui-response.d.ts.map +1 -1
  98. package/dist/index.d.ts +231 -214
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +3497 -2559
  101. package/dist/inject.d.ts +1 -6
  102. package/dist/inject.d.ts.map +1 -1
  103. package/dist/lowLevelInject.d.ts.map +1 -1
  104. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  105. package/dist/protocols/protocol-v2/lockedError.d.ts +2 -0
  106. package/dist/protocols/protocol-v2/lockedError.d.ts.map +1 -0
  107. package/dist/protocols/protocol-v2/uiInteraction.d.ts +4 -7
  108. package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -1
  109. package/dist/protocols/protocol-v2/unlockPolicy.d.ts +3 -1
  110. package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -1
  111. package/dist/protocols/protocol-v2/unlockRetry.d.ts +10 -0
  112. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -0
  113. package/dist/protocols/protocol-v2/walletSession.d.ts +1 -5
  114. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  115. package/dist/topLevelInject.d.ts.map +1 -1
  116. package/dist/types/api/checkAllFirmwareRelease.d.ts +4 -2
  117. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  118. package/dist/types/api/deviceUnlock.d.ts +1 -5
  119. package/dist/types/api/deviceUnlock.d.ts.map +1 -1
  120. package/dist/types/api/deviceUpdateBootloader.d.ts +6 -0
  121. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  122. package/dist/types/api/export.d.ts +4 -2
  123. package/dist/types/api/export.d.ts.map +1 -1
  124. package/dist/types/api/firmwareUpdate.d.ts +70 -2
  125. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  126. package/dist/types/api/firmwareUpdateCapabilities.d.ts +9 -0
  127. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +1 -0
  128. package/dist/types/api/firmwareUpdatePlan.d.ts +28 -0
  129. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -0
  130. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +42 -0
  131. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +1 -0
  132. package/dist/types/api/index.d.ts +10 -7
  133. package/dist/types/api/index.d.ts.map +1 -1
  134. package/dist/types/api/protocolV2.d.ts +0 -3
  135. package/dist/types/api/protocolV2.d.ts.map +1 -1
  136. package/dist/types/device.d.ts.map +1 -1
  137. package/dist/types/params.d.ts +0 -1
  138. package/dist/types/params.d.ts.map +1 -1
  139. package/dist/types/settings.d.ts +13 -22
  140. package/dist/types/settings.d.ts.map +1 -1
  141. package/dist/utils/deviceFeaturesCompat.d.ts +1 -0
  142. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  143. package/dist/utils/deviceFeaturesUtils.d.ts +0 -4
  144. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  145. package/dist/utils/homescreen.d.ts +0 -4
  146. package/dist/utils/homescreen.d.ts.map +1 -1
  147. package/dist/utils/index.d.ts +1 -1
  148. package/dist/utils/index.d.ts.map +1 -1
  149. package/dist/utils/patch.d.ts +1 -1
  150. package/dist/utils/patch.d.ts.map +1 -1
  151. package/dist/utils/pro2Wallpaper.d.ts +0 -10
  152. package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
  153. package/package.json +4 -4
  154. package/src/api/BaseMethod.ts +10 -8
  155. package/src/api/CheckAllFirmwareRelease.ts +57 -54
  156. package/src/api/FirmwareUpdate.ts +15 -7
  157. package/src/api/FirmwareUpdateV2.ts +316 -126
  158. package/src/api/FirmwareUpdateV3.ts +265 -63
  159. package/src/api/FirmwareUpdateV4.ts +965 -566
  160. package/src/api/GetPassphraseState.ts +1 -7
  161. package/src/api/OpenWalletSession.ts +17 -54
  162. package/src/api/SearchDevices.ts +1 -3
  163. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +11 -32
  164. package/src/api/device/DeviceUnlock.ts +3 -8
  165. package/src/api/device/DeviceUpdateBootloader.ts +122 -6
  166. package/src/api/firmware/FirmwareArtifactSource.ts +278 -0
  167. package/src/api/firmware/FirmwareHostBinding.ts +100 -0
  168. package/src/api/firmware/FirmwarePreparationError.ts +12 -0
  169. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +83 -16
  170. package/src/api/firmware/FirmwareUpdateCapabilities.ts +9 -0
  171. package/src/api/firmware/FirmwareUpdatePlan.ts +772 -0
  172. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +415 -0
  173. package/src/api/firmware/progressThrottle.ts +44 -0
  174. package/src/api/firmware/updateBootloader.ts +19 -4
  175. package/src/api/firmware/uploadFirmware.ts +144 -39
  176. package/src/api/helpers/protocolV2FileWrite.ts +4 -11
  177. package/src/api/index.ts +0 -2
  178. package/src/api/protocol-v2/ProtocolInfoRequest.ts +1 -3
  179. package/src/api/protocol-v2/helpers.ts +0 -1
  180. package/src/core/deviceEventRegistration.ts +0 -4
  181. package/src/core/index.ts +83 -167
  182. package/src/data/messages/messages-protocol-v2.json +0 -25
  183. package/src/data-manager/DataManager.ts +38 -67
  184. package/src/data-manager/connectSettings.ts +11 -0
  185. package/src/device/Device.ts +42 -258
  186. package/src/device/DeviceCommands.ts +1 -7
  187. package/src/device/DevicePool.ts +2 -7
  188. package/src/events/device.ts +0 -4
  189. package/src/events/logBlockEvent.ts +0 -1
  190. package/src/events/ui-promise.ts +2 -4
  191. package/src/events/ui-request.ts +2 -36
  192. package/src/events/ui-response.ts +2 -12
  193. package/src/index.ts +5 -0
  194. package/src/inject.ts +24 -60
  195. package/src/lowLevelInject.ts +8 -7
  196. package/src/protocols/protocol-v2/features.ts +3 -10
  197. package/src/protocols/protocol-v2/lockedError.ts +10 -0
  198. package/src/protocols/protocol-v2/uiInteraction.ts +6 -46
  199. package/src/protocols/protocol-v2/unlockPolicy.ts +105 -1
  200. package/src/protocols/protocol-v2/unlockRetry.ts +109 -0
  201. package/src/protocols/protocol-v2/walletSession.ts +30 -180
  202. package/src/topLevelInject.ts +8 -7
  203. package/src/types/api/checkAllFirmwareRelease.ts +4 -2
  204. package/src/types/api/deviceUnlock.ts +1 -12
  205. package/src/types/api/deviceUpdateBootloader.ts +6 -0
  206. package/src/types/api/export.ts +18 -1
  207. package/src/types/api/firmwareUpdate.ts +76 -3
  208. package/src/types/api/firmwareUpdateCapabilities.ts +9 -0
  209. package/src/types/api/firmwareUpdatePlan.ts +38 -0
  210. package/src/types/api/firmwareUpdatePreparedPlan.ts +53 -0
  211. package/src/types/api/index.ts +14 -13
  212. package/src/types/api/protocolV2.ts +0 -13
  213. package/src/types/device.ts +0 -3
  214. package/src/types/params.ts +1 -7
  215. package/src/types/settings.ts +13 -42
  216. package/src/utils/deviceFeaturesCompat.ts +6 -0
  217. package/src/utils/deviceFeaturesUtils.ts +0 -8
  218. package/src/utils/homescreen.ts +6 -32
  219. package/src/utils/index.ts +1 -6
  220. package/src/utils/pro2Wallpaper.ts +8 -35
  221. package/__tests__/deviceUploadNft.test.ts +0 -293
  222. package/__tests__/pro2Nft.test.ts +0 -108
  223. package/__tests__/protocol-binding.test.ts +0 -89
  224. package/__tests__/protocol-v2-resources.test.ts +0 -284
  225. package/__tests__/protocol-v2-ui-lifecycle.test.ts +0 -91
  226. package/__tests__/uiProgressThrottle.test.ts +0 -74
  227. package/__tests__/uiPromiseRegistry.test.ts +0 -86
  228. package/dist/api/DetectDeviceConnectProtocol.d.ts +0 -7
  229. package/dist/api/DetectDeviceConnectProtocol.d.ts.map +0 -1
  230. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +0 -31
  231. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +0 -1
  232. package/dist/core/uiPromiseRegistry.d.ts +0 -4
  233. package/dist/core/uiPromiseRegistry.d.ts.map +0 -1
  234. package/dist/protocols/protocol-v2/resources.d.ts +0 -30
  235. package/dist/protocols/protocol-v2/resources.d.ts.map +0 -1
  236. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +0 -22
  237. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +0 -1
  238. package/dist/types/api/detectDeviceConnectProtocol.d.ts +0 -4
  239. package/dist/types/api/detectDeviceConnectProtocol.d.ts.map +0 -1
  240. package/dist/utils/pro2Nft.d.ts +0 -31
  241. package/dist/utils/pro2Nft.d.ts.map +0 -1
  242. package/dist/utils/uiProgressThrottle.d.ts +0 -3
  243. package/dist/utils/uiProgressThrottle.d.ts.map +0 -1
  244. package/src/api/DetectDeviceConnectProtocol.ts +0 -18
  245. package/src/api/protocol-v2/DeviceUploadNft.ts +0 -189
  246. package/src/core/uiPromiseRegistry.ts +0 -41
  247. package/src/protocols/protocol-v2/resources.ts +0 -359
  248. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +0 -118
  249. package/src/types/api/detectDeviceConnectProtocol.ts +0 -7
  250. package/src/utils/pro2Nft.ts +0 -142
  251. package/src/utils/uiProgressThrottle.ts +0 -63
@@ -2,6 +2,7 @@ import { EDeviceType, ERRORS, HardwareError, HardwareErrorCode, wait } from '@on
2
2
  import {
3
3
  DeviceRebootType,
4
4
  PROTOCOL_V2_BLE_FILE_CHUNK_SIZE,
5
+ PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE,
5
6
  PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE,
6
7
  } from '@onekeyfe/hd-transport';
7
8
  import { sha256 } from '@noble/hashes/sha256';
@@ -18,6 +19,7 @@ import {
18
19
  getLogger,
19
20
  } from '../utils';
20
21
  import { getSysResourceBinary } from './firmware/getBinary';
22
+ import { normalizeFirmwarePreparationError } from './firmware/FirmwarePreparationError';
21
23
  import { DataManager } from '../data-manager';
22
24
  import { FirmwareUpdateBaseMethod } from './firmware/FirmwareUpdateBaseMethod';
23
25
  import { DevicePool } from '../device/DevicePool';
@@ -26,41 +28,41 @@ import {
26
28
  ProtocolV2FirmwareTargetType,
27
29
  } from '../protocols/protocol-v2';
28
30
  import { requestProtocolV2DeviceInfo } from '../protocols/protocol-v2/features';
29
- import {
30
- PROTOCOL_V2_RESOURCE_DEVICE_PATHS,
31
- buildProtocolV2ResourceUpdatePlan,
32
- isProtocolV2ResourceFileValid,
33
- readProtocolV2ResourceInventory,
34
- } from '../protocols/protocol-v2/resources';
35
31
  import {
36
32
  getProtocolV2UnknownErrorText,
37
33
  isProtocolV2DeviceDisconnectedError,
38
34
  } from './protocol-v2/helpers';
35
+ import { openFirmwareByteSource, writeFirmwareByteSource } from './firmware/FirmwareArtifactSource';
36
+ import { resolveFirmwareUpdateHostBinding } from './firmware/FirmwareHostBinding';
37
+ import {
38
+ assertFirmwareUpdatePreparedPlanBinding,
39
+ assertFirmwareUpdatePreparedPlanDeviceIdentity,
40
+ } from './firmware/FirmwareUpdatePreparedPlan';
39
41
 
40
- import type { FirmwareUpdateV4Params, FirmwareUpdateV4Target } from '../types/api/firmwareUpdate';
42
+ import type {
43
+ FirmwareArtifactReference,
44
+ FirmwareUpdateV4Params,
45
+ FirmwareUpdateV4Target,
46
+ } from '../types/api/firmwareUpdate';
41
47
  import type { EFirmwareType } from '@onekeyfe/hd-shared';
42
48
  import type { ProtocolV2DeviceInfo } from '@onekeyfe/hd-transport';
43
- import type { PROTO } from '../constants';
44
49
  import type { TypedResponseMessage } from '../device/DeviceCommands';
45
50
  import type {
46
51
  Features,
47
52
  IFirmwareReleaseInfo,
48
- IProtocolV2BootResources,
49
53
  IProtocolV2FirmwareComponent,
50
- IProtocolV2Resource,
51
54
  IVersionArray,
52
55
  } from '../types';
56
+ import type { FirmwareByteSource } from './firmware/FirmwareArtifactSource';
53
57
 
54
58
  const Log = getLogger(LoggerNames.Method);
55
59
 
56
60
  const SESSION_ERROR = 'session not found';
57
- const PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT = 90 * 1000;
58
- const PROTOCOL_V2_FINAL_RECONNECT_TIMEOUT = 3 * 60 * 1000;
61
+ const PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT = 60 * 1000;
62
+ const PROTOCOL_V2_FINAL_RECONNECT_TIMEOUT = 2 * 60 * 1000;
59
63
  const PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT = 5 * 1000;
60
- const PROTOCOL_V2_FIRMWARE_STATUS_RESPONSE_TIMEOUT = 15 * 1000;
61
64
  const PROTOCOL_V2_START_UPDATE_TIMEOUT = 3 * 60 * 1000;
62
- const PROTOCOL_V2_INSTALL_TIMEOUT = 8 * 60 * 1000;
63
- const PROTOCOL_V2_MISSING_TARGET_STATUS_GRACE_TIMEOUT = 30 * 1000;
65
+ const PROTOCOL_V2_INSTALL_TIMEOUT = 5 * 60 * 1000;
64
66
  const PROTOCOL_V2_TARGET_STATUS_PENDING = 0;
65
67
  const PROTOCOL_V2_TARGET_STATUS_IN_PROGRESS = 1;
66
68
  const PROTOCOL_V2_TARGET_STATUS_FINISHED = 2;
@@ -71,13 +73,12 @@ const PROTOCOL_V2_MIN_FILE_CHUNK_SIZE = 64;
71
73
  const PROTOCOL_V2_CONNECT_RETRY_COUNT = 10;
72
74
  const PROTOCOL_V2_CONNECT_POLL_INTERVAL = 500;
73
75
  const PROTOCOL_V2_CONNECT_SINGLE_TIMEOUT = 75 * 1000;
74
- const PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT = 30 * 1000;
76
+ const PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT = 60 * 1000;
75
77
  const PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT = 3;
76
78
  const PROTOCOL_V2_OKPP_HEADER_SIZE = 0x52a0;
77
79
  const PROTOCOL_V2_OKPP_PAYLOAD_HASH_OFFSET = 0x200;
78
80
  const PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET = 0x240;
79
81
  const PROTOCOL_V2_OKPP_HASH_SIZE = 64;
80
- const PROTOCOL_V2_BOOT_RESOURCES_FILE_NAME = 'boot_resources.crate.okpkg';
81
82
 
82
83
  const getProtocolV2DeviceTransferProgress = (
83
84
  bytesBeforeChunk: number,
@@ -96,11 +97,6 @@ const getProtocolV2DeviceTransferProgress = (
96
97
  return Math.min(Math.max(Math.ceil((bytesAfterChunk / totalBytes) * 100), 1), 99);
97
98
  };
98
99
 
99
- const formatProtocolV2TransferSpeed = (bytes: number, elapsedMs: number) => {
100
- const safeElapsedMs = Math.max(elapsedMs, 1);
101
- return (bytes / 1024 / (safeElapsedMs / 1000)).toFixed(2);
102
- };
103
-
104
100
  type ProtocolV2FirmwareUpdateStatusTarget = {
105
101
  target_id: number | string;
106
102
  status?: number | string;
@@ -112,24 +108,18 @@ type ProtocolV2FirmwareUpdateStartResponse = TypedResponseMessage<'Success'>;
112
108
 
113
109
  type ProtocolV2TargetBinary = { fileName: string; binary: ArrayBuffer; targetId: number };
114
110
  type ProtocolV2InstallItem = ProtocolV2TargetBinary & {
115
- kind: ProtocolV2RemoteComponentTarget['kind'] | 'boot_resources';
116
- };
117
- type ProtocolV2InstallTarget = ProtocolV2InstallItem & {
118
- path: string;
119
- };
120
-
121
- type ProtocolV2FileTransferParams = PROTO.FirmwareUpload & {
122
- filePath: string;
123
- processedSize?: number;
124
- totalSize?: number;
125
- onTransferredBytes?: (transferredBytes: number) => void;
111
+ kind: ProtocolV2RemoteComponentTarget['kind'];
126
112
  };
127
-
128
113
  /** RESC bundle okpkg written independently to devicePath through FileWrite. */
129
114
  type ProtocolV2ResourceBundleBinary = {
130
115
  name: string;
131
116
  binary: ArrayBuffer;
132
117
  devicePath: string;
118
+ /** Download URL for remote-config mode; omitted in manual mode. */
119
+ url?: string;
120
+ version?: IVersionArray;
121
+ payloadHash?: string;
122
+ headerHash?: string;
133
123
  };
134
124
 
135
125
  type ProtocolV2RemoteComponentBinary = ProtocolV2RemoteComponentTarget & {
@@ -142,6 +132,35 @@ type ProtocolV2RemoteComponentTarget = {
142
132
  kind: 'bootloader' | 'firmware';
143
133
  };
144
134
 
135
+ type ProtocolV2InstallSource = {
136
+ fileName: string;
137
+ source: FirmwareByteSource;
138
+ targetId: number;
139
+ kind: ProtocolV2RemoteComponentTarget['kind'];
140
+ };
141
+
142
+ type ProtocolV2ResourceBundleSource = {
143
+ name: string;
144
+ source: FirmwareByteSource;
145
+ devicePath: string;
146
+ version?: IVersionArray;
147
+ payloadHash?: string;
148
+ headerHash?: string;
149
+ };
150
+
151
+ type ProtocolV2ExecutionPhaseKind =
152
+ | 'resource-sync'
153
+ | 'bootloader-install'
154
+ | 'bootloader-verify'
155
+ | 'component-install'
156
+ | 'final-verify';
157
+
158
+ type ProtocolV2ExecutionPhase = {
159
+ kind: ProtocolV2ExecutionPhaseKind;
160
+ installSources: ProtocolV2InstallSource[];
161
+ resourceSources: ProtocolV2ResourceBundleSource[];
162
+ };
163
+
145
164
  type ProtocolV2OkppHeader = {
146
165
  type: string;
147
166
  version: IVersionArray;
@@ -281,6 +300,18 @@ const normalizeProtocolV2TargetStatus = (
281
300
 
282
301
  const normalizeProtocolV2Hex = (value?: string) => value?.replace(/^0x/i, '').toLowerCase();
283
302
 
303
+ const versionArrayToNumber = (version?: IVersionArray) => {
304
+ if (!version) return undefined;
305
+ return version[0] * 0x10000 + version[1] * 0x100 + version[2];
306
+ };
307
+
308
+ const compareProtocolV2Versions = (current?: IVersionArray, target?: IVersionArray) => {
309
+ const currentNumber = versionArrayToNumber(current);
310
+ const targetNumber = versionArrayToNumber(target);
311
+ if (currentNumber === undefined || targetNumber === undefined) return undefined;
312
+ return currentNumber - targetNumber;
313
+ };
314
+
284
315
  const bytesToHex = (bytes: Uint8Array) =>
285
316
  Array.from(bytes)
286
317
  .map(byte => byte.toString(16).padStart(2, '0'))
@@ -373,10 +404,11 @@ export const assertProtocolV2ReconnectIdentity = (
373
404
  expectedSerialNumber?: string,
374
405
  actualSerialNumber?: string
375
406
  ) => {
376
- // Some Pro2 loader builds omit serial_no. Reconnect still uses the same BLE
377
- // descriptor or requires a uniquely enumerated USB device before this check.
378
407
  if (!expectedSerialNumber || !actualSerialNumber) {
379
- return;
408
+ throw ERRORS.TypedError(
409
+ HardwareErrorCode.DeviceNotFound,
410
+ 'Protocol V2 reconnect physical identity is unavailable'
411
+ );
380
412
  }
381
413
  if (actualSerialNumber !== expectedSerialNumber) {
382
414
  throw ERRORS.TypedError(
@@ -401,6 +433,16 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
401
433
  return ['V2'] as const;
402
434
  }
403
435
 
436
+ private protocolV2PreparedSources: FirmwareByteSource[] = [];
437
+
438
+ private protocolV2ExecutionInLoader = false;
439
+
440
+ private protocolV2CompletedTargetVersions = new Map<number, number>();
441
+
442
+ private protocolV2LatestFinalFeatures?: Features;
443
+
444
+ private protocolV2FinalStatusVerified = false;
445
+
404
446
  init() {
405
447
  this.allowDeviceMode = [UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE];
406
448
  this.requireDeviceMode = [];
@@ -427,7 +469,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
427
469
  { name: 'chunkSize', type: 'number' },
428
470
  { name: 'forcedUpdateRes', type: 'boolean' },
429
471
  { name: 'bootloaderBinary', type: 'buffer' },
430
- { name: 'bootResourcesBinary', type: 'buffer' },
431
472
  { name: 'romloaderBinary', type: 'buffer' },
432
473
  { name: 'applicationP1Binary', type: 'buffer' },
433
474
  { name: 'applicationP2Binary', type: 'buffer' },
@@ -439,14 +480,65 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
439
480
  { name: 'firmwareType', type: 'string' },
440
481
  { name: 'targetsToUpdate', type: 'array', allowEmpty: true },
441
482
  { name: 'platform', type: 'string' },
483
+ { name: 'expectedDeviceId', type: 'string' },
442
484
  { name: 'resourceBundleFiles', type: 'array', allowEmpty: true },
443
485
  ]);
486
+ if (
487
+ payload.expectedDeviceId !== undefined &&
488
+ (payload.expectedDeviceId.length === 0 || payload.expectedDeviceId.length > 160)
489
+ ) {
490
+ throw ERRORS.TypedError(
491
+ HardwareErrorCode.CallMethodInvalidParameter,
492
+ 'Protocol V2 expected device identity is invalid'
493
+ );
494
+ }
495
+ const hostBinding =
496
+ payload.preparedPlan || payload.componentArtifacts || payload.resourceBundleArtifacts
497
+ ? resolveFirmwareUpdateHostBinding(payload.hostBindingGeneration)
498
+ : undefined;
499
+ if (hostBinding) {
500
+ assertFirmwareUpdatePreparedPlanBinding({
501
+ preparedPlan: payload.preparedPlan,
502
+ executor: 'v4',
503
+ platform: payload.platform,
504
+ scopeTargets: [
505
+ 'boot',
506
+ 'app_v1',
507
+ 'app_v2',
508
+ 'coprocessor',
509
+ 'resource',
510
+ 'se01',
511
+ 'se02',
512
+ 'se03',
513
+ 'se04',
514
+ ],
515
+ bindings: [
516
+ ...Object.entries(payload.componentArtifacts ?? {}).flatMap(([target, artifact]) =>
517
+ artifact
518
+ ? [
519
+ {
520
+ target: target as Exclude<FirmwareUpdateV4Target, 'resource'>,
521
+ artifact,
522
+ },
523
+ ]
524
+ : []
525
+ ),
526
+ ...(payload.resourceBundleArtifacts ?? []).map(
527
+ (entry: { name: string; artifact: FirmwareArtifactReference }) => ({
528
+ target: 'resource' as const,
529
+ logicalName: entry.name,
530
+ artifact: entry.artifact,
531
+ })
532
+ ),
533
+ ],
534
+ });
535
+ }
444
536
 
445
537
  this.params = {
538
+ preparedPlan: payload.preparedPlan,
446
539
  chunkSize: payload.chunkSize,
447
540
  forcedUpdateRes: payload.forcedUpdateRes,
448
541
  bootloaderBinary: payload.bootloaderBinary,
449
- bootResourcesBinary: payload.bootResourcesBinary,
450
542
  romloaderBinary: payload.romloaderBinary,
451
543
  applicationP1Binary: payload.applicationP1Binary,
452
544
  applicationP2Binary: payload.applicationP2Binary,
@@ -458,17 +550,25 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
458
550
  resourceBundleFiles: payload.resourceBundleFiles,
459
551
  firmwareType: payload.firmwareType,
460
552
  targetsToUpdate: payload.targetsToUpdate,
553
+ expectedTargetVersions: payload.expectedTargetVersions,
461
554
  platform: payload.platform,
555
+ expectedDeviceId: payload.expectedDeviceId,
556
+ artifactReader: hostBinding?.artifactReader ?? payload.artifactReader,
557
+ componentArtifacts: payload.componentArtifacts,
558
+ resourceBundleArtifacts: payload.resourceBundleArtifacts,
462
559
  };
463
560
  }
464
561
 
465
- private getProtocolV2FirmwareChunkSize() {
562
+ private getProtocolV2FirmwareChunkSize(direction: 'read' | 'write' = 'write') {
466
563
  const payloadChunkSize = Number(this.params?.chunkSize);
467
564
  const env = DataManager.getSettings('env');
468
565
  const isBle = this.params?.platform === 'native' || (env && DataManager.isBleConnect(env));
469
566
  let maxChunkSize = PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE;
470
567
  if (isBle) {
471
- maxChunkSize = PROTOCOL_V2_BLE_FILE_CHUNK_SIZE;
568
+ maxChunkSize =
569
+ direction === 'read'
570
+ ? PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE
571
+ : PROTOCOL_V2_BLE_FILE_CHUNK_SIZE;
472
572
  }
473
573
  if (!Number.isFinite(payloadChunkSize) || payloadChunkSize <= 0) {
474
574
  return maxChunkSize;
@@ -489,29 +589,36 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
489
589
  const deviceFeatures = await this.getProtocolV2DeviceFeatures();
490
590
  const deviceFirmwareType = getFirmwareType(deviceFeatures);
491
591
  const firmwareType = this.params.firmwareType ?? deviceFirmwareType;
492
- const needsRemoteResources =
493
- !this.params.resourceBundleFiles?.length &&
494
- !!this.params.targetsToUpdate?.includes('resource');
592
+ this.validateExpectedTargetVersions();
593
+
594
+ if (
595
+ (this.params.componentArtifacts && Object.keys(this.params.componentArtifacts).length > 0) ||
596
+ this.params.resourceBundleArtifacts?.length
597
+ ) {
598
+ return this.runProtocolV2PreparedArtifacts(deviceFeatures, firmwareType);
599
+ }
495
600
 
496
601
  let fwBinaryMap: ProtocolV2TargetBinary[] = [];
497
602
  let bootloaderBinary: ArrayBuffer | null = null;
498
- let bootResourcesInstallItem: ProtocolV2InstallItem | undefined;
499
603
  let installItems: ProtocolV2InstallItem[] | undefined;
500
604
  let resourceBundles: ProtocolV2ResourceBundleBinary[] | undefined;
501
605
  try {
502
606
  this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
503
- resourceBundles = this.prepareExplicitProtocolV2ResourceBundles();
504
607
  fwBinaryMap = this.collectExplicitTargetBinaries();
505
608
  bootloaderBinary = this.prepareBootloaderBinary();
506
- const needsRemoteFirmware = !this.hasExplicitProtocolV2Payload(fwBinaryMap);
507
- const needsRemoteBootResources =
508
- !this.params.bootResourcesBinary &&
509
- !!this.params.targetsToUpdate?.includes('boot_resources');
510
- if (needsRemoteFirmware || needsRemoteResources || needsRemoteBootResources) {
511
- // Remote updates must use a freshly fetched config before any reboot or file write.
512
- await DataManager.forceReloadData();
513
- }
514
- if (needsRemoteFirmware) {
609
+ if (!this.hasExplicitProtocolV2Payload(fwBinaryMap)) {
610
+ if (
611
+ this.params.artifactReader ||
612
+ DataManager.getSettings('firmwareManifestMode') === 'external-only'
613
+ ) {
614
+ throw ERRORS.TypedError(
615
+ HardwareErrorCode.RuntimeError,
616
+ 'Protocol V2 firmware artifacts must be prepared by the external firmware host',
617
+ {
618
+ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
619
+ }
620
+ );
621
+ }
515
622
  const remoteBinaries = await this.prepareRemoteProtocolV2Binaries(
516
623
  firmwareType,
517
624
  deviceFeatures
@@ -520,69 +627,308 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
520
627
  fwBinaryMap = remoteBinaries.fwBinaryMap;
521
628
  installItems = remoteBinaries.installItems;
522
629
  }
523
- bootResourcesInstallItem = await this.prepareProtocolV2BootResources();
524
- if (bootResourcesInstallItem) {
525
- installItems = [
526
- bootResourcesInstallItem,
527
- ...(installItems ?? this.buildProtocolV2InstallItems({ bootloaderBinary, fwBinaryMap })),
528
- ];
529
- }
530
- if (!needsRemoteResources) {
531
- this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
532
- }
630
+ const resourceDescriptors = this.prepareProtocolV2ResourceBundles(
631
+ firmwareType,
632
+ deviceFeatures
633
+ );
634
+ resourceBundles = resourceDescriptors?.length
635
+ ? await this.materializeProtocolV2ResourceBundles(resourceDescriptors)
636
+ : undefined;
637
+ this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
533
638
  } catch (err) {
534
- throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateDownloadFailed, err.message ?? err);
639
+ throw normalizeFirmwarePreparationError(err);
535
640
  }
536
641
 
537
- if (
538
- !bootloaderBinary &&
539
- fwBinaryMap.length === 0 &&
540
- !installItems?.length &&
541
- !resourceBundles?.length &&
542
- !needsRemoteResources
543
- ) {
642
+ if (!bootloaderBinary && fwBinaryMap.length === 0 && !resourceBundles?.length) {
544
643
  throw ERRORS.TypedError(
545
644
  HardwareErrorCode.FirmwareUpdateDownloadFailed,
546
645
  'No firmware to update'
547
646
  );
548
647
  }
549
648
 
550
- const enteredBootloader = await this.enterProtocolV2BootloaderMode();
551
-
552
- if (needsRemoteResources) {
553
- try {
554
- resourceBundles = await this.prepareProtocolV2ResourceBundles();
555
- this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
556
- } catch (err) {
557
- if (enteredBootloader) {
558
- try {
559
- await this.exitProtocolV2BootloaderToNormal();
560
- } catch (restoreError) {
561
- Log.warn(
562
- '[FirmwareUpdateV4] failed to restore App mode after resource preparation error:',
563
- restoreError
564
- );
565
- }
566
- }
567
- throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateDownloadFailed, err.message ?? err);
568
- }
569
- }
570
-
571
- await this.executeProtocolV2Update({
649
+ return this.executeProtocolV2Update({
572
650
  fwBinaryMap,
573
651
  bootloaderBinary,
574
652
  ...(installItems ? { installItems } : undefined),
575
653
  ...(resourceBundles?.length ? { resourceBundles } : undefined),
576
654
  });
655
+ }
577
656
 
578
- this.postTipMessage(FirmwareUpdateTipMessage.SwitchFirmwareReconnectDevice);
579
- await this.exitProtocolV2BootloaderToNormal();
657
+ private async openProtocolV2PreparedSource(artifact: FirmwareArtifactReference) {
658
+ const source = await openFirmwareByteSource({
659
+ artifact,
660
+ reader: this.params.artifactReader,
661
+ });
662
+ if (!source) {
663
+ throw ERRORS.TypedError(
664
+ HardwareErrorCode.RuntimeError,
665
+ `Firmware artifact ${artifact.artifactRef} is not prepared`,
666
+ {
667
+ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
668
+ }
669
+ );
670
+ }
671
+ this.protocolV2PreparedSources.push(source);
672
+ return source;
673
+ }
580
674
 
581
- const versions = await this.waitForProtocolV2FinalFeatures();
582
- this.postTipMessage(FirmwareUpdateTipMessage.FirmwareUpdateCompleted);
583
- DevicePool.resetState();
675
+ private async openProtocolV2MemorySource(binary: ArrayBuffer) {
676
+ const source = await openFirmwareByteSource({ binary });
677
+ if (!source) {
678
+ throw ERRORS.TypedError(
679
+ HardwareErrorCode.RuntimeError,
680
+ 'Protocol V2 firmware binary is empty'
681
+ );
682
+ }
683
+ this.protocolV2PreparedSources.push(source);
684
+ return source;
685
+ }
584
686
 
585
- return versions;
687
+ private async closeProtocolV2PreparedSources() {
688
+ const sources = this.protocolV2PreparedSources.splice(0);
689
+ await Promise.all(sources.map(source => source.close().catch(() => undefined)));
690
+ }
691
+
692
+ private async prepareProtocolV2InstallSources(
693
+ firmwareType: EFirmwareType,
694
+ features: Features
695
+ ): Promise<ProtocolV2InstallSource[]> {
696
+ const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
697
+ if (!release) {
698
+ throw ERRORS.TypedError(
699
+ HardwareErrorCode.RuntimeError,
700
+ 'Protocol V2 firmware release is unavailable'
701
+ );
702
+ }
703
+ const componentArtifacts = this.params.componentArtifacts ?? {};
704
+ const requestedTargets = new Set(
705
+ this.params.targetsToUpdate ?? (Object.keys(componentArtifacts) as FirmwareUpdateV4Target[])
706
+ );
707
+ const installSources: ProtocolV2InstallSource[] = [];
708
+ const preparedTargets = new Set<FirmwareUpdateV4Target>();
709
+
710
+ for (const [key, component] of this.getRemoteComponentEntries(release)) {
711
+ const target = this.getRemoteComponentTarget(key, component);
712
+ const updateTarget = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(target.targetId);
713
+ if (updateTarget && updateTarget !== 'resource' && requestedTargets.has(updateTarget)) {
714
+ const artifact = componentArtifacts[updateTarget];
715
+ if (!artifact) {
716
+ throw ERRORS.TypedError(
717
+ HardwareErrorCode.RuntimeError,
718
+ `Protocol V2 ${updateTarget} artifact is not prepared`,
719
+ {
720
+ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
721
+ artifactName: updateTarget,
722
+ }
723
+ );
724
+ }
725
+ installSources.push({
726
+ ...target,
727
+ source: await this.openProtocolV2PreparedSource(artifact),
728
+ });
729
+ preparedTargets.add(updateTarget);
730
+ }
731
+ }
732
+
733
+ const unknownTarget = Array.from(requestedTargets).find(
734
+ target => target !== 'resource' && !preparedTargets.has(target)
735
+ );
736
+ if (unknownTarget) {
737
+ throw ERRORS.TypedError(
738
+ HardwareErrorCode.RuntimeError,
739
+ `Protocol V2 release does not contain requested target ${unknownTarget}`
740
+ );
741
+ }
742
+ return installSources;
743
+ }
744
+
745
+ private async prepareProtocolV2ResourceSources(
746
+ firmwareType: EFirmwareType,
747
+ features: Features
748
+ ): Promise<ProtocolV2ResourceBundleSource[]> {
749
+ const preparedArtifacts = this.params.resourceBundleArtifacts ?? [];
750
+ const resourceRequested =
751
+ this.params.targetsToUpdate?.includes('resource') || preparedArtifacts.length > 0;
752
+ if (!resourceRequested) {
753
+ return [];
754
+ }
755
+ const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
756
+ const descriptors = release?.resourceBundles ?? [];
757
+ const artifactByName = new Map(
758
+ preparedArtifacts.map(item => [item.name, item.artifact] as const)
759
+ );
760
+ const sources: ProtocolV2ResourceBundleSource[] = [];
761
+ for (const descriptor of descriptors) {
762
+ const artifact = artifactByName.get(descriptor.name);
763
+ if (!artifact) {
764
+ throw ERRORS.TypedError(
765
+ HardwareErrorCode.RuntimeError,
766
+ `Protocol V2 resource bundle ${descriptor.name} is not prepared`,
767
+ {
768
+ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
769
+ artifactName: descriptor.name,
770
+ }
771
+ );
772
+ }
773
+ const devicePath = validateProtocolV2FilesystemPath(
774
+ descriptor.devicePath,
775
+ 'resourceBundles[].devicePath'
776
+ );
777
+ sources.push({
778
+ name: descriptor.name,
779
+ source: await this.openProtocolV2PreparedSource(artifact),
780
+ devicePath,
781
+ version: descriptor.version,
782
+ payloadHash: descriptor.payloadHash,
783
+ headerHash: descriptor.headerHash,
784
+ });
785
+ artifactByName.delete(descriptor.name);
786
+ }
787
+ if (artifactByName.size > 0) {
788
+ throw ERRORS.TypedError(
789
+ HardwareErrorCode.RuntimeError,
790
+ `Protocol V2 release does not contain resource bundle ${artifactByName.keys().next().value}`
791
+ );
792
+ }
793
+ return sources;
794
+ }
795
+
796
+ private async runProtocolV2PreparedArtifacts(features: Features, firmwareType: EFirmwareType) {
797
+ try {
798
+ this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
799
+ const installSources = await this.prepareProtocolV2InstallSources(firmwareType, features);
800
+ const resourceSources = await this.prepareProtocolV2ResourceSources(firmwareType, features);
801
+ if (installSources.length === 0 && resourceSources.length === 0) {
802
+ throw ERRORS.TypedError(
803
+ HardwareErrorCode.FirmwareUpdateDownloadFailed,
804
+ 'No firmware to update'
805
+ );
806
+ }
807
+ this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
808
+
809
+ return await this.executeProtocolV2SourceUpdate({
810
+ installSources,
811
+ resourceSources,
812
+ });
813
+ } finally {
814
+ await this.closeProtocolV2PreparedSources();
815
+ }
816
+ }
817
+
818
+ private validateExpectedTargetVersions() {
819
+ const expected = this.params.expectedTargetVersions;
820
+ if (expected === undefined) return;
821
+ if (typeof expected !== 'object' || expected === null || Array.isArray(expected)) {
822
+ throw ERRORS.TypedError(
823
+ HardwareErrorCode.RuntimeError,
824
+ 'Protocol V2 expected target versions are invalid'
825
+ );
826
+ }
827
+ for (const [target, version] of Object.entries(expected)) {
828
+ if (
829
+ !Array.from(PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.values()).includes(
830
+ target as FirmwareUpdateV4Target
831
+ ) ||
832
+ typeof version !== 'string' ||
833
+ !/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/u.test(version) ||
834
+ version.length > 64
835
+ ) {
836
+ throw ERRORS.TypedError(
837
+ HardwareErrorCode.RuntimeError,
838
+ 'Protocol V2 expected target version is invalid'
839
+ );
840
+ }
841
+ }
842
+ }
843
+
844
+ private getExpectedProtocolV2TargetVersion(targetId: number) {
845
+ const target = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(targetId);
846
+ const version = target ? this.params?.expectedTargetVersions?.[target] : undefined;
847
+ if (!version) return undefined;
848
+ const parts = version.split(/[+-]/u, 1)[0].split('.').map(Number);
849
+ if (
850
+ parts.length !== 3 ||
851
+ parts.some(part => !Number.isSafeInteger(part) || part < 0 || part > 0xff)
852
+ ) {
853
+ return undefined;
854
+ }
855
+ return parts[0] * 0x10000 + parts[1] * 0x100 + parts[2];
856
+ }
857
+
858
+ private assertExpectedProtocolV2Versions(targets?: readonly FirmwareUpdateV4Target[]) {
859
+ const expected = this.params?.expectedTargetVersions;
860
+ if (!expected) return;
861
+ const features = this.protocolV2LatestFinalFeatures;
862
+ const requestedTargets = this.getProtocolV2RequestedTargets();
863
+ const applicationTargets = requestedTargets.filter(
864
+ target => target === 'app_v1' || target === 'app_v2'
865
+ );
866
+ const visibleVersions: Partial<Record<FirmwareUpdateV4Target, string>> = {
867
+ boot: features ? getDeviceBootloaderVersion(features).join('.') : undefined,
868
+ coprocessor: features ? getDeviceBLEFirmwareVersion(features).join('.') : undefined,
869
+ se01: features?.se01Version ?? undefined,
870
+ se02: features?.se02Version ?? undefined,
871
+ se03: features?.se03Version ?? undefined,
872
+ se04: features?.se04Version ?? undefined,
873
+ };
874
+ if (features && applicationTargets.length === 1) {
875
+ visibleVersions[applicationTargets[0]] = getDeviceFirmwareVersion(features).join('.');
876
+ }
877
+ const expectedEntries = (
878
+ Object.entries(expected) as Array<[FirmwareUpdateV4Target, string]>
879
+ ).filter(([target]) => !targets || targets.includes(target));
880
+ for (const [target, expectedVersion] of expectedEntries) {
881
+ const targetId = Array.from(PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.entries()).find(
882
+ ([, mappedTarget]) => mappedTarget === target
883
+ )?.[0];
884
+ const statusVersion =
885
+ targetId === undefined ? undefined : this.protocolV2CompletedTargetVersions.get(targetId);
886
+ const observedVersion =
887
+ statusVersion === undefined
888
+ ? visibleVersions[target]
889
+ : `${Math.floor(statusVersion / 0x10000) % 0x100}.${
890
+ Math.floor(statusVersion / 0x100) % 0x100
891
+ }.${statusVersion % 0x100}`;
892
+ if (!observedVersion) {
893
+ if (this.protocolV2FinalStatusVerified) {
894
+ Log.warn(
895
+ `Protocol V2 target ${target} has no observable final version; completed target status is authoritative`
896
+ );
897
+ } else {
898
+ throw ERRORS.TypedError(
899
+ HardwareErrorCode.FirmwareVerificationFailed,
900
+ `Protocol V2 target ${target} has no observable final version after status fallback`
901
+ );
902
+ }
903
+ } else if (observedVersion !== expectedVersion) {
904
+ throw ERRORS.TypedError(
905
+ HardwareErrorCode.FirmwareVerificationFailed,
906
+ `Protocol V2 target ${target} reached ${observedVersion}, expected ${expectedVersion}`
907
+ );
908
+ }
909
+ }
910
+ }
911
+
912
+ private getProtocolV2RequestedTargets(): FirmwareUpdateV4Target[] {
913
+ if (this.params.targetsToUpdate?.length) {
914
+ return [...new Set(this.params.targetsToUpdate)];
915
+ }
916
+ const targets = new Set<FirmwareUpdateV4Target>();
917
+ Object.keys(this.params.componentArtifacts ?? {}).forEach(target =>
918
+ targets.add(target as FirmwareUpdateV4Target)
919
+ );
920
+ if (this.params.resourceBundleArtifacts?.length || this.params.resourceBundleFiles?.length) {
921
+ targets.add('resource');
922
+ }
923
+ if (this.params.bootloaderBinary) targets.add('boot');
924
+ if (this.params.applicationP1Binary) targets.add('app_v1');
925
+ if (this.params.applicationP2Binary) targets.add('app_v2');
926
+ if (this.params.coprocessorBinary) targets.add('coprocessor');
927
+ if (this.params.se01Binary) targets.add('se01');
928
+ if (this.params.se02Binary) targets.add('se02');
929
+ if (this.params.se03Binary) targets.add('se03');
930
+ if (this.params.se04Binary) targets.add('se04');
931
+ return Array.from(targets);
586
932
  }
587
933
 
588
934
  private async getProtocolV2DeviceFeatures(): Promise<Features> {
@@ -608,23 +954,26 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
608
954
  );
609
955
  }
610
956
 
611
- private getProtocolV2DeviceInfoTimeout() {
612
- return this.isBleReconnect()
613
- ? this.payload.protocolV2DeviceInfoTimeoutMs ?? PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT
614
- : PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT;
615
- }
616
-
617
957
  private async requestProtocolV2PhysicalIdentity() {
618
958
  return requestProtocolV2DeviceInfo({
619
959
  commands: this.device.getCommands(),
620
- timeoutMs: this.getProtocolV2DeviceInfoTimeout(),
960
+ timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
621
961
  });
622
962
  }
623
963
 
624
964
  private async captureProtocolV2PhysicalIdentity() {
625
965
  const deviceInfo = await this.requestProtocolV2PhysicalIdentity();
626
966
  const serialNumber = this.getProtocolV2SerialNumber(deviceInfo);
627
- assertProtocolV2ReconnectIdentity(serialNumber, serialNumber);
967
+ if (this.params?.preparedPlan) {
968
+ assertFirmwareUpdatePreparedPlanDeviceIdentity({
969
+ preparedPlan: this.params.preparedPlan,
970
+ deviceIdentity: serialNumber,
971
+ });
972
+ } else if (this.params?.expectedDeviceId) {
973
+ assertProtocolV2ReconnectIdentity(this.params?.expectedDeviceId, serialNumber);
974
+ } else {
975
+ assertProtocolV2ReconnectIdentity(serialNumber, serialNumber);
976
+ }
628
977
  this.protocolV2ExpectedSerialNumber = serialNumber;
629
978
  }
630
979
 
@@ -642,7 +991,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
642
991
  return (
643
992
  !!this.params.resourceBundleFiles?.length ||
644
993
  !!this.params.bootloaderBinary ||
645
- !!this.params.bootResourcesBinary ||
646
994
  fwBinaryMap.length > 0
647
995
  );
648
996
  }
@@ -704,6 +1052,57 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
704
1052
  return target;
705
1053
  }
706
1054
 
1055
+ private getProtocolV2ResourceFilePath(path: string) {
1056
+ if (path.startsWith('vol')) return path;
1057
+ if (path.startsWith('/')) return `vol0:${path}`;
1058
+ return `vol0:/${path}`;
1059
+ }
1060
+
1061
+ private async readProtocolV2DeviceFileHeader(path: string) {
1062
+ const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
1063
+ const filePath = this.getProtocolV2ResourceFilePath(path);
1064
+ const pathInfoRes = await typedCall('FilesystemPathInfoQuery', 'FilesystemPathInfo', {
1065
+ path: filePath,
1066
+ });
1067
+ const fileSize = toProtocolV2FiniteNumber(pathInfoRes.message?.size);
1068
+ if (
1069
+ !pathInfoRes.message?.exist ||
1070
+ pathInfoRes.message?.directory ||
1071
+ fileSize === undefined ||
1072
+ fileSize < PROTOCOL_V2_OKPP_HEADER_SIZE
1073
+ ) {
1074
+ return null;
1075
+ }
1076
+
1077
+ const chunkSize = this.getProtocolV2FirmwareChunkSize('read');
1078
+ const chunks: Uint8Array[] = [];
1079
+ let offset = 0;
1080
+ while (offset < PROTOCOL_V2_OKPP_HEADER_SIZE) {
1081
+ const readLen = Math.min(chunkSize, PROTOCOL_V2_OKPP_HEADER_SIZE - offset);
1082
+ const res = await typedCall('FilesystemFileRead', 'FilesystemFile', {
1083
+ file: {
1084
+ path: filePath,
1085
+ offset,
1086
+ total_size: 0,
1087
+ },
1088
+ chunk_len: readLen,
1089
+ ui_percentage: undefined,
1090
+ });
1091
+ const data = toProtocolV2Bytes(res.message?.data);
1092
+ if (data.byteLength === 0) return null;
1093
+ chunks.push(data);
1094
+ offset += data.byteLength;
1095
+ }
1096
+
1097
+ const headerBytes = new Uint8Array(offset);
1098
+ let cursor = 0;
1099
+ chunks.forEach(chunk => {
1100
+ headerBytes.set(chunk, cursor);
1101
+ cursor += chunk.byteLength;
1102
+ });
1103
+ return parseProtocolV2OkppHeader(headerBytes);
1104
+ }
1105
+
707
1106
  private async downloadRemoteProtocolV2Component(
708
1107
  key: string,
709
1108
  component: IProtocolV2FirmwareComponent
@@ -792,159 +1191,155 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
792
1191
  };
793
1192
  }
794
1193
 
795
- private validateProtocolV2BootResourcesBinary(
796
- binary: ArrayBuffer,
797
- resource?: IProtocolV2BootResources
798
- ) {
799
- if (resource && !isProtocolV2ResourceFileValid(binary, resource)) {
800
- throw new Error('Pro2 boot resources file verification failed');
801
- }
802
- const header = parseProtocolV2OkppHeader(toProtocolV2Bytes(binary));
803
- if (!header || header.type !== 'CRAT') {
804
- throw new Error('Invalid Pro2 boot resources CRATE header');
805
- }
806
- if (resource) {
807
- const expectedPayloadHash = normalizeProtocolV2Hex(resource.payloadHash);
808
- const expectedHeaderHash = normalizeProtocolV2Hex(resource.headerHash);
809
- if (header.payloadHash !== expectedPayloadHash) {
810
- throw new Error('Pro2 boot resources payload hash mismatch');
811
- }
812
- if (header.headerHash !== expectedHeaderHash) {
813
- throw new Error('Pro2 boot resources header hash mismatch');
814
- }
815
- }
816
- }
817
-
818
- private async prepareProtocolV2BootResources(): Promise<ProtocolV2InstallItem | undefined> {
819
- let binary = this.params.bootResourcesBinary;
820
- let resource: IProtocolV2BootResources | undefined;
1194
+ // ============================================================
1195
+ // Incremental RESC bundle sync through FileRead and FilesystemFileWrite
1196
+ // ============================================================
821
1197
 
822
- if (!binary) {
823
- if (!this.params.targetsToUpdate?.includes('boot_resources')) {
824
- return undefined;
825
- }
826
- resource = DataManager.getProtocolV2BootResources();
827
- if (!resource) {
828
- throw new Error('Missing Pro2 boot resources configuration');
829
- }
830
- Log.log('[FirmwareUpdateV4] downloading Pro2 boot resources CRATE');
831
- ({ binary } = await getSysResourceBinary(resource.url));
1198
+ /**
1199
+ * Prepare the RESC bundle list.
1200
+ *
1201
+ * Two modes, matching FirmwareUpdateV3 binary-versus-version behavior:
1202
+ * - resourceBundleFiles supplied: use binaries directly without version checks.
1203
+ * - Otherwise load release.resourceBundles from remote config.json; the binaries
1204
+ * are prefetched before reboot and synchronization skips matching device headers.
1205
+ */
1206
+ private prepareProtocolV2ResourceBundles(
1207
+ firmwareType: EFirmwareType,
1208
+ features: Features
1209
+ ): ProtocolV2ResourceBundleBinary[] | undefined {
1210
+ // Manual binaries are installed directly without comparison.
1211
+ if (this.params.resourceBundleFiles?.length) {
1212
+ return this.params.resourceBundleFiles.map((file, index) => {
1213
+ const devicePath = validateProtocolV2FilesystemPath(
1214
+ file.devicePath,
1215
+ `resourceBundleFiles[${index}].devicePath`
1216
+ );
1217
+ return {
1218
+ name: devicePath.split('/').pop() ?? devicePath,
1219
+ binary: file.binary,
1220
+ devicePath,
1221
+ };
1222
+ });
832
1223
  }
833
1224
 
834
- this.validateProtocolV2BootResourcesBinary(binary, resource);
835
- return {
836
- fileName: PROTOCOL_V2_BOOT_RESOURCES_FILE_NAME,
837
- binary,
838
- targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_CRATE,
839
- kind: 'boot_resources',
840
- };
841
- }
842
-
843
- private prepareExplicitProtocolV2ResourceBundles(): ProtocolV2ResourceBundleBinary[] | undefined {
844
- if (!this.params.resourceBundleFiles?.length) return undefined;
845
-
846
- return this.params.resourceBundleFiles.map((file, index) => {
847
- const devicePath = validateProtocolV2FilesystemPath(
848
- file.devicePath,
849
- `resourceBundleFiles[${index}].devicePath`
850
- );
851
- return {
852
- name: devicePath.split('/').pop() ?? devicePath,
853
- binary: file.binary,
854
- devicePath,
855
- };
856
- });
857
- }
858
-
859
- private async prepareProtocolV2ResourceBundles(): Promise<
860
- ProtocolV2ResourceBundleBinary[] | undefined
861
- > {
862
1225
  if (!this.params.targetsToUpdate?.includes('resource')) {
863
1226
  return undefined;
864
1227
  }
865
1228
 
866
- const resources = DataManager.getProtocolV2Resources();
867
- if (!resources?.length) {
868
- throw new Error('Missing Pro2 stable resource configuration');
869
- }
870
-
871
- const inventory = this.params.forcedUpdateRes
872
- ? undefined
873
- : await readProtocolV2ResourceInventory({
874
- commands: this.device.getCommands(),
875
- resources,
876
- chunkSize: this.getProtocolV2FirmwareChunkSize(),
877
- timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
878
- });
879
- const plan = buildProtocolV2ResourceUpdatePlan({
880
- resources,
881
- inventory,
882
- mode: 'bootloader-recovery',
883
- forced: this.params.forcedUpdateRes,
884
- });
885
- Log.log(
886
- `[FirmwareUpdateV4] Pro2 resource plan mode=bootloader-recovery status=${plan.status} count=${plan.resources.length}`
887
- );
888
-
889
- const bundles: ProtocolV2ResourceBundleBinary[] = [];
890
- for (const resource of plan.resources) {
891
- bundles.push(await this.downloadProtocolV2Resource(resource));
1229
+ // Remote-config bundles are downloaded and compared later.
1230
+ const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
1231
+ if (!release?.resourceBundles?.length) return undefined;
1232
+
1233
+ return release.resourceBundles.map((bundle, index) => ({
1234
+ name: bundle.name,
1235
+ binary: new ArrayBuffer(0),
1236
+ devicePath: validateProtocolV2FilesystemPath(
1237
+ bundle.devicePath,
1238
+ `resourceBundles[${index}].devicePath`
1239
+ ),
1240
+ url: bundle.url,
1241
+ version: bundle.version,
1242
+ payloadHash: bundle.payloadHash,
1243
+ headerHash: bundle.headerHash,
1244
+ })) as ProtocolV2ResourceBundleBinary[];
1245
+ }
1246
+
1247
+ private async materializeProtocolV2ResourceBundles(
1248
+ bundles: ProtocolV2ResourceBundleBinary[]
1249
+ ): Promise<ProtocolV2ResourceBundleBinary[]> {
1250
+ for (const bundle of bundles) {
1251
+ let { binary } = bundle;
1252
+ let downloadedFromRemote = false;
1253
+ if (binary.byteLength === 0) {
1254
+ if (!bundle.url) {
1255
+ throw ERRORS.TypedError(
1256
+ HardwareErrorCode.FirmwareUpdateDownloadFailed,
1257
+ `Missing Protocol V2 RESC bundle binary: ${bundle.name}`
1258
+ );
1259
+ }
1260
+ if (
1261
+ this.params.artifactReader ||
1262
+ DataManager.getSettings('firmwareManifestMode') === 'external-only'
1263
+ ) {
1264
+ throw ERRORS.TypedError(
1265
+ HardwareErrorCode.RuntimeError,
1266
+ `Firmware resource bundle ${bundle.name} is not prepared`,
1267
+ {
1268
+ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
1269
+ artifactName: bundle.name,
1270
+ }
1271
+ );
1272
+ }
1273
+ Log.log(`[FirmwareUpdateV4] downloading remote RESC bundle ${bundle.name}`);
1274
+ ({ binary } = await getSysResourceBinary(bundle.url));
1275
+ bundle.binary = binary;
1276
+ downloadedFromRemote = true;
1277
+ }
1278
+ if (bundle.binary.byteLength === 0) {
1279
+ throw ERRORS.TypedError(
1280
+ HardwareErrorCode.FirmwareUpdateDownloadFailed,
1281
+ `Protocol V2 RESC bundle is empty: ${bundle.name}`
1282
+ );
1283
+ }
1284
+ if (downloadedFromRemote) {
1285
+ const header = parseProtocolV2OkppHeader(toProtocolV2Bytes(binary));
1286
+ if (!header || header.type !== 'RESC') {
1287
+ throw new Error(`Invalid Protocol V2 RESC bundle header: ${bundle.name}`);
1288
+ }
1289
+ if (bundle.version && compareProtocolV2Versions(header.version, bundle.version) !== 0) {
1290
+ throw new Error(`Protocol V2 RESC bundle version mismatch: ${bundle.name}`);
1291
+ }
1292
+ const expectedPayloadHash = normalizeProtocolV2Hex(bundle.payloadHash);
1293
+ if (expectedPayloadHash && header.payloadHash !== expectedPayloadHash) {
1294
+ throw new Error(`Protocol V2 RESC bundle payload hash mismatch: ${bundle.name}`);
1295
+ }
1296
+ const expectedHeaderHash = normalizeProtocolV2Hex(bundle.headerHash);
1297
+ if (expectedHeaderHash && header.headerHash !== expectedHeaderHash) {
1298
+ throw new Error(`Protocol V2 RESC bundle header hash mismatch: ${bundle.name}`);
1299
+ }
1300
+ }
892
1301
  }
893
1302
  return bundles;
894
1303
  }
895
1304
 
896
- private async downloadProtocolV2Resource(
897
- resource: IProtocolV2Resource
898
- ): Promise<ProtocolV2ResourceBundleBinary> {
899
- Log.log(`[FirmwareUpdateV4] downloading Pro2 resource ${resource.type}`);
900
- const { binary } = await getSysResourceBinary(resource.url);
901
- if (!isProtocolV2ResourceFileValid(binary, resource)) {
902
- throw new Error(`Pro2 resource file verification failed: ${resource.type}`);
903
- }
904
- return {
905
- name: `${resource.type}.okpkg`,
906
- binary,
907
- devicePath: PROTOCOL_V2_RESOURCE_DEVICE_PATHS[resource.type],
908
- };
909
- }
910
-
911
- private async syncProtocolV2ResourceBundles(
912
- bundles: ProtocolV2ResourceBundleBinary[],
913
- firmwareSize: number
914
- ): Promise<{ processedSize: number; totalSize: number }> {
915
- const transferStartTime = Date.now();
916
- const transferTransport = this.getProtocolV2FirmwareTransferTransport();
1305
+ /**
1306
+ * Compare the existing okpkg OKPP header in remote-config mode.
1307
+ */
1308
+ private async isProtocolV2ResourceBundleUpToDate(
1309
+ bundle: Pick<
1310
+ ProtocolV2ResourceBundleBinary,
1311
+ 'name' | 'devicePath' | 'version' | 'payloadHash' | 'headerHash'
1312
+ >
1313
+ ): Promise<boolean> {
1314
+ if (this.params?.forcedUpdateRes) return false;
1315
+ if (!bundle.version && !bundle.payloadHash) return false;
917
1316
 
918
- // Write directly to the device through FileWrite.
919
- let totalSize = 0;
920
- for (const b of bundles) totalSize += b.binary.byteLength;
1317
+ try {
1318
+ const header = await this.readProtocolV2DeviceFileHeader(bundle.devicePath);
1319
+ if (!header) return false;
921
1320
 
922
- const transferTotalSize = totalSize + firmwareSize;
923
- let processedSize = 0;
924
- for (const bundle of bundles) {
925
- Log.log(
926
- `[FirmwareUpdateV4] syncing RESC bundle ${bundle.name} -> ${bundle.devicePath} bytes=${bundle.binary.byteLength}`
927
- );
928
- processedSize = await this.protocolV2CommonUpdateProcess({
929
- payload: bundle.binary,
930
- filePath: bundle.devicePath,
931
- processedSize,
932
- totalSize: transferTotalSize,
933
- });
1321
+ if (bundle.version) {
1322
+ const cmp = compareProtocolV2Versions(header.version, bundle.version);
1323
+ if (cmp === undefined || cmp !== 0) return false;
1324
+ }
1325
+ if (bundle.payloadHash) {
1326
+ const expected = normalizeProtocolV2Hex(bundle.payloadHash);
1327
+ if (expected && header.payloadHash !== expected) return false;
1328
+ }
1329
+ if (bundle.headerHash) {
1330
+ const expected = normalizeProtocolV2Hex(bundle.headerHash);
1331
+ if (expected && header.headerHash !== expected) return false;
1332
+ }
1333
+ return true;
1334
+ } catch (error) {
1335
+ Log.log(`[FirmwareUpdateV4] RESC bundle ${bundle.name} header check failed: `, error);
1336
+ return false;
934
1337
  }
935
-
936
- const elapsedMs = Date.now() - transferStartTime;
937
- Log.log(
938
- `[FirmwareUpdateV4] RESC bundle sync finished transport=${transferTransport} bytes=${totalSize} elapsed=${(
939
- elapsedMs / 1000
940
- ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(totalSize, elapsedMs)} KB/s`
941
- );
942
- return { processedSize, totalSize: transferTotalSize };
943
1338
  }
944
1339
 
945
1340
  private isProtocolV2BootloaderMode() {
946
1341
  if (typeof this.device.isBootloader === 'function') {
947
- return !!this.device.isBootloader();
1342
+ return this.device.isBootloader();
948
1343
  }
949
1344
  return (
950
1345
  this.device.features?.mode === 'bootloader' ||
@@ -967,10 +1362,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
967
1362
  // It rejects DeviceRebootType.Bootloader, so reuse the current connection.
968
1363
  if (this.isProtocolV2RomloaderMode()) {
969
1364
  Log.debug('Protocol V2 device is in romloader mode; start firmware update directly');
1365
+ this.protocolV2ExecutionInLoader = true;
970
1366
  return false;
971
1367
  }
972
1368
  if (this.isProtocolV2BootloaderMode()) {
973
1369
  Log.debug('Protocol V2 device is already in bootloader mode, skip reboot');
1370
+ this.protocolV2ExecutionInLoader = true;
974
1371
  return false;
975
1372
  }
976
1373
 
@@ -980,6 +1377,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
980
1377
  this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
981
1378
  await wait(1000);
982
1379
  await this.waitForProtocolV2BootloaderMode();
1380
+ this.protocolV2ExecutionInLoader = true;
983
1381
  return true;
984
1382
  } catch (error) {
985
1383
  if (error instanceof HardwareError) {
@@ -996,17 +1394,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
996
1394
  ) {
997
1395
  const startTime = Date.now();
998
1396
  let lastError: unknown;
999
- let shouldReconnect = true;
1000
1397
 
1001
1398
  while (Date.now() - startTime < timeout) {
1002
1399
  try {
1003
- if (shouldReconnect) {
1004
- await this.reconnectProtocolV2Device();
1005
- shouldReconnect = false;
1006
- }
1400
+ await this.reconnectProtocolV2Device();
1007
1401
  const deviceInfo = await requestProtocolV2DeviceInfo({
1008
1402
  commands: this.device.getCommands(),
1009
- timeoutMs: this.getProtocolV2DeviceInfoTimeout(),
1403
+ timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
1010
1404
  });
1011
1405
  this.assertProtocolV2DeviceInfoIdentity(deviceInfo);
1012
1406
  const features = await this.device.probeProtocolV2RuntimeState(
@@ -1021,7 +1415,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1021
1415
  if (this.isProtocolV2ReconnectIdentityError(error)) {
1022
1416
  throw error;
1023
1417
  }
1024
- shouldReconnect = true;
1025
1418
  lastError = error;
1026
1419
  Log.log('Protocol V2 bootloader mode not ready, polling reconnect: ', error);
1027
1420
  }
@@ -1074,6 +1467,98 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1074
1467
  return entries;
1075
1468
  }
1076
1469
 
1470
+ private buildProtocolV2ExecutionPhases({
1471
+ installSources,
1472
+ resourceSources,
1473
+ }: {
1474
+ installSources: ProtocolV2InstallSource[];
1475
+ resourceSources: ProtocolV2ResourceBundleSource[];
1476
+ }): ProtocolV2ExecutionPhase[] {
1477
+ const phases: ProtocolV2ExecutionPhase[] = [];
1478
+ if (resourceSources.length > 0) {
1479
+ phases.push({
1480
+ kind: 'resource-sync',
1481
+ installSources: [],
1482
+ resourceSources,
1483
+ });
1484
+ }
1485
+ const bootloaderSources = installSources.filter(source => source.kind === 'bootloader');
1486
+ if (bootloaderSources.length > 0) {
1487
+ phases.push(
1488
+ {
1489
+ kind: 'bootloader-install',
1490
+ installSources: bootloaderSources,
1491
+ resourceSources: [],
1492
+ },
1493
+ {
1494
+ kind: 'bootloader-verify',
1495
+ installSources: [],
1496
+ resourceSources: [],
1497
+ }
1498
+ );
1499
+ }
1500
+ const componentSources = installSources.filter(source => source.kind !== 'bootloader');
1501
+ if (componentSources.length > 0) {
1502
+ phases.push({
1503
+ kind: 'component-install',
1504
+ installSources: componentSources,
1505
+ resourceSources: [],
1506
+ });
1507
+ }
1508
+ phases.push({
1509
+ kind: 'final-verify',
1510
+ installSources: [],
1511
+ resourceSources: [],
1512
+ });
1513
+ return phases;
1514
+ }
1515
+
1516
+ private async executeProtocolV2Phases({
1517
+ installSources,
1518
+ resourceSources,
1519
+ }: {
1520
+ installSources: ProtocolV2InstallSource[];
1521
+ resourceSources: ProtocolV2ResourceBundleSource[];
1522
+ }) {
1523
+ const phases = this.buildProtocolV2ExecutionPhases({
1524
+ installSources,
1525
+ resourceSources,
1526
+ });
1527
+ for (const phase of phases) {
1528
+ if (phase.kind === 'final-verify') {
1529
+ return this.completeProtocolV2FinalVerification();
1530
+ }
1531
+ if (phase.kind === 'resource-sync') {
1532
+ await this.enterProtocolV2BootloaderMode();
1533
+ await this.executeProtocolV2TransferPhase(phase);
1534
+ } else if (phase.kind === 'bootloader-install' || phase.kind === 'component-install') {
1535
+ await this.enterProtocolV2BootloaderMode();
1536
+ await this.executeProtocolV2TransferPhase(phase);
1537
+ await this.exitProtocolV2BootloaderToNormal();
1538
+ } else if (phase.kind === 'bootloader-verify') {
1539
+ await this.waitForProtocolV2FinalFeatures();
1540
+ this.assertExpectedProtocolV2Versions(['boot']);
1541
+ }
1542
+ }
1543
+ throw ERRORS.TypedError(
1544
+ HardwareErrorCode.RuntimeError,
1545
+ 'Protocol V2 execution has no final verification phase'
1546
+ );
1547
+ }
1548
+
1549
+ private async executeProtocolV2SourceUpdate({
1550
+ installSources,
1551
+ resourceSources,
1552
+ }: {
1553
+ installSources: ProtocolV2InstallSource[];
1554
+ resourceSources: ProtocolV2ResourceBundleSource[];
1555
+ }) {
1556
+ return this.executeProtocolV2Phases({
1557
+ installSources,
1558
+ resourceSources,
1559
+ });
1560
+ }
1561
+
1077
1562
  private async executeProtocolV2Update({
1078
1563
  fwBinaryMap,
1079
1564
  bootloaderBinary,
@@ -1085,104 +1570,158 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1085
1570
  installItems?: ProtocolV2InstallItem[];
1086
1571
  resourceBundles?: ProtocolV2ResourceBundleBinary[];
1087
1572
  }) {
1088
- const orderedInstallItems =
1573
+ const memoryInstallItems =
1089
1574
  installItems ??
1090
1575
  this.buildProtocolV2InstallItems({
1091
- bootloaderBinary: bootloaderBinary ?? null,
1092
1576
  fwBinaryMap: fwBinaryMap ?? [],
1577
+ bootloaderBinary: bootloaderBinary ?? null,
1093
1578
  });
1094
-
1095
- let firmwareSize = 0;
1096
- for (const item of orderedInstallItems) firmwareSize += item.binary.byteLength;
1097
-
1098
- this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
1099
-
1100
- let processedSize = 0;
1101
- let totalSize = firmwareSize;
1102
- if (resourceBundles?.length) {
1103
- const resourceTransfer = await this.syncProtocolV2ResourceBundles(
1104
- resourceBundles,
1105
- firmwareSize
1579
+ try {
1580
+ const installSources = await Promise.all(
1581
+ memoryInstallItems.map(async item => ({
1582
+ fileName: item.fileName,
1583
+ source: await this.openProtocolV2MemorySource(item.binary),
1584
+ targetId: item.targetId,
1585
+ kind: item.kind,
1586
+ }))
1106
1587
  );
1107
- processedSize = resourceTransfer.processedSize;
1108
- totalSize = resourceTransfer.totalSize;
1588
+ const resourceSources = await Promise.all(
1589
+ (resourceBundles ?? []).map(async bundle => ({
1590
+ name: bundle.name,
1591
+ source: await this.openProtocolV2MemorySource(bundle.binary),
1592
+ devicePath: bundle.devicePath,
1593
+ version: bundle.version,
1594
+ payloadHash: bundle.payloadHash,
1595
+ headerHash: bundle.headerHash,
1596
+ }))
1597
+ );
1598
+ return await this.executeProtocolV2Phases({
1599
+ installSources,
1600
+ resourceSources,
1601
+ });
1602
+ } finally {
1603
+ await this.closeProtocolV2PreparedSources();
1109
1604
  }
1605
+ }
1110
1606
 
1111
- // Skip staging and installation when the update contains RESC bundles only.
1112
- if (orderedInstallItems.length === 0) {
1113
- Log.log('[FirmwareUpdateV4] no firmware targets to install (RESC bundles only)');
1114
- if (totalSize > 0) {
1115
- this.postProgressMessage(100, 'transferData');
1607
+ private async executeProtocolV2TransferPhase({
1608
+ installSources,
1609
+ resourceSources,
1610
+ }: ProtocolV2ExecutionPhase) {
1611
+ let totalSize = installSources.reduce((total, item) => total + item.source.size, 0);
1612
+ const resourcesToSync: ProtocolV2ResourceBundleSource[] = [];
1613
+ for (const resource of resourceSources) {
1614
+ if (await this.isProtocolV2ResourceBundleUpToDate(resource)) {
1615
+ Log.log(`[FirmwareUpdateV4] skip RESC bundle ${resource.name}; already up to date`);
1616
+ } else {
1617
+ resourcesToSync.push(resource);
1618
+ totalSize += resource.source.size;
1116
1619
  }
1117
- return;
1118
1620
  }
1119
- let transferredSize = processedSize;
1120
- const transferStartTime = Date.now();
1121
- const transferTransport = this.getProtocolV2FirmwareTransferTransport();
1122
- const chunkSize = this.getProtocolV2FirmwareChunkSize();
1123
- const onTransferredBytes = (bytes: number) => {
1124
- transferredSize = bytes;
1125
- };
1126
- Log.log(
1127
- `[FirmwareUpdateV4] transfer started transport=${transferTransport} total=${totalSize} bytes chunk=${chunkSize} bytes`
1128
- );
1129
1621
 
1130
- const stagedInstallTargets: ProtocolV2InstallTarget[] = [];
1131
-
1132
- try {
1133
- for (const item of orderedInstallItems) {
1134
- const filePath = this.getProtocolV2InstallItemStagingPath(item);
1135
- Log.log(
1136
- `[FirmwareUpdateV4] staging ${item.kind} via FilesystemFileWrite target=${item.targetId} path=${filePath} source=${item.fileName} bytes=${item.binary.byteLength}`
1137
- );
1138
- processedSize = await this.protocolV2CommonUpdateProcess({
1139
- payload: item.binary,
1140
- filePath,
1141
- processedSize,
1142
- totalSize,
1143
- onTransferredBytes,
1144
- });
1145
- transferredSize = processedSize;
1146
- await this.verifyProtocolV2StagedFile(filePath, item.binary.byteLength);
1147
-
1148
- stagedInstallTargets.push({
1149
- ...item,
1150
- path: filePath,
1151
- });
1152
- }
1622
+ this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
1623
+ let processedSize = 0;
1624
+ for (const resource of resourcesToSync) {
1625
+ processedSize = await this.protocolV2SourceUpdateProcess({
1626
+ source: resource.source,
1627
+ filePath: resource.devicePath,
1628
+ processedSize,
1629
+ totalSize,
1630
+ });
1631
+ await this.verifyProtocolV2StagedFile(resource.devicePath, resource.source.size);
1632
+ }
1153
1633
 
1154
- if (totalSize > 0) {
1155
- this.postProgressMessage(100, 'transferData');
1156
- }
1634
+ const stagedInstallTargets: Array<{ targetId: number; path: string }> = [];
1635
+ for (const item of installSources) {
1636
+ const filePath = this.getProtocolV2InstallItemStagingPath(item);
1637
+ processedSize = await this.protocolV2SourceUpdateProcess({
1638
+ source: item.source,
1639
+ filePath,
1640
+ processedSize,
1641
+ totalSize,
1642
+ });
1643
+ await this.verifyProtocolV2StagedFile(filePath, item.source.size);
1644
+ stagedInstallTargets.push({
1645
+ targetId: item.targetId,
1646
+ path: filePath,
1647
+ });
1648
+ }
1157
1649
 
1158
- const elapsedMs = Date.now() - transferStartTime;
1159
- Log.log(
1160
- `[FirmwareUpdateV4] transfer finished transport=${transferTransport} bytes=${totalSize} elapsed=${(
1161
- elapsedMs / 1000
1162
- ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(totalSize, elapsedMs)} KB/s`
1163
- );
1164
- } catch (error) {
1165
- const elapsedMs = Date.now() - transferStartTime;
1166
- Log.warn(
1167
- `[FirmwareUpdateV4] transfer failed transport=${transferTransport} bytes=${transferredSize}/${totalSize} elapsed=${(
1168
- elapsedMs / 1000
1169
- ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(transferredSize, elapsedMs)} KB/s`
1170
- );
1171
- throw error;
1650
+ if (totalSize > 0) {
1651
+ this.postProgressMessage(100, 'transferData');
1652
+ }
1653
+ if (stagedInstallTargets.length === 0) {
1654
+ return;
1172
1655
  }
1173
1656
 
1174
1657
  this.postTipMessage(FirmwareUpdateTipMessage.ConfirmOnDevice);
1175
-
1176
- const allTargets = stagedInstallTargets.map(item => ({
1658
+ const targets = stagedInstallTargets.map(item => ({
1177
1659
  target_id: item.targetId,
1178
1660
  path: item.path,
1179
1661
  }));
1180
- Log.log(`[FirmwareUpdateV4] DeviceFirmwareUpdateRequest targets=${JSON.stringify(allTargets)}`);
1181
- await this.protocolV2StartFirmwareUpdate({ targets: allTargets });
1182
- await this.waitForProtocolV2FirmwareUpdateComplete(allTargets);
1662
+ await this.protocolV2StartFirmwareUpdate({ targets });
1663
+ await this.waitForProtocolV2FirmwareUpdateComplete(targets);
1664
+ }
1665
+
1666
+ private async protocolV2SourceUpdateProcess({
1667
+ source,
1668
+ filePath,
1669
+ processedSize,
1670
+ totalSize,
1671
+ }: {
1672
+ source: FirmwareByteSource;
1673
+ filePath: string;
1674
+ processedSize: number;
1675
+ totalSize: number;
1676
+ }) {
1677
+ let lastError: unknown;
1678
+ for (let attempt = 1; attempt <= PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT; attempt += 1) {
1679
+ try {
1680
+ await writeFirmwareByteSource({
1681
+ source,
1682
+ chunkSize: this.getProtocolV2FirmwareChunkSize(),
1683
+ write: async ({ data, sourceOffset, length, first }) => {
1684
+ const chunkEnd = sourceOffset + length;
1685
+ const progress = getProtocolV2DeviceTransferProgress(
1686
+ processedSize + sourceOffset,
1687
+ processedSize + chunkEnd,
1688
+ totalSize
1689
+ );
1690
+ const response = await this.fileWriteChunk(
1691
+ filePath,
1692
+ source.size,
1693
+ sourceOffset,
1694
+ data,
1695
+ first,
1696
+ progress
1697
+ );
1698
+ const rawProcessedByte = response.message.processed_byte;
1699
+ const nextOffset = rawProcessedByte === undefined ? chunkEnd : Number(rawProcessedByte);
1700
+ if (!Number.isFinite(nextOffset) || nextOffset !== chunkEnd) {
1701
+ throw ERRORS.TypedError(
1702
+ HardwareErrorCode.EmmcFileWriteFirmwareError,
1703
+ `invalid processed_byte ${rawProcessedByte} for offset ${sourceOffset}`
1704
+ );
1705
+ }
1706
+ this.postProgressMessage(progress, 'transferData');
1707
+ return length;
1708
+ },
1709
+ });
1710
+ return processedSize + source.size;
1711
+ } catch (error) {
1712
+ lastError = error;
1713
+ if (attempt < PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT) {
1714
+ await this.recoverProtocolV2FileTransfer();
1715
+ }
1716
+ }
1717
+ }
1718
+ throw ERRORS.TypedError(
1719
+ HardwareErrorCode.EmmcFileWriteFirmwareError,
1720
+ `transfer data error: ${getProtocolV2UnknownErrorText(lastError)}`
1721
+ );
1183
1722
  }
1184
1723
 
1185
- private getProtocolV2InstallItemStagingPath(item: ProtocolV2InstallItem) {
1724
+ private getProtocolV2InstallItemStagingPath(item: Pick<ProtocolV2InstallItem, 'fileName'>) {
1186
1725
  return `${PROTOCOL_V2_FIRMWARE_STAGING_VOLUME}${item.fileName}`;
1187
1726
  }
1188
1727
 
@@ -1201,7 +1740,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1201
1740
 
1202
1741
  private assertProtocolV2TargetStatus(
1203
1742
  statusTargets: ProtocolV2FirmwareUpdateStatusTarget[],
1204
- expectedTargetIds: Set<number>
1743
+ expectedTargetIds: Set<number>,
1744
+ expectedPaths = new Map<number, string>()
1205
1745
  ) {
1206
1746
  Log.log(
1207
1747
  `[FirmwareUpdateV4] DeviceFirmwareUpdateStatus records=${JSON.stringify(statusTargets)}`
@@ -1232,17 +1772,54 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1232
1772
  const matchingTargets = statusTargets.filter(target =>
1233
1773
  expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1)
1234
1774
  );
1775
+ const seenTargetIds = new Set<number>();
1776
+ for (const target of matchingTargets) {
1777
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
1778
+ if (
1779
+ targetId === undefined ||
1780
+ seenTargetIds.has(targetId) ||
1781
+ (target.path && expectedPaths.get(targetId) && target.path !== expectedPaths.get(targetId))
1782
+ ) {
1783
+ throw ERRORS.TypedError(
1784
+ HardwareErrorCode.RuntimeError,
1785
+ `Protocol V2 install status conflicts with target ${target.target_id}`
1786
+ );
1787
+ }
1788
+ seenTargetIds.add(targetId);
1789
+ }
1790
+ const completedTargets = matchingTargets.filter(target =>
1791
+ isProtocolV2TargetStatusFinished(target.status)
1792
+ );
1235
1793
  const completedTargetIds = new Set<number>();
1236
- matchingTargets.forEach(target => {
1794
+ completedTargets.forEach(target => {
1237
1795
  const targetId = normalizeProtocolV2TargetId(target.target_id);
1238
- if (targetId !== undefined && isProtocolV2TargetStatusFinished(target.status)) {
1796
+ if (targetId !== undefined) {
1239
1797
  completedTargetIds.add(targetId);
1240
1798
  }
1241
1799
  });
1242
- const allExpectedTargetsCompleted =
1243
- expectedTargetIds.size > 0 &&
1244
- Array.from(expectedTargetIds).every(targetId => completedTargetIds.has(targetId));
1245
- if (allExpectedTargetsCompleted) {
1800
+ if (completedTargetIds.size === expectedTargetIds.size && expectedTargetIds.size > 0) {
1801
+ for (const target of completedTargets) {
1802
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
1803
+ const payloadVersion = toProtocolV2FiniteNumber(target.payload_version);
1804
+ if (targetId === undefined) {
1805
+ throw ERRORS.TypedError(
1806
+ HardwareErrorCode.RuntimeError,
1807
+ 'Protocol V2 completed target identity is invalid'
1808
+ );
1809
+ }
1810
+ const expectedVersion = this.getExpectedProtocolV2TargetVersion(targetId);
1811
+ if (expectedVersion !== undefined && payloadVersion !== undefined) {
1812
+ if (payloadVersion !== expectedVersion) {
1813
+ throw ERRORS.TypedError(
1814
+ HardwareErrorCode.FirmwareVerificationFailed,
1815
+ `Protocol V2 target ${targetId} reached payload version ${payloadVersion}, expected ${expectedVersion}`
1816
+ );
1817
+ }
1818
+ }
1819
+ if (payloadVersion !== undefined) {
1820
+ this.protocolV2CompletedTargetVersions.set(targetId, payloadVersion);
1821
+ }
1822
+ }
1246
1823
  this.postProgressMessage(100, 'installingFirmware');
1247
1824
  return true;
1248
1825
  }
@@ -1263,43 +1840,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1263
1840
  return false;
1264
1841
  }
1265
1842
 
1266
- private getProtocolV2MissingTargetIds(
1267
- statusTargets: ProtocolV2FirmwareUpdateStatusTarget[],
1268
- expectedTargetIds: Set<number>
1269
- ) {
1270
- const reportedTargetIds = new Set<number>();
1271
- statusTargets.forEach(target => {
1272
- const targetId = normalizeProtocolV2TargetId(target.target_id);
1273
- if (targetId !== undefined) {
1274
- reportedTargetIds.add(targetId);
1275
- }
1276
- });
1277
- return Array.from(expectedTargetIds).filter(targetId => !reportedTargetIds.has(targetId));
1278
- }
1279
-
1280
1843
  private async waitForProtocolV2FirmwareUpdateComplete(
1281
1844
  targets: Array<{ target_id: number; path: string }>
1282
1845
  ) {
1846
+ this.protocolV2FinalStatusVerified = false;
1283
1847
  const expectedTargetIds = new Set(targets.map(target => target.target_id));
1848
+ const expectedPaths = new Map(targets.map(target => [target.target_id, target.path]));
1284
1849
  const startTime = Date.now();
1285
1850
  let lastError: unknown;
1286
- let shouldReconnect = true;
1287
- let deviceInfo: ProtocolV2DeviceInfo | undefined;
1288
- let missingTargetStatusSince: number | undefined;
1289
- let missingTargetStatusKey: string | undefined;
1290
- const resetMissingTargetStatusGrace = () => {
1291
- missingTargetStatusSince = undefined;
1292
- missingTargetStatusKey = undefined;
1293
- };
1294
1851
 
1295
1852
  while (Date.now() - startTime < PROTOCOL_V2_INSTALL_TIMEOUT) {
1296
1853
  try {
1297
- if (shouldReconnect) {
1298
- await this.reconnectProtocolV2Device();
1299
- deviceInfo = await this.verifyProtocolV2ReconnectIdentity();
1300
- shouldReconnect = false;
1301
- }
1302
- const currentDeviceInfo = deviceInfo;
1854
+ await this.reconnectProtocolV2Device();
1855
+ const deviceInfo = await this.verifyProtocolV2ReconnectIdentity();
1303
1856
  try {
1304
1857
  const statusResponse = await this.device.getCommands().typedCall(
1305
1858
  'DeviceFirmwareUpdateStatusGet',
@@ -1311,57 +1864,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1311
1864
  path: true,
1312
1865
  },
1313
1866
  },
1314
- { timeoutMs: PROTOCOL_V2_FIRMWARE_STATUS_RESPONSE_TIMEOUT }
1867
+ { timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT }
1315
1868
  );
1316
- const statusTargets = (statusResponse.message.records ??
1317
- []) as ProtocolV2FirmwareUpdateStatusTarget[];
1318
- if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds)) {
1319
- return;
1320
- }
1321
-
1322
1869
  if (
1323
- statusTargets.length === 0 &&
1324
- currentDeviceInfo &&
1325
- (await this.probeProtocolV2NormalMode(currentDeviceInfo))
1870
+ this.assertProtocolV2TargetStatus(
1871
+ (statusResponse.message.records ?? []) as ProtocolV2FirmwareUpdateStatusTarget[],
1872
+ expectedTargetIds,
1873
+ expectedPaths
1874
+ )
1326
1875
  ) {
1327
- Log.log(
1328
- '[FirmwareUpdateV4] empty firmware status after confirmed App reboot; update complete'
1329
- );
1330
- this.postProgressMessage(100, 'installingFirmware');
1876
+ this.protocolV2FinalStatusVerified = true;
1331
1877
  return;
1332
1878
  }
1333
-
1334
- const missingTargetIds = this.getProtocolV2MissingTargetIds(
1335
- statusTargets,
1336
- expectedTargetIds
1337
- );
1338
- if (missingTargetIds.length > 0) {
1339
- const now = Date.now();
1340
- const missingKey = missingTargetIds.join(',');
1341
- if (missingTargetStatusSince === undefined || missingTargetStatusKey !== missingKey) {
1342
- missingTargetStatusSince = now;
1343
- missingTargetStatusKey = missingKey;
1344
- } else if (
1345
- now - missingTargetStatusSince >=
1346
- PROTOCOL_V2_MISSING_TARGET_STATUS_GRACE_TIMEOUT
1347
- ) {
1348
- const reportedTargetIds = statusTargets
1349
- .map(target => normalizeProtocolV2TargetId(target.target_id))
1350
- .filter((targetId): targetId is number => targetId !== undefined);
1351
- throw ERRORS.TypedError(
1352
- HardwareErrorCode.FirmwareError,
1353
- `Protocol V2 firmware status is missing requested records: targetIds=${missingKey} reportedTargetIds=${reportedTargetIds.join(
1354
- ','
1355
- )}`
1356
- );
1357
- }
1358
- lastError = new Error(
1359
- `Protocol V2 firmware status is temporarily missing targetIds=${missingKey}`
1360
- );
1361
- } else {
1362
- resetMissingTargetStatusGrace();
1363
- lastError = new Error('Protocol V2 firmware targets are still installing');
1364
- }
1879
+ lastError = new Error('Protocol V2 firmware targets are still installing');
1365
1880
  } catch (error) {
1366
1881
  if (
1367
1882
  error instanceof HardwareError &&
@@ -1369,31 +1884,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1369
1884
  ) {
1370
1885
  throw error;
1371
1886
  }
1372
- // Only consecutive full status dumps can prove that a record is absent.
1373
- // A reboot or transport interruption starts a fresh grace window.
1374
- resetMissingTargetStatusGrace();
1375
1887
  // App firmware does not register DeviceFirmwareUpdateStatusGet. Treat the
1376
1888
  // missing endpoint as completion only after the runtime probe confirms App mode.
1377
1889
  if (isProtocolV2FirmwareStatusEndpointUnavailable(error)) {
1378
- if (!currentDeviceInfo) {
1379
- throw ERRORS.TypedError(
1380
- HardwareErrorCode.RuntimeError,
1381
- 'Protocol V2 device identity is unavailable during install polling'
1382
- );
1383
- }
1384
- if (await this.probeProtocolV2NormalMode(currentDeviceInfo)) {
1890
+ if (await this.probeProtocolV2NormalMode(deviceInfo)) {
1385
1891
  Log.log(
1386
1892
  '[FirmwareUpdateV4] firmware status endpoint unavailable after confirmed App reboot'
1387
1893
  );
1388
- this.postProgressMessage(100, 'installingFirmware');
1389
1894
  return;
1390
1895
  }
1391
1896
  lastError = new Error(
1392
1897
  'Protocol V2 firmware status endpoint is unavailable while the device remains in loader mode'
1393
1898
  );
1394
1899
  } else {
1395
- shouldReconnect = true;
1396
- deviceInfo = undefined;
1397
1900
  lastError = error;
1398
1901
  Log.log(
1399
1902
  '[FirmwareUpdateV4] DeviceFirmwareUpdateStatusGet unavailable during install: ',
@@ -1409,9 +1912,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1409
1912
  if (error instanceof HardwareError && error.errorCode === HardwareErrorCode.FirmwareError) {
1410
1913
  throw error;
1411
1914
  }
1412
- shouldReconnect = true;
1413
- deviceInfo = undefined;
1414
- resetMissingTargetStatusGrace();
1915
+ if (
1916
+ error instanceof HardwareError &&
1917
+ error.params?.firmwareUpdateCode === 'FirmwareInstallStatusUnavailable'
1918
+ ) {
1919
+ throw error;
1920
+ }
1415
1921
  Log.log('Protocol V2 firmware install device readiness probe failed: ', error);
1416
1922
  }
1417
1923
  await wait(1000);
@@ -1419,24 +1925,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1419
1925
 
1420
1926
  throw ERRORS.TypedError(
1421
1927
  HardwareErrorCode.RuntimeError,
1422
- `Protocol V2 firmware update status not complete within ${
1423
- PROTOCOL_V2_INSTALL_TIMEOUT / 1000
1424
- }s: ${this.normalizeErrorMessage(lastError)}`
1928
+ `Protocol V2 firmware update status timeout: ${this.normalizeErrorMessage(lastError)}`
1425
1929
  );
1426
1930
  }
1427
1931
 
1428
1932
  private async exitProtocolV2BootloaderToNormal() {
1429
1933
  await this.reconnectProtocolV2Device();
1430
- const deviceInfo = await this.verifyProtocolV2ReconnectIdentity();
1431
- try {
1432
- if (await this.probeProtocolV2NormalMode(deviceInfo)) {
1433
- Log.log('[FirmwareUpdateV4] device already returned to App mode; skip Normal reboot');
1434
- return;
1435
- }
1436
- } catch (error) {
1437
- Log.log('[FirmwareUpdateV4] unable to confirm App mode before Normal reboot: ', error);
1438
- }
1934
+ await this.verifyProtocolV2ReconnectIdentity();
1935
+ // The connection may still be in bootloader. Request a Normal reboot directly;
1936
+ // repeating it after an automatic App reboot is idempotent.
1439
1937
  await this.protocolV2Reboot(DeviceRebootType.Normal);
1938
+ this.protocolV2ExecutionInLoader = false;
1440
1939
  }
1441
1940
 
1442
1941
  private async probeProtocolV2NormalMode(deviceInfo: ProtocolV2DeviceInfo) {
@@ -1452,13 +1951,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1452
1951
  PROTOCOL_V2_FINAL_RECONNECT_TIMEOUT
1453
1952
  );
1454
1953
 
1455
- return this.getProtocolV2VersionResult(features);
1456
- }
1457
-
1458
- private getProtocolV2VersionResult(features: Features) {
1459
1954
  const bootloaderVersion = getDeviceBootloaderVersion(features).join('.');
1460
1955
  const bleVersion = getDeviceBLEFirmwareVersion(features).join('.');
1461
1956
  const firmwareVersion = getDeviceFirmwareVersion(features).join('.');
1957
+ this.protocolV2LatestFinalFeatures = features;
1462
1958
  if (firmwareVersion === '0.0.0') {
1463
1959
  Log.warn(
1464
1960
  'Protocol V2 firmware update finished but app firmware version is still 0.0.0. This is allowed for Pro2 debug BLE-only update flows.'
@@ -1472,20 +1968,27 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1472
1968
  };
1473
1969
  }
1474
1970
 
1971
+ private async completeProtocolV2FinalVerification() {
1972
+ if (this.protocolV2ExecutionInLoader || this.isProtocolV2BootloaderMode()) {
1973
+ await this.exitProtocolV2BootloaderToNormal();
1974
+ }
1975
+ const versions = await this.waitForProtocolV2FinalFeatures();
1976
+ this.assertExpectedProtocolV2Versions();
1977
+ this.postTipMessage(FirmwareUpdateTipMessage.FirmwareUpdateCompleted);
1978
+ DevicePool.resetState();
1979
+ return versions;
1980
+ }
1981
+
1475
1982
  private async waitForProtocolV2ReconnectAndFeatures(timeout: number) {
1476
1983
  const startTime = Date.now();
1477
1984
  let lastError: unknown;
1478
- let shouldReconnect = true;
1479
1985
 
1480
1986
  while (Date.now() - startTime < timeout) {
1481
1987
  try {
1482
- if (shouldReconnect) {
1483
- await this.reconnectProtocolV2Device();
1484
- shouldReconnect = false;
1485
- }
1988
+ await this.reconnectProtocolV2Device();
1486
1989
  const deviceInfo = await requestProtocolV2DeviceInfo({
1487
1990
  commands: this.device.getCommands(),
1488
- timeoutMs: this.getProtocolV2DeviceInfoTimeout(),
1991
+ timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
1489
1992
  // Completion needs target versions only; keep scope aligned with the request.
1490
1993
  request: PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST,
1491
1994
  });
@@ -1494,22 +1997,21 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1494
1997
  deviceInfo,
1495
1998
  PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT
1496
1999
  );
1497
- if (features.mode === 'normal' && !features.bootloaderMode) {
1498
- return features;
2000
+ if (features.mode !== 'normal' || features.bootloaderMode) {
2001
+ throw ERRORS.TypedError(
2002
+ HardwareErrorCode.DeviceNotFound,
2003
+ 'Protocol V2 device is still in bootloader mode'
2004
+ );
1499
2005
  }
1500
- lastError = ERRORS.TypedError(
1501
- HardwareErrorCode.DeviceNotFound,
1502
- 'Protocol V2 device is still in bootloader mode'
1503
- );
2006
+ return features;
1504
2007
  } catch (error) {
1505
2008
  if (this.isProtocolV2ReconnectIdentityError(error)) {
1506
2009
  throw error;
1507
2010
  }
1508
- shouldReconnect = true;
1509
2011
  lastError = error;
1510
2012
  Log.log('Protocol V2 normal mode not ready, polling Ping: ', error);
2013
+ await wait(1000);
1511
2014
  }
1512
- await wait(1000);
1513
2015
  }
1514
2016
 
1515
2017
  throw ERRORS.TypedError(
@@ -1546,33 +2048,20 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1546
2048
  return;
1547
2049
  }
1548
2050
 
1549
- // Reinitialize every USB descriptor so DeviceInfo can provide the physical serial.
1550
- // Enumeration order is not stable and must never decide the reconnect target.
2051
+ // App and bootloader serials may differ temporarily. During V4 reconnect, accept
2052
+ // only a uniquely enumerated device instead of repeatedly using the old App path.
1551
2053
  const { deviceList } = await DevicePool.getDevices(devicesDescriptor, undefined, {
1552
2054
  connectProtocol: PROTOCOL_V2_CONNECT_PROTOCOL,
1553
2055
  });
1554
- const expectedSerialNumber = this.protocolV2ExpectedSerialNumber?.trim();
1555
- const identityMatch = expectedSerialNumber
1556
- ? deviceList.find(
1557
- candidate => candidate.getCurrentSerialNo?.().trim() === expectedSerialNumber
1558
- )
1559
- : undefined;
1560
- const singleCandidate = deviceList.length === 1 ? deviceList.at(0) : undefined;
1561
- const singleCandidateSerialNumber = singleCandidate?.getCurrentSerialNo?.().trim();
1562
- const reconnectDevice =
1563
- identityMatch ??
1564
- (singleCandidate && (!expectedSerialNumber || !singleCandidateSerialNumber)
1565
- ? singleCandidate
1566
- : undefined);
1567
- if (!reconnectDevice) {
2056
+ if (deviceList.length !== 1) {
1568
2057
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
1569
2058
  }
1570
2059
 
1571
2060
  Log.debug(
1572
- 'Protocol V2 firmware reconnect using matched device:',
1573
- reconnectDevice.getConnectId()
2061
+ 'Protocol V2 firmware reconnect using single enumerated device:',
2062
+ deviceList[0].getConnectId()
1574
2063
  );
1575
- this.device.updateFromCache(reconnectDevice);
2064
+ this.device.updateFromCache(deviceList[0]);
1576
2065
  await this.ensureProtocolV2DeviceAcquired();
1577
2066
  this.device.commands.disposed = false;
1578
2067
  this.device.getCommands().mainId = this.device.mainId ?? '';
@@ -1596,96 +2085,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1596
2085
  return this.normalizeErrorMessage(error).includes('Protocol V2 reconnect physical identity');
1597
2086
  }
1598
2087
 
1599
- private async protocolV2CommonUpdateProcess(params: ProtocolV2FileTransferParams) {
1600
- let lastError: unknown;
1601
- for (let attempt = 1; attempt <= PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT; attempt += 1) {
1602
- try {
1603
- return await this.protocolV2WriteWholeFile(params);
1604
- } catch (error) {
1605
- lastError = error;
1606
- Log.error(
1607
- `Protocol V2 file transfer failed path=${params.filePath} attempt=${attempt}/${PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT}; restarting from offset 0`,
1608
- error
1609
- );
1610
- if (attempt === PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT) {
1611
- break;
1612
- }
1613
- await this.recoverProtocolV2FileTransfer();
1614
- }
1615
- }
1616
-
1617
- throw ERRORS.TypedError(
1618
- HardwareErrorCode.EmmcFileWriteFirmwareError,
1619
- `transfer data error: ${getProtocolV2UnknownErrorText(lastError)}`
1620
- );
1621
- }
1622
-
1623
- private async protocolV2WriteWholeFile({
1624
- payload,
1625
- filePath,
1626
- processedSize,
1627
- totalSize,
1628
- onTransferredBytes,
1629
- }: ProtocolV2FileTransferParams) {
1630
- const chunkSize = this.getProtocolV2FirmwareChunkSize();
1631
- let offset = 0;
1632
- const getUploadProgress = (fileOffset: number) => {
1633
- if (totalSize !== undefined && processedSize !== undefined) {
1634
- return Math.min(Math.ceil(((processedSize + fileOffset) / totalSize) * 100), 99);
1635
- }
1636
- return Math.min(Math.ceil((fileOffset / payload.byteLength) * 100), 99);
1637
- };
1638
-
1639
- while (offset < payload.byteLength) {
1640
- const chunkEnd = Math.min(offset + chunkSize, payload.byteLength);
1641
- const chunkLength = chunkEnd - offset;
1642
- const chunk = payload.slice(offset, chunkEnd);
1643
- const overwrite = offset === 0;
1644
- const progress = getProtocolV2DeviceTransferProgress(
1645
- (processedSize ?? 0) + offset,
1646
- (processedSize ?? 0) + chunkEnd,
1647
- totalSize ?? payload.byteLength
1648
- );
1649
-
1650
- const writeRes = await this.fileWriteChunk(
1651
- filePath,
1652
- payload.byteLength,
1653
- offset,
1654
- chunk,
1655
- overwrite,
1656
- progress
1657
- );
1658
- const rawProcessedByte = writeRes.message.processed_byte;
1659
- const processedByte = Number(rawProcessedByte);
1660
- const nextOffset = rawProcessedByte === undefined ? offset + chunkLength : processedByte;
1661
- if (!Number.isFinite(nextOffset) || nextOffset <= offset || nextOffset > chunkEnd) {
1662
- throw ERRORS.TypedError(
1663
- HardwareErrorCode.EmmcFileWriteFirmwareError,
1664
- `invalid processed_byte ${writeRes.message.processed_byte} for offset ${offset}`
1665
- );
1666
- }
1667
- offset = nextOffset;
1668
- onTransferredBytes?.((processedSize ?? 0) + offset);
1669
- this.postProgressMessage(getUploadProgress(offset), 'transferData');
1670
- }
1671
-
1672
- return totalSize !== undefined ? (processedSize ?? 0) + payload.byteLength : 0;
1673
- }
1674
-
1675
- private getProtocolV2FirmwareTransferTransport() {
1676
- const env = DataManager.getSettings('env');
1677
- if (env && DataManager.isBleConnect(env)) {
1678
- return 'BLE';
1679
- }
1680
- if (
1681
- env &&
1682
- (DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env) || env === 'web')
1683
- ) {
1684
- return 'WebUSB';
1685
- }
1686
- return env ?? 'unknown';
1687
- }
1688
-
1689
2088
  private async fileWriteChunk(
1690
2089
  filePath: string,
1691
2090
  totalFileSize: number,