@onekeyfe/hd-core 1.2.0-alpha.56 → 1.2.0-alpha.62

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 (262) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +0 -231
  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__/device-state-contract.test.ts +0 -1
  9. package/__tests__/device-state-mapper.test.ts +1 -1
  10. package/__tests__/deviceSettings.test.ts +1 -1
  11. package/__tests__/firmware-update/check-all-firmware-release.test.ts +175 -0
  12. package/__tests__/firmware-update/firmware-artifact-source.test.ts +119 -0
  13. package/__tests__/firmware-update/firmware-host-binding.test.ts +70 -0
  14. package/__tests__/firmware-update/firmware-preparation-error.test.ts +27 -0
  15. package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +200 -0
  16. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +13 -0
  17. package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +294 -0
  18. package/__tests__/firmware-update/firmware-update-plan.test.ts +593 -0
  19. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +213 -0
  20. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +59 -31
  21. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +58 -0
  22. package/__tests__/firmware-update/firmware-upload-source.test.ts +70 -0
  23. package/__tests__/get-device-state.test.ts +12 -132
  24. package/__tests__/homescreen.test.ts +1 -41
  25. package/__tests__/kaspaSignTransaction.test.ts +26 -3
  26. package/__tests__/logBlockEvent.test.ts +1 -1
  27. package/__tests__/method-protocol-support.test.ts +0 -19
  28. package/__tests__/open-wallet-session.test.ts +37 -568
  29. package/__tests__/protocol-v2-unlock-policy.test.ts +35 -201
  30. package/__tests__/protocol-v2.test.ts +1046 -1388
  31. package/__tests__/protocolV2FileWrite.test.ts +0 -39
  32. package/__tests__/protocolV2UiInteraction.test.ts +0 -63
  33. package/__tests__/public-pro2-api-boundary.test.ts +0 -1
  34. package/__tests__/search-devices.test.ts +3 -12
  35. package/dist/api/BaseMethod.d.ts +1 -3
  36. package/dist/api/BaseMethod.d.ts.map +1 -1
  37. package/dist/api/CheckAllFirmwareRelease.d.ts +1 -2
  38. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  39. package/dist/api/FirmwareUpdate.d.ts.map +1 -1
  40. package/dist/api/FirmwareUpdateV2.d.ts +11 -0
  41. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  42. package/dist/api/FirmwareUpdateV3.d.ts +7 -3
  43. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  44. package/dist/api/FirmwareUpdateV4.d.ts +25 -11
  45. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  46. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  47. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  48. package/dist/api/SearchDevices.d.ts.map +1 -1
  49. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  50. package/dist/api/device/DeviceUnlock.d.ts +2 -2
  51. package/dist/api/device/DeviceUnlock.d.ts.map +1 -1
  52. package/dist/api/device/DeviceUpdateBootloader.d.ts +5 -1
  53. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  54. package/dist/api/firmware/FirmwareArtifactSource.d.ts +25 -0
  55. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +1 -0
  56. package/dist/api/firmware/FirmwareHostBinding.d.ts +6 -0
  57. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +1 -0
  58. package/dist/api/firmware/FirmwarePreparationError.d.ts +3 -0
  59. package/dist/api/firmware/FirmwarePreparationError.d.ts.map +1 -0
  60. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +9 -0
  61. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  62. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +3 -0
  63. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +1 -0
  64. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +25 -0
  65. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +1 -0
  66. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +32 -0
  67. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +1 -0
  68. package/dist/api/firmware/getBinary.d.ts +7 -0
  69. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  70. package/dist/api/firmware/progressThrottle.d.ts +3 -0
  71. package/dist/api/firmware/progressThrottle.d.ts.map +1 -0
  72. package/dist/api/firmware/updateBootloader.d.ts +2 -0
  73. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  74. package/dist/api/firmware/uploadFirmware.d.ts +9 -1
  75. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  76. package/dist/api/helpers/protocolV2FileWrite.d.ts +0 -2
  77. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  78. package/dist/api/index.d.ts +0 -2
  79. package/dist/api/index.d.ts.map +1 -1
  80. package/dist/api/kaspa/KaspaSignTransaction.d.ts.map +1 -1
  81. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -1
  82. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  83. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  84. package/dist/core/deviceEventRegistration.d.ts +0 -2
  85. package/dist/core/deviceEventRegistration.d.ts.map +1 -1
  86. package/dist/core/index.d.ts.map +1 -1
  87. package/dist/data-manager/DataManager.d.ts +2 -4
  88. package/dist/data-manager/DataManager.d.ts.map +1 -1
  89. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  90. package/dist/device/Device.d.ts +6 -29
  91. package/dist/device/Device.d.ts.map +1 -1
  92. package/dist/device/DeviceCommands.d.ts +4 -4
  93. package/dist/device/DeviceCommands.d.ts.map +1 -1
  94. package/dist/device/DevicePool.d.ts.map +1 -1
  95. package/dist/events/device.d.ts +0 -4
  96. package/dist/events/device.d.ts.map +1 -1
  97. package/dist/events/logBlockEvent.d.ts.map +1 -1
  98. package/dist/events/ui-promise.d.ts +2 -4
  99. package/dist/events/ui-promise.d.ts.map +1 -1
  100. package/dist/events/ui-request.d.ts +2 -31
  101. package/dist/events/ui-request.d.ts.map +1 -1
  102. package/dist/events/ui-response.d.ts +2 -10
  103. package/dist/events/ui-response.d.ts.map +1 -1
  104. package/dist/index.d.ts +231 -214
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js +3530 -2634
  107. package/dist/inject.d.ts +1 -6
  108. package/dist/inject.d.ts.map +1 -1
  109. package/dist/lowLevelInject.d.ts.map +1 -1
  110. package/dist/protocols/protocol-v2/features.d.ts +4 -0
  111. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  112. package/dist/protocols/protocol-v2/firmware.d.ts.map +1 -1
  113. package/dist/protocols/protocol-v2/lockedError.d.ts +2 -0
  114. package/dist/protocols/protocol-v2/lockedError.d.ts.map +1 -0
  115. package/dist/protocols/protocol-v2/uiInteraction.d.ts +4 -7
  116. package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -1
  117. package/dist/protocols/protocol-v2/unlockPolicy.d.ts +3 -1
  118. package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -1
  119. package/dist/protocols/protocol-v2/unlockRetry.d.ts +10 -0
  120. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -0
  121. package/dist/protocols/protocol-v2/walletSession.d.ts +1 -6
  122. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  123. package/dist/topLevelInject.d.ts.map +1 -1
  124. package/dist/types/api/checkAllFirmwareRelease.d.ts +4 -2
  125. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  126. package/dist/types/api/deviceUnlock.d.ts +1 -5
  127. package/dist/types/api/deviceUnlock.d.ts.map +1 -1
  128. package/dist/types/api/deviceUpdateBootloader.d.ts +6 -0
  129. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  130. package/dist/types/api/export.d.ts +4 -2
  131. package/dist/types/api/export.d.ts.map +1 -1
  132. package/dist/types/api/firmwareUpdate.d.ts +70 -2
  133. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  134. package/dist/types/api/firmwareUpdateCapabilities.d.ts +9 -0
  135. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +1 -0
  136. package/dist/types/api/firmwareUpdatePlan.d.ts +28 -0
  137. package/dist/types/api/firmwareUpdatePlan.d.ts.map +1 -0
  138. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +42 -0
  139. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +1 -0
  140. package/dist/types/api/index.d.ts +10 -7
  141. package/dist/types/api/index.d.ts.map +1 -1
  142. package/dist/types/api/protocolV2.d.ts +0 -3
  143. package/dist/types/api/protocolV2.d.ts.map +1 -1
  144. package/dist/types/device.d.ts.map +1 -1
  145. package/dist/types/params.d.ts +0 -1
  146. package/dist/types/params.d.ts.map +1 -1
  147. package/dist/types/settings.d.ts +13 -22
  148. package/dist/types/settings.d.ts.map +1 -1
  149. package/dist/utils/deviceFeaturesCompat.d.ts +1 -0
  150. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  151. package/dist/utils/deviceFeaturesUtils.d.ts +0 -4
  152. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  153. package/dist/utils/homescreen.d.ts +0 -4
  154. package/dist/utils/homescreen.d.ts.map +1 -1
  155. package/dist/utils/index.d.ts +1 -1
  156. package/dist/utils/index.d.ts.map +1 -1
  157. package/dist/utils/patch.d.ts +1 -1
  158. package/dist/utils/patch.d.ts.map +1 -1
  159. package/dist/utils/pro2Wallpaper.d.ts +0 -10
  160. package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
  161. package/package.json +4 -4
  162. package/src/api/BaseMethod.ts +10 -8
  163. package/src/api/CheckAllFirmwareRelease.ts +57 -54
  164. package/src/api/FirmwareUpdate.ts +15 -7
  165. package/src/api/FirmwareUpdateV2.ts +316 -126
  166. package/src/api/FirmwareUpdateV3.ts +265 -63
  167. package/src/api/FirmwareUpdateV4.ts +975 -584
  168. package/src/api/GetPassphraseState.ts +1 -7
  169. package/src/api/OpenWalletSession.ts +17 -70
  170. package/src/api/SearchDevices.ts +1 -3
  171. package/src/api/allnetwork/AllNetworkGetAddress.ts +21 -21
  172. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +11 -34
  173. package/src/api/device/DeviceUnlock.ts +3 -8
  174. package/src/api/device/DeviceUpdateBootloader.ts +122 -6
  175. package/src/api/firmware/FirmwareArtifactSource.ts +278 -0
  176. package/src/api/firmware/FirmwareHostBinding.ts +100 -0
  177. package/src/api/firmware/FirmwarePreparationError.ts +12 -0
  178. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +83 -16
  179. package/src/api/firmware/FirmwareUpdateCapabilities.ts +9 -0
  180. package/src/api/firmware/FirmwareUpdatePlan.ts +772 -0
  181. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +415 -0
  182. package/src/api/firmware/progressThrottle.ts +44 -0
  183. package/src/api/firmware/updateBootloader.ts +19 -4
  184. package/src/api/firmware/uploadFirmware.ts +144 -39
  185. package/src/api/helpers/protocolV2FileWrite.ts +7 -27
  186. package/src/api/index.ts +0 -2
  187. package/src/api/kaspa/KaspaSignTransaction.ts +12 -1
  188. package/src/api/protocol-v2/ProtocolInfoRequest.ts +1 -3
  189. package/src/api/protocol-v2/helpers.ts +0 -1
  190. package/src/core/deviceEventRegistration.ts +0 -4
  191. package/src/core/index.ts +83 -167
  192. package/src/data/messages/messages-protocol-v2.json +0 -25
  193. package/src/data-manager/DataManager.ts +39 -71
  194. package/src/data-manager/connectSettings.ts +11 -0
  195. package/src/device/Device.ts +43 -259
  196. package/src/device/DeviceCommands.ts +1 -7
  197. package/src/device/DevicePool.ts +2 -7
  198. package/src/events/device.ts +0 -4
  199. package/src/events/logBlockEvent.ts +0 -1
  200. package/src/events/ui-promise.ts +2 -4
  201. package/src/events/ui-request.ts +2 -36
  202. package/src/events/ui-response.ts +2 -12
  203. package/src/index.ts +5 -0
  204. package/src/inject.ts +24 -60
  205. package/src/lowLevelInject.ts +8 -7
  206. package/src/protocols/protocol-v2/features.ts +7 -10
  207. package/src/protocols/protocol-v2/firmware.ts +0 -2
  208. package/src/protocols/protocol-v2/lockedError.ts +10 -0
  209. package/src/protocols/protocol-v2/uiInteraction.ts +6 -46
  210. package/src/protocols/protocol-v2/unlockPolicy.ts +105 -1
  211. package/src/protocols/protocol-v2/unlockRetry.ts +109 -0
  212. package/src/protocols/protocol-v2/walletSession.ts +36 -214
  213. package/src/topLevelInject.ts +8 -7
  214. package/src/types/api/checkAllFirmwareRelease.ts +4 -2
  215. package/src/types/api/deviceUnlock.ts +1 -12
  216. package/src/types/api/deviceUpdateBootloader.ts +6 -0
  217. package/src/types/api/export.ts +18 -1
  218. package/src/types/api/firmwareUpdate.ts +76 -3
  219. package/src/types/api/firmwareUpdateCapabilities.ts +9 -0
  220. package/src/types/api/firmwareUpdatePlan.ts +38 -0
  221. package/src/types/api/firmwareUpdatePreparedPlan.ts +53 -0
  222. package/src/types/api/index.ts +14 -13
  223. package/src/types/api/protocolV2.ts +0 -13
  224. package/src/types/device.ts +0 -3
  225. package/src/types/params.ts +1 -7
  226. package/src/types/settings.ts +13 -42
  227. package/src/utils/deviceFeaturesCompat.ts +6 -0
  228. package/src/utils/deviceFeaturesUtils.ts +0 -8
  229. package/src/utils/homescreen.ts +6 -32
  230. package/src/utils/index.ts +1 -6
  231. package/src/utils/pro2Wallpaper.ts +8 -35
  232. package/__tests__/deviceUploadNft.test.ts +0 -293
  233. package/__tests__/pro2Nft.test.ts +0 -108
  234. package/__tests__/protocol-binding.test.ts +0 -89
  235. package/__tests__/protocol-v2-resources.test.ts +0 -286
  236. package/__tests__/protocol-v2-ui-lifecycle.test.ts +0 -91
  237. package/__tests__/uiProgressThrottle.test.ts +0 -74
  238. package/__tests__/uiPromiseRegistry.test.ts +0 -86
  239. package/dist/api/DetectDeviceConnectProtocol.d.ts +0 -7
  240. package/dist/api/DetectDeviceConnectProtocol.d.ts.map +0 -1
  241. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +0 -31
  242. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +0 -1
  243. package/dist/core/uiPromiseRegistry.d.ts +0 -4
  244. package/dist/core/uiPromiseRegistry.d.ts.map +0 -1
  245. package/dist/protocols/protocol-v2/resources.d.ts +0 -30
  246. package/dist/protocols/protocol-v2/resources.d.ts.map +0 -1
  247. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +0 -22
  248. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +0 -1
  249. package/dist/types/api/detectDeviceConnectProtocol.d.ts +0 -4
  250. package/dist/types/api/detectDeviceConnectProtocol.d.ts.map +0 -1
  251. package/dist/utils/pro2Nft.d.ts +0 -31
  252. package/dist/utils/pro2Nft.d.ts.map +0 -1
  253. package/dist/utils/uiProgressThrottle.d.ts +0 -3
  254. package/dist/utils/uiProgressThrottle.d.ts.map +0 -1
  255. package/src/api/DetectDeviceConnectProtocol.ts +0 -18
  256. package/src/api/protocol-v2/DeviceUploadNft.ts +0 -189
  257. package/src/core/uiPromiseRegistry.ts +0 -41
  258. package/src/protocols/protocol-v2/resources.ts +0 -359
  259. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +0 -118
  260. package/src/types/api/detectDeviceConnectProtocol.ts +0 -6
  261. package/src/utils/pro2Nft.ts +0 -142
  262. 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,72 +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
- if (err instanceof HardwareError && err.errorCode === HardwareErrorCode.NetworkError) {
535
- throw err;
536
- }
537
- throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateDownloadFailed, err.message ?? err);
639
+ throw normalizeFirmwarePreparationError(err);
538
640
  }
539
641
 
540
- if (
541
- !bootloaderBinary &&
542
- fwBinaryMap.length === 0 &&
543
- !installItems?.length &&
544
- !resourceBundles?.length &&
545
- !needsRemoteResources
546
- ) {
642
+ if (!bootloaderBinary && fwBinaryMap.length === 0 && !resourceBundles?.length) {
547
643
  throw ERRORS.TypedError(
548
644
  HardwareErrorCode.FirmwareUpdateDownloadFailed,
549
645
  'No firmware to update'
550
646
  );
551
647
  }
552
648
 
553
- const enteredBootloader = await this.enterProtocolV2BootloaderMode();
554
-
555
- if (needsRemoteResources) {
556
- try {
557
- resourceBundles = await this.prepareProtocolV2ResourceBundles();
558
- this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
559
- } catch (err) {
560
- if (enteredBootloader) {
561
- try {
562
- await this.exitProtocolV2BootloaderToNormal();
563
- } catch (restoreError) {
564
- Log.warn(
565
- '[FirmwareUpdateV4] failed to restore App mode after resource preparation error:',
566
- restoreError
567
- );
568
- }
569
- }
570
- throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateDownloadFailed, err.message ?? err);
571
- }
572
- }
573
-
574
- await this.executeProtocolV2Update({
649
+ return this.executeProtocolV2Update({
575
650
  fwBinaryMap,
576
651
  bootloaderBinary,
577
652
  ...(installItems ? { installItems } : undefined),
578
653
  ...(resourceBundles?.length ? { resourceBundles } : undefined),
579
654
  });
655
+ }
580
656
 
581
- this.postTipMessage(FirmwareUpdateTipMessage.SwitchFirmwareReconnectDevice);
582
- 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
+ }
583
674
 
584
- const versions = await this.waitForProtocolV2FinalFeatures();
585
- this.postTipMessage(FirmwareUpdateTipMessage.FirmwareUpdateCompleted);
586
- 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
+ }
587
686
 
588
- 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);
589
932
  }
590
933
 
591
934
  private async getProtocolV2DeviceFeatures(): Promise<Features> {
@@ -611,23 +954,26 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
611
954
  );
612
955
  }
613
956
 
614
- private getProtocolV2DeviceInfoTimeout() {
615
- return this.isBleReconnect()
616
- ? this.payload.protocolV2DeviceInfoTimeoutMs ?? PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT
617
- : PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT;
618
- }
619
-
620
957
  private async requestProtocolV2PhysicalIdentity() {
621
958
  return requestProtocolV2DeviceInfo({
622
959
  commands: this.device.getCommands(),
623
- timeoutMs: this.getProtocolV2DeviceInfoTimeout(),
960
+ timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
624
961
  });
625
962
  }
626
963
 
627
964
  private async captureProtocolV2PhysicalIdentity() {
628
965
  const deviceInfo = await this.requestProtocolV2PhysicalIdentity();
629
966
  const serialNumber = this.getProtocolV2SerialNumber(deviceInfo);
630
- 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
+ }
631
977
  this.protocolV2ExpectedSerialNumber = serialNumber;
632
978
  }
633
979
 
@@ -645,7 +991,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
645
991
  return (
646
992
  !!this.params.resourceBundleFiles?.length ||
647
993
  !!this.params.bootloaderBinary ||
648
- !!this.params.bootResourcesBinary ||
649
994
  fwBinaryMap.length > 0
650
995
  );
651
996
  }
@@ -707,6 +1052,57 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
707
1052
  return target;
708
1053
  }
709
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
+
710
1106
  private async downloadRemoteProtocolV2Component(
711
1107
  key: string,
712
1108
  component: IProtocolV2FirmwareComponent
@@ -795,159 +1191,155 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
795
1191
  };
796
1192
  }
797
1193
 
798
- private validateProtocolV2BootResourcesBinary(
799
- binary: ArrayBuffer,
800
- resource?: IProtocolV2BootResources
801
- ) {
802
- if (resource && !isProtocolV2ResourceFileValid(binary, resource)) {
803
- throw new Error('Pro2 boot resources file verification failed');
804
- }
805
- const header = parseProtocolV2OkppHeader(toProtocolV2Bytes(binary));
806
- if (!header || header.type !== 'CRAT') {
807
- throw new Error('Invalid Pro2 boot resources CRATE header');
808
- }
809
- if (resource) {
810
- const expectedPayloadHash = normalizeProtocolV2Hex(resource.payloadHash);
811
- const expectedHeaderHash = normalizeProtocolV2Hex(resource.headerHash);
812
- if (header.payloadHash !== expectedPayloadHash) {
813
- throw new Error('Pro2 boot resources payload hash mismatch');
814
- }
815
- if (header.headerHash !== expectedHeaderHash) {
816
- throw new Error('Pro2 boot resources header hash mismatch');
817
- }
818
- }
819
- }
820
-
821
- private async prepareProtocolV2BootResources(): Promise<ProtocolV2InstallItem | undefined> {
822
- let binary = this.params.bootResourcesBinary;
823
- let resource: IProtocolV2BootResources | undefined;
1194
+ // ============================================================
1195
+ // Incremental RESC bundle sync through FileRead and FilesystemFileWrite
1196
+ // ============================================================
824
1197
 
825
- if (!binary) {
826
- if (!this.params.targetsToUpdate?.includes('boot_resources')) {
827
- return undefined;
828
- }
829
- resource = DataManager.getProtocolV2BootResources();
830
- if (!resource) {
831
- throw new Error('Missing Pro2 boot resources configuration');
832
- }
833
- Log.log('[FirmwareUpdateV4] downloading Pro2 boot resources CRATE');
834
- ({ 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
+ });
835
1223
  }
836
1224
 
837
- this.validateProtocolV2BootResourcesBinary(binary, resource);
838
- return {
839
- fileName: PROTOCOL_V2_BOOT_RESOURCES_FILE_NAME,
840
- binary,
841
- targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_CRATE,
842
- kind: 'boot_resources',
843
- };
844
- }
845
-
846
- private prepareExplicitProtocolV2ResourceBundles(): ProtocolV2ResourceBundleBinary[] | undefined {
847
- if (!this.params.resourceBundleFiles?.length) return undefined;
848
-
849
- return this.params.resourceBundleFiles.map((file, index) => {
850
- const devicePath = validateProtocolV2FilesystemPath(
851
- file.devicePath,
852
- `resourceBundleFiles[${index}].devicePath`
853
- );
854
- return {
855
- name: devicePath.split('/').pop() ?? devicePath,
856
- binary: file.binary,
857
- devicePath,
858
- };
859
- });
860
- }
861
-
862
- private async prepareProtocolV2ResourceBundles(): Promise<
863
- ProtocolV2ResourceBundleBinary[] | undefined
864
- > {
865
1225
  if (!this.params.targetsToUpdate?.includes('resource')) {
866
1226
  return undefined;
867
1227
  }
868
1228
 
869
- const resources = DataManager.getProtocolV2Resources();
870
- if (!resources?.length) {
871
- throw new Error('Missing Pro2 stable resource configuration');
872
- }
873
-
874
- const inventory = this.params.forcedUpdateRes
875
- ? undefined
876
- : await readProtocolV2ResourceInventory({
877
- commands: this.device.getCommands(),
878
- resources,
879
- chunkSize: this.getProtocolV2FirmwareChunkSize(),
880
- timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
881
- });
882
- const plan = buildProtocolV2ResourceUpdatePlan({
883
- resources,
884
- inventory,
885
- mode: 'bootloader-recovery',
886
- forced: this.params.forcedUpdateRes,
887
- });
888
- Log.log(
889
- `[FirmwareUpdateV4] Pro2 resource plan mode=bootloader-recovery status=${plan.status} count=${plan.resources.length}`
890
- );
891
-
892
- const bundles: ProtocolV2ResourceBundleBinary[] = [];
893
- for (const resource of plan.resources) {
894
- 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
+ }
895
1301
  }
896
1302
  return bundles;
897
1303
  }
898
1304
 
899
- private async downloadProtocolV2Resource(
900
- resource: IProtocolV2Resource
901
- ): Promise<ProtocolV2ResourceBundleBinary> {
902
- Log.log(`[FirmwareUpdateV4] downloading Pro2 resource ${resource.type}`);
903
- const { binary } = await getSysResourceBinary(resource.url);
904
- if (!isProtocolV2ResourceFileValid(binary, resource)) {
905
- throw new Error(`Pro2 resource file verification failed: ${resource.type}`);
906
- }
907
- return {
908
- name: `${resource.type}.okpkg`,
909
- binary,
910
- devicePath: PROTOCOL_V2_RESOURCE_DEVICE_PATHS[resource.type],
911
- };
912
- }
913
-
914
- private async syncProtocolV2ResourceBundles(
915
- bundles: ProtocolV2ResourceBundleBinary[],
916
- firmwareSize: number
917
- ): Promise<{ processedSize: number; totalSize: number }> {
918
- const transferStartTime = Date.now();
919
- 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;
920
1316
 
921
- // Write directly to the device through FileWrite.
922
- let totalSize = 0;
923
- for (const b of bundles) totalSize += b.binary.byteLength;
1317
+ try {
1318
+ const header = await this.readProtocolV2DeviceFileHeader(bundle.devicePath);
1319
+ if (!header) return false;
924
1320
 
925
- const transferTotalSize = totalSize + firmwareSize;
926
- let processedSize = 0;
927
- for (const bundle of bundles) {
928
- Log.log(
929
- `[FirmwareUpdateV4] syncing RESC bundle ${bundle.name} -> ${bundle.devicePath} bytes=${bundle.binary.byteLength}`
930
- );
931
- processedSize = await this.protocolV2CommonUpdateProcess({
932
- payload: bundle.binary,
933
- filePath: bundle.devicePath,
934
- processedSize,
935
- totalSize: transferTotalSize,
936
- });
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;
937
1337
  }
938
-
939
- const elapsedMs = Date.now() - transferStartTime;
940
- Log.log(
941
- `[FirmwareUpdateV4] RESC bundle sync finished transport=${transferTransport} bytes=${totalSize} elapsed=${(
942
- elapsedMs / 1000
943
- ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(totalSize, elapsedMs)} KB/s`
944
- );
945
- return { processedSize, totalSize: transferTotalSize };
946
1338
  }
947
1339
 
948
1340
  private isProtocolV2BootloaderMode() {
949
1341
  if (typeof this.device.isBootloader === 'function') {
950
- return !!this.device.isBootloader();
1342
+ return this.device.isBootloader();
951
1343
  }
952
1344
  return (
953
1345
  this.device.features?.mode === 'bootloader' ||
@@ -970,10 +1362,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
970
1362
  // It rejects DeviceRebootType.Bootloader, so reuse the current connection.
971
1363
  if (this.isProtocolV2RomloaderMode()) {
972
1364
  Log.debug('Protocol V2 device is in romloader mode; start firmware update directly');
1365
+ this.protocolV2ExecutionInLoader = true;
973
1366
  return false;
974
1367
  }
975
1368
  if (this.isProtocolV2BootloaderMode()) {
976
1369
  Log.debug('Protocol V2 device is already in bootloader mode, skip reboot');
1370
+ this.protocolV2ExecutionInLoader = true;
977
1371
  return false;
978
1372
  }
979
1373
 
@@ -983,6 +1377,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
983
1377
  this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
984
1378
  await wait(1000);
985
1379
  await this.waitForProtocolV2BootloaderMode();
1380
+ this.protocolV2ExecutionInLoader = true;
986
1381
  return true;
987
1382
  } catch (error) {
988
1383
  if (error instanceof HardwareError) {
@@ -999,17 +1394,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
999
1394
  ) {
1000
1395
  const startTime = Date.now();
1001
1396
  let lastError: unknown;
1002
- let shouldReconnect = true;
1003
1397
 
1004
1398
  while (Date.now() - startTime < timeout) {
1005
1399
  try {
1006
- if (shouldReconnect) {
1007
- await this.reconnectProtocolV2Device();
1008
- shouldReconnect = false;
1009
- }
1400
+ await this.reconnectProtocolV2Device();
1010
1401
  const deviceInfo = await requestProtocolV2DeviceInfo({
1011
1402
  commands: this.device.getCommands(),
1012
- timeoutMs: this.getProtocolV2DeviceInfoTimeout(),
1403
+ timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
1013
1404
  });
1014
1405
  this.assertProtocolV2DeviceInfoIdentity(deviceInfo);
1015
1406
  const features = await this.device.probeProtocolV2RuntimeState(
@@ -1024,7 +1415,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1024
1415
  if (this.isProtocolV2ReconnectIdentityError(error)) {
1025
1416
  throw error;
1026
1417
  }
1027
- shouldReconnect = true;
1028
1418
  lastError = error;
1029
1419
  Log.log('Protocol V2 bootloader mode not ready, polling reconnect: ', error);
1030
1420
  }
@@ -1077,6 +1467,98 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1077
1467
  return entries;
1078
1468
  }
1079
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
+
1080
1562
  private async executeProtocolV2Update({
1081
1563
  fwBinaryMap,
1082
1564
  bootloaderBinary,
@@ -1088,104 +1570,158 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1088
1570
  installItems?: ProtocolV2InstallItem[];
1089
1571
  resourceBundles?: ProtocolV2ResourceBundleBinary[];
1090
1572
  }) {
1091
- const orderedInstallItems =
1573
+ const memoryInstallItems =
1092
1574
  installItems ??
1093
1575
  this.buildProtocolV2InstallItems({
1094
- bootloaderBinary: bootloaderBinary ?? null,
1095
1576
  fwBinaryMap: fwBinaryMap ?? [],
1577
+ bootloaderBinary: bootloaderBinary ?? null,
1096
1578
  });
1097
-
1098
- let firmwareSize = 0;
1099
- for (const item of orderedInstallItems) firmwareSize += item.binary.byteLength;
1100
-
1101
- this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
1102
-
1103
- let processedSize = 0;
1104
- let totalSize = firmwareSize;
1105
- if (resourceBundles?.length) {
1106
- const resourceTransfer = await this.syncProtocolV2ResourceBundles(
1107
- resourceBundles,
1108
- 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
+ }))
1587
+ );
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
+ }))
1109
1597
  );
1110
- processedSize = resourceTransfer.processedSize;
1111
- totalSize = resourceTransfer.totalSize;
1598
+ return await this.executeProtocolV2Phases({
1599
+ installSources,
1600
+ resourceSources,
1601
+ });
1602
+ } finally {
1603
+ await this.closeProtocolV2PreparedSources();
1112
1604
  }
1605
+ }
1113
1606
 
1114
- // Skip staging and installation when the update contains RESC bundles only.
1115
- if (orderedInstallItems.length === 0) {
1116
- Log.log('[FirmwareUpdateV4] no firmware targets to install (RESC bundles only)');
1117
- if (totalSize > 0) {
1118
- 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;
1119
1619
  }
1120
- return;
1121
1620
  }
1122
- let transferredSize = processedSize;
1123
- const transferStartTime = Date.now();
1124
- const transferTransport = this.getProtocolV2FirmwareTransferTransport();
1125
- const chunkSize = this.getProtocolV2FirmwareChunkSize();
1126
- const onTransferredBytes = (bytes: number) => {
1127
- transferredSize = bytes;
1128
- };
1129
- Log.log(
1130
- `[FirmwareUpdateV4] transfer started transport=${transferTransport} total=${totalSize} bytes chunk=${chunkSize} bytes`
1131
- );
1132
-
1133
- const stagedInstallTargets: ProtocolV2InstallTarget[] = [];
1134
-
1135
- try {
1136
- for (const item of orderedInstallItems) {
1137
- const filePath = this.getProtocolV2InstallItemStagingPath(item);
1138
- Log.log(
1139
- `[FirmwareUpdateV4] staging ${item.kind} via FilesystemFileWrite target=${item.targetId} path=${filePath} source=${item.fileName} bytes=${item.binary.byteLength}`
1140
- );
1141
- processedSize = await this.protocolV2CommonUpdateProcess({
1142
- payload: item.binary,
1143
- filePath,
1144
- processedSize,
1145
- totalSize,
1146
- onTransferredBytes,
1147
- });
1148
- transferredSize = processedSize;
1149
- await this.verifyProtocolV2StagedFile(filePath, item.binary.byteLength);
1150
1621
 
1151
- stagedInstallTargets.push({
1152
- ...item,
1153
- path: filePath,
1154
- });
1155
- }
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
+ }
1156
1633
 
1157
- if (totalSize > 0) {
1158
- this.postProgressMessage(100, 'transferData');
1159
- }
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
+ }
1160
1649
 
1161
- const elapsedMs = Date.now() - transferStartTime;
1162
- Log.log(
1163
- `[FirmwareUpdateV4] transfer finished transport=${transferTransport} bytes=${totalSize} elapsed=${(
1164
- elapsedMs / 1000
1165
- ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(totalSize, elapsedMs)} KB/s`
1166
- );
1167
- } catch (error) {
1168
- const elapsedMs = Date.now() - transferStartTime;
1169
- Log.warn(
1170
- `[FirmwareUpdateV4] transfer failed transport=${transferTransport} bytes=${transferredSize}/${totalSize} elapsed=${(
1171
- elapsedMs / 1000
1172
- ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(transferredSize, elapsedMs)} KB/s`
1173
- );
1174
- throw error;
1650
+ if (totalSize > 0) {
1651
+ this.postProgressMessage(100, 'transferData');
1652
+ }
1653
+ if (stagedInstallTargets.length === 0) {
1654
+ return;
1175
1655
  }
1176
1656
 
1177
1657
  this.postTipMessage(FirmwareUpdateTipMessage.ConfirmOnDevice);
1178
-
1179
- const allTargets = stagedInstallTargets.map(item => ({
1658
+ const targets = stagedInstallTargets.map(item => ({
1180
1659
  target_id: item.targetId,
1181
1660
  path: item.path,
1182
1661
  }));
1183
- Log.log(`[FirmwareUpdateV4] DeviceFirmwareUpdateRequest targets=${JSON.stringify(allTargets)}`);
1184
- await this.protocolV2StartFirmwareUpdate({ targets: allTargets });
1185
- 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
+ );
1186
1722
  }
1187
1723
 
1188
- private getProtocolV2InstallItemStagingPath(item: ProtocolV2InstallItem) {
1724
+ private getProtocolV2InstallItemStagingPath(item: Pick<ProtocolV2InstallItem, 'fileName'>) {
1189
1725
  return `${PROTOCOL_V2_FIRMWARE_STAGING_VOLUME}${item.fileName}`;
1190
1726
  }
1191
1727
 
@@ -1204,7 +1740,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1204
1740
 
1205
1741
  private assertProtocolV2TargetStatus(
1206
1742
  statusTargets: ProtocolV2FirmwareUpdateStatusTarget[],
1207
- expectedTargetIds: Set<number>
1743
+ expectedTargetIds: Set<number>,
1744
+ expectedPaths = new Map<number, string>()
1208
1745
  ) {
1209
1746
  Log.log(
1210
1747
  `[FirmwareUpdateV4] DeviceFirmwareUpdateStatus records=${JSON.stringify(statusTargets)}`
@@ -1235,17 +1772,54 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1235
1772
  const matchingTargets = statusTargets.filter(target =>
1236
1773
  expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1)
1237
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
+ );
1238
1793
  const completedTargetIds = new Set<number>();
1239
- matchingTargets.forEach(target => {
1794
+ completedTargets.forEach(target => {
1240
1795
  const targetId = normalizeProtocolV2TargetId(target.target_id);
1241
- if (targetId !== undefined && isProtocolV2TargetStatusFinished(target.status)) {
1796
+ if (targetId !== undefined) {
1242
1797
  completedTargetIds.add(targetId);
1243
1798
  }
1244
1799
  });
1245
- const allExpectedTargetsCompleted =
1246
- expectedTargetIds.size > 0 &&
1247
- Array.from(expectedTargetIds).every(targetId => completedTargetIds.has(targetId));
1248
- 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
+ }
1249
1823
  this.postProgressMessage(100, 'installingFirmware');
1250
1824
  return true;
1251
1825
  }
@@ -1266,43 +1840,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1266
1840
  return false;
1267
1841
  }
1268
1842
 
1269
- private getProtocolV2MissingTargetIds(
1270
- statusTargets: ProtocolV2FirmwareUpdateStatusTarget[],
1271
- expectedTargetIds: Set<number>
1272
- ) {
1273
- const reportedTargetIds = new Set<number>();
1274
- statusTargets.forEach(target => {
1275
- const targetId = normalizeProtocolV2TargetId(target.target_id);
1276
- if (targetId !== undefined) {
1277
- reportedTargetIds.add(targetId);
1278
- }
1279
- });
1280
- return Array.from(expectedTargetIds).filter(targetId => !reportedTargetIds.has(targetId));
1281
- }
1282
-
1283
1843
  private async waitForProtocolV2FirmwareUpdateComplete(
1284
1844
  targets: Array<{ target_id: number; path: string }>
1285
1845
  ) {
1846
+ this.protocolV2FinalStatusVerified = false;
1286
1847
  const expectedTargetIds = new Set(targets.map(target => target.target_id));
1848
+ const expectedPaths = new Map(targets.map(target => [target.target_id, target.path]));
1287
1849
  const startTime = Date.now();
1288
1850
  let lastError: unknown;
1289
- let shouldReconnect = true;
1290
- let deviceInfo: ProtocolV2DeviceInfo | undefined;
1291
- let missingTargetStatusSince: number | undefined;
1292
- let missingTargetStatusKey: string | undefined;
1293
- const resetMissingTargetStatusGrace = () => {
1294
- missingTargetStatusSince = undefined;
1295
- missingTargetStatusKey = undefined;
1296
- };
1297
1851
 
1298
1852
  while (Date.now() - startTime < PROTOCOL_V2_INSTALL_TIMEOUT) {
1299
1853
  try {
1300
- if (shouldReconnect) {
1301
- await this.reconnectProtocolV2Device();
1302
- deviceInfo = await this.verifyProtocolV2ReconnectIdentity();
1303
- shouldReconnect = false;
1304
- }
1305
- const currentDeviceInfo = deviceInfo;
1854
+ await this.reconnectProtocolV2Device();
1855
+ const deviceInfo = await this.verifyProtocolV2ReconnectIdentity();
1306
1856
  try {
1307
1857
  const statusResponse = await this.device.getCommands().typedCall(
1308
1858
  'DeviceFirmwareUpdateStatusGet',
@@ -1314,57 +1864,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1314
1864
  path: true,
1315
1865
  },
1316
1866
  },
1317
- { timeoutMs: PROTOCOL_V2_FIRMWARE_STATUS_RESPONSE_TIMEOUT }
1867
+ { timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT }
1318
1868
  );
1319
- const statusTargets = (statusResponse.message.records ??
1320
- []) as ProtocolV2FirmwareUpdateStatusTarget[];
1321
- if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds)) {
1322
- return;
1323
- }
1324
-
1325
1869
  if (
1326
- statusTargets.length === 0 &&
1327
- currentDeviceInfo &&
1328
- (await this.probeProtocolV2NormalMode(currentDeviceInfo))
1870
+ this.assertProtocolV2TargetStatus(
1871
+ (statusResponse.message.records ?? []) as ProtocolV2FirmwareUpdateStatusTarget[],
1872
+ expectedTargetIds,
1873
+ expectedPaths
1874
+ )
1329
1875
  ) {
1330
- Log.log(
1331
- '[FirmwareUpdateV4] empty firmware status after confirmed App reboot; update complete'
1332
- );
1333
- this.postProgressMessage(100, 'installingFirmware');
1876
+ this.protocolV2FinalStatusVerified = true;
1334
1877
  return;
1335
1878
  }
1336
-
1337
- const missingTargetIds = this.getProtocolV2MissingTargetIds(
1338
- statusTargets,
1339
- expectedTargetIds
1340
- );
1341
- if (missingTargetIds.length > 0) {
1342
- const now = Date.now();
1343
- const missingKey = missingTargetIds.join(',');
1344
- if (missingTargetStatusSince === undefined || missingTargetStatusKey !== missingKey) {
1345
- missingTargetStatusSince = now;
1346
- missingTargetStatusKey = missingKey;
1347
- } else if (
1348
- now - missingTargetStatusSince >=
1349
- PROTOCOL_V2_MISSING_TARGET_STATUS_GRACE_TIMEOUT
1350
- ) {
1351
- const reportedTargetIds = statusTargets
1352
- .map(target => normalizeProtocolV2TargetId(target.target_id))
1353
- .filter((targetId): targetId is number => targetId !== undefined);
1354
- throw ERRORS.TypedError(
1355
- HardwareErrorCode.FirmwareError,
1356
- `Protocol V2 firmware status is missing requested records: targetIds=${missingKey} reportedTargetIds=${reportedTargetIds.join(
1357
- ','
1358
- )}`
1359
- );
1360
- }
1361
- lastError = new Error(
1362
- `Protocol V2 firmware status is temporarily missing targetIds=${missingKey}`
1363
- );
1364
- } else {
1365
- resetMissingTargetStatusGrace();
1366
- lastError = new Error('Protocol V2 firmware targets are still installing');
1367
- }
1879
+ lastError = new Error('Protocol V2 firmware targets are still installing');
1368
1880
  } catch (error) {
1369
1881
  if (
1370
1882
  error instanceof HardwareError &&
@@ -1372,31 +1884,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1372
1884
  ) {
1373
1885
  throw error;
1374
1886
  }
1375
- // Only consecutive full status dumps can prove that a record is absent.
1376
- // A reboot or transport interruption starts a fresh grace window.
1377
- resetMissingTargetStatusGrace();
1378
1887
  // App firmware does not register DeviceFirmwareUpdateStatusGet. Treat the
1379
1888
  // missing endpoint as completion only after the runtime probe confirms App mode.
1380
1889
  if (isProtocolV2FirmwareStatusEndpointUnavailable(error)) {
1381
- if (!currentDeviceInfo) {
1382
- throw ERRORS.TypedError(
1383
- HardwareErrorCode.RuntimeError,
1384
- 'Protocol V2 device identity is unavailable during install polling'
1385
- );
1386
- }
1387
- if (await this.probeProtocolV2NormalMode(currentDeviceInfo)) {
1890
+ if (await this.probeProtocolV2NormalMode(deviceInfo)) {
1388
1891
  Log.log(
1389
1892
  '[FirmwareUpdateV4] firmware status endpoint unavailable after confirmed App reboot'
1390
1893
  );
1391
- this.postProgressMessage(100, 'installingFirmware');
1392
1894
  return;
1393
1895
  }
1394
1896
  lastError = new Error(
1395
1897
  'Protocol V2 firmware status endpoint is unavailable while the device remains in loader mode'
1396
1898
  );
1397
1899
  } else {
1398
- shouldReconnect = true;
1399
- deviceInfo = undefined;
1400
1900
  lastError = error;
1401
1901
  Log.log(
1402
1902
  '[FirmwareUpdateV4] DeviceFirmwareUpdateStatusGet unavailable during install: ',
@@ -1412,9 +1912,12 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1412
1912
  if (error instanceof HardwareError && error.errorCode === HardwareErrorCode.FirmwareError) {
1413
1913
  throw error;
1414
1914
  }
1415
- shouldReconnect = true;
1416
- deviceInfo = undefined;
1417
- resetMissingTargetStatusGrace();
1915
+ if (
1916
+ error instanceof HardwareError &&
1917
+ error.params?.firmwareUpdateCode === 'FirmwareInstallStatusUnavailable'
1918
+ ) {
1919
+ throw error;
1920
+ }
1418
1921
  Log.log('Protocol V2 firmware install device readiness probe failed: ', error);
1419
1922
  }
1420
1923
  await wait(1000);
@@ -1422,24 +1925,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1422
1925
 
1423
1926
  throw ERRORS.TypedError(
1424
1927
  HardwareErrorCode.RuntimeError,
1425
- `Protocol V2 firmware update status not complete within ${
1426
- PROTOCOL_V2_INSTALL_TIMEOUT / 1000
1427
- }s: ${this.normalizeErrorMessage(lastError)}`
1928
+ `Protocol V2 firmware update status timeout: ${this.normalizeErrorMessage(lastError)}`
1428
1929
  );
1429
1930
  }
1430
1931
 
1431
1932
  private async exitProtocolV2BootloaderToNormal() {
1432
1933
  await this.reconnectProtocolV2Device();
1433
- const deviceInfo = await this.verifyProtocolV2ReconnectIdentity();
1434
- try {
1435
- if (await this.probeProtocolV2NormalMode(deviceInfo)) {
1436
- Log.log('[FirmwareUpdateV4] device already returned to App mode; skip Normal reboot');
1437
- return;
1438
- }
1439
- } catch (error) {
1440
- Log.log('[FirmwareUpdateV4] unable to confirm App mode before Normal reboot: ', error);
1441
- }
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.
1442
1937
  await this.protocolV2Reboot(DeviceRebootType.Normal);
1938
+ this.protocolV2ExecutionInLoader = false;
1443
1939
  }
1444
1940
 
1445
1941
  private async probeProtocolV2NormalMode(deviceInfo: ProtocolV2DeviceInfo) {
@@ -1455,13 +1951,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1455
1951
  PROTOCOL_V2_FINAL_RECONNECT_TIMEOUT
1456
1952
  );
1457
1953
 
1458
- return this.getProtocolV2VersionResult(features);
1459
- }
1460
-
1461
- private getProtocolV2VersionResult(features: Features) {
1462
1954
  const bootloaderVersion = getDeviceBootloaderVersion(features).join('.');
1463
1955
  const bleVersion = getDeviceBLEFirmwareVersion(features).join('.');
1464
1956
  const firmwareVersion = getDeviceFirmwareVersion(features).join('.');
1957
+ this.protocolV2LatestFinalFeatures = features;
1465
1958
  if (firmwareVersion === '0.0.0') {
1466
1959
  Log.warn(
1467
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.'
@@ -1475,20 +1968,27 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1475
1968
  };
1476
1969
  }
1477
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
+
1478
1982
  private async waitForProtocolV2ReconnectAndFeatures(timeout: number) {
1479
1983
  const startTime = Date.now();
1480
1984
  let lastError: unknown;
1481
- let shouldReconnect = true;
1482
1985
 
1483
1986
  while (Date.now() - startTime < timeout) {
1484
1987
  try {
1485
- if (shouldReconnect) {
1486
- await this.reconnectProtocolV2Device();
1487
- shouldReconnect = false;
1488
- }
1988
+ await this.reconnectProtocolV2Device();
1489
1989
  const deviceInfo = await requestProtocolV2DeviceInfo({
1490
1990
  commands: this.device.getCommands(),
1491
- timeoutMs: this.getProtocolV2DeviceInfoTimeout(),
1991
+ timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
1492
1992
  // Completion needs target versions only; keep scope aligned with the request.
1493
1993
  request: PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST,
1494
1994
  });
@@ -1497,22 +1997,21 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1497
1997
  deviceInfo,
1498
1998
  PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT
1499
1999
  );
1500
- if (features.mode === 'normal' && !features.bootloaderMode) {
1501
- 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
+ );
1502
2005
  }
1503
- lastError = ERRORS.TypedError(
1504
- HardwareErrorCode.DeviceNotFound,
1505
- 'Protocol V2 device is still in bootloader mode'
1506
- );
2006
+ return features;
1507
2007
  } catch (error) {
1508
2008
  if (this.isProtocolV2ReconnectIdentityError(error)) {
1509
2009
  throw error;
1510
2010
  }
1511
- shouldReconnect = true;
1512
2011
  lastError = error;
1513
2012
  Log.log('Protocol V2 normal mode not ready, polling Ping: ', error);
2013
+ await wait(1000);
1514
2014
  }
1515
- await wait(1000);
1516
2015
  }
1517
2016
 
1518
2017
  throw ERRORS.TypedError(
@@ -1549,33 +2048,20 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1549
2048
  return;
1550
2049
  }
1551
2050
 
1552
- // Reinitialize every USB descriptor so DeviceInfo can provide the physical serial.
1553
- // 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.
1554
2053
  const { deviceList } = await DevicePool.getDevices(devicesDescriptor, undefined, {
1555
2054
  connectProtocol: PROTOCOL_V2_CONNECT_PROTOCOL,
1556
2055
  });
1557
- const expectedSerialNumber = this.protocolV2ExpectedSerialNumber?.trim();
1558
- const identityMatch = expectedSerialNumber
1559
- ? deviceList.find(
1560
- candidate => candidate.getCurrentSerialNo?.().trim() === expectedSerialNumber
1561
- )
1562
- : undefined;
1563
- const singleCandidate = deviceList.length === 1 ? deviceList.at(0) : undefined;
1564
- const singleCandidateSerialNumber = singleCandidate?.getCurrentSerialNo?.().trim();
1565
- const reconnectDevice =
1566
- identityMatch ??
1567
- (singleCandidate && (!expectedSerialNumber || !singleCandidateSerialNumber)
1568
- ? singleCandidate
1569
- : undefined);
1570
- if (!reconnectDevice) {
2056
+ if (deviceList.length !== 1) {
1571
2057
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
1572
2058
  }
1573
2059
 
1574
2060
  Log.debug(
1575
- 'Protocol V2 firmware reconnect using matched device:',
1576
- reconnectDevice.getConnectId()
2061
+ 'Protocol V2 firmware reconnect using single enumerated device:',
2062
+ deviceList[0].getConnectId()
1577
2063
  );
1578
- this.device.updateFromCache(reconnectDevice);
2064
+ this.device.updateFromCache(deviceList[0]);
1579
2065
  await this.ensureProtocolV2DeviceAcquired();
1580
2066
  this.device.commands.disposed = false;
1581
2067
  this.device.getCommands().mainId = this.device.mainId ?? '';
@@ -1599,96 +2085,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1599
2085
  return this.normalizeErrorMessage(error).includes('Protocol V2 reconnect physical identity');
1600
2086
  }
1601
2087
 
1602
- private async protocolV2CommonUpdateProcess(params: ProtocolV2FileTransferParams) {
1603
- let lastError: unknown;
1604
- for (let attempt = 1; attempt <= PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT; attempt += 1) {
1605
- try {
1606
- return await this.protocolV2WriteWholeFile(params);
1607
- } catch (error) {
1608
- lastError = error;
1609
- Log.error(
1610
- `Protocol V2 file transfer failed path=${params.filePath} attempt=${attempt}/${PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT}; restarting from offset 0`,
1611
- error
1612
- );
1613
- if (attempt === PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT) {
1614
- break;
1615
- }
1616
- await this.recoverProtocolV2FileTransfer();
1617
- }
1618
- }
1619
-
1620
- throw ERRORS.TypedError(
1621
- HardwareErrorCode.EmmcFileWriteFirmwareError,
1622
- `transfer data error: ${getProtocolV2UnknownErrorText(lastError)}`
1623
- );
1624
- }
1625
-
1626
- private async protocolV2WriteWholeFile({
1627
- payload,
1628
- filePath,
1629
- processedSize,
1630
- totalSize,
1631
- onTransferredBytes,
1632
- }: ProtocolV2FileTransferParams) {
1633
- const chunkSize = this.getProtocolV2FirmwareChunkSize();
1634
- let offset = 0;
1635
- const getUploadProgress = (fileOffset: number) => {
1636
- if (totalSize !== undefined && processedSize !== undefined) {
1637
- return Math.min(Math.ceil(((processedSize + fileOffset) / totalSize) * 100), 99);
1638
- }
1639
- return Math.min(Math.ceil((fileOffset / payload.byteLength) * 100), 99);
1640
- };
1641
-
1642
- while (offset < payload.byteLength) {
1643
- const chunkEnd = Math.min(offset + chunkSize, payload.byteLength);
1644
- const chunkLength = chunkEnd - offset;
1645
- const chunk = payload.slice(offset, chunkEnd);
1646
- const overwrite = offset === 0;
1647
- const progress = getProtocolV2DeviceTransferProgress(
1648
- (processedSize ?? 0) + offset,
1649
- (processedSize ?? 0) + chunkEnd,
1650
- totalSize ?? payload.byteLength
1651
- );
1652
-
1653
- const writeRes = await this.fileWriteChunk(
1654
- filePath,
1655
- payload.byteLength,
1656
- offset,
1657
- chunk,
1658
- overwrite,
1659
- progress
1660
- );
1661
- const rawProcessedByte = writeRes.message.processed_byte;
1662
- const processedByte = Number(rawProcessedByte);
1663
- const nextOffset = rawProcessedByte === undefined ? offset + chunkLength : processedByte;
1664
- if (!Number.isFinite(nextOffset) || nextOffset <= offset || nextOffset > chunkEnd) {
1665
- throw ERRORS.TypedError(
1666
- HardwareErrorCode.EmmcFileWriteFirmwareError,
1667
- `invalid processed_byte ${writeRes.message.processed_byte} for offset ${offset}`
1668
- );
1669
- }
1670
- offset = nextOffset;
1671
- onTransferredBytes?.((processedSize ?? 0) + offset);
1672
- this.postProgressMessage(getUploadProgress(offset), 'transferData');
1673
- }
1674
-
1675
- return totalSize !== undefined ? (processedSize ?? 0) + payload.byteLength : 0;
1676
- }
1677
-
1678
- private getProtocolV2FirmwareTransferTransport() {
1679
- const env = DataManager.getSettings('env');
1680
- if (env && DataManager.isBleConnect(env)) {
1681
- return 'BLE';
1682
- }
1683
- if (
1684
- env &&
1685
- (DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env) || env === 'web')
1686
- ) {
1687
- return 'WebUSB';
1688
- }
1689
- return env ?? 'unknown';
1690
- }
1691
-
1692
2088
  private async fileWriteChunk(
1693
2089
  filePath: string,
1694
2090
  totalFileSize: number,
@@ -1698,22 +2094,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1698
2094
  progress: number | null
1699
2095
  ): Promise<TypedResponseMessage<'FilesystemFile'>> {
1700
2096
  const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
1701
- const writeRes = await typedCall(
1702
- 'FilesystemFileWrite',
1703
- 'FilesystemFile',
1704
- {
1705
- file: {
1706
- path: filePath,
1707
- offset,
1708
- total_size: totalFileSize,
1709
- data: chunk,
1710
- },
1711
- overwrite,
1712
- append: false,
1713
- ui_percentage: progress ?? undefined,
2097
+ const writeRes = await typedCall('FilesystemFileWrite', 'FilesystemFile', {
2098
+ file: {
2099
+ path: filePath,
2100
+ offset,
2101
+ total_size: totalFileSize,
2102
+ data: chunk,
1714
2103
  },
1715
- { writeWithResponse: true }
1716
- );
2104
+ overwrite,
2105
+ append: false,
2106
+ ui_percentage: progress ?? undefined,
2107
+ });
1717
2108
  if (writeRes.type !== 'FilesystemFile') {
1718
2109
  if ((writeRes as any).type === 'CallMethodError') {
1719
2110
  if (((writeRes as any).message.error ?? '').indexOf(SESSION_ERROR) > -1) {