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

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 (256) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +231 -0
  2. package/__tests__/DeviceCommands.test.ts +45 -0
  3. package/__tests__/DeviceEventRegistration.test.ts +6 -0
  4. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +198 -3
  5. package/__tests__/connectSettings.test.ts +5 -47
  6. package/__tests__/device-lifecycle-events.test.ts +105 -3
  7. package/__tests__/deviceUploadNft.test.ts +293 -0
  8. package/__tests__/firmware-update/firmware-update-v2-download-before-boot.test.ts +31 -59
  9. package/__tests__/get-device-state.test.ts +132 -12
  10. package/__tests__/homescreen.test.ts +41 -1
  11. package/__tests__/kaspaSignTransaction.test.ts +3 -26
  12. package/__tests__/logBlockEvent.test.ts +1 -1
  13. package/__tests__/method-protocol-support.test.ts +19 -0
  14. package/__tests__/open-wallet-session.test.ts +568 -37
  15. package/__tests__/pro2Nft.test.ts +108 -0
  16. package/__tests__/protocol-binding.test.ts +89 -0
  17. package/__tests__/protocol-v2-resources.test.ts +286 -0
  18. package/__tests__/protocol-v2-ui-lifecycle.test.ts +91 -0
  19. package/__tests__/protocol-v2-unlock-policy.test.ts +201 -35
  20. package/__tests__/protocol-v2.test.ts +1350 -1012
  21. package/__tests__/protocolV2UiInteraction.test.ts +63 -0
  22. package/__tests__/public-pro2-api-boundary.test.ts +1 -0
  23. package/__tests__/search-devices.test.ts +12 -3
  24. package/__tests__/uiProgressThrottle.test.ts +74 -0
  25. package/__tests__/uiPromiseRegistry.test.ts +86 -0
  26. package/dist/api/BaseMethod.d.ts +3 -1
  27. package/dist/api/BaseMethod.d.ts.map +1 -1
  28. package/dist/api/CheckAllFirmwareRelease.d.ts +2 -1
  29. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  30. package/dist/api/DetectDeviceConnectProtocol.d.ts +7 -0
  31. package/dist/api/DetectDeviceConnectProtocol.d.ts.map +1 -0
  32. package/dist/api/FirmwareUpdate.d.ts.map +1 -1
  33. package/dist/api/FirmwareUpdateV2.d.ts +0 -11
  34. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  35. package/dist/api/FirmwareUpdateV3.d.ts +3 -7
  36. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  37. package/dist/api/FirmwareUpdateV4.d.ts +11 -25
  38. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  39. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  40. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  41. package/dist/api/SearchDevices.d.ts.map +1 -1
  42. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  43. package/dist/api/device/DeviceUnlock.d.ts +2 -2
  44. package/dist/api/device/DeviceUnlock.d.ts.map +1 -1
  45. package/dist/api/device/DeviceUpdateBootloader.d.ts +1 -5
  46. package/dist/api/device/DeviceUpdateBootloader.d.ts.map +1 -1
  47. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +0 -9
  48. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  49. package/dist/api/firmware/getBinary.d.ts +0 -7
  50. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  51. package/dist/api/firmware/updateBootloader.d.ts +0 -2
  52. package/dist/api/firmware/updateBootloader.d.ts.map +1 -1
  53. package/dist/api/firmware/uploadFirmware.d.ts +1 -9
  54. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  55. package/dist/api/helpers/protocolV2FileWrite.d.ts +2 -0
  56. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  57. package/dist/api/index.d.ts +2 -0
  58. package/dist/api/index.d.ts.map +1 -1
  59. package/dist/api/kaspa/KaspaSignTransaction.d.ts.map +1 -1
  60. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +31 -0
  61. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -0
  62. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -1
  63. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  64. package/dist/api/stellar/StellarSignTransaction.d.ts +1 -1
  65. package/dist/core/deviceEventRegistration.d.ts +2 -0
  66. package/dist/core/deviceEventRegistration.d.ts.map +1 -1
  67. package/dist/core/index.d.ts.map +1 -1
  68. package/dist/core/uiPromiseRegistry.d.ts +4 -0
  69. package/dist/core/uiPromiseRegistry.d.ts.map +1 -0
  70. package/dist/data-manager/DataManager.d.ts +4 -2
  71. package/dist/data-manager/DataManager.d.ts.map +1 -1
  72. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  73. package/dist/device/Device.d.ts +29 -6
  74. package/dist/device/Device.d.ts.map +1 -1
  75. package/dist/device/DeviceCommands.d.ts +4 -4
  76. package/dist/device/DeviceCommands.d.ts.map +1 -1
  77. package/dist/device/DevicePool.d.ts.map +1 -1
  78. package/dist/events/device.d.ts +4 -0
  79. package/dist/events/device.d.ts.map +1 -1
  80. package/dist/events/logBlockEvent.d.ts.map +1 -1
  81. package/dist/events/ui-promise.d.ts +4 -2
  82. package/dist/events/ui-promise.d.ts.map +1 -1
  83. package/dist/events/ui-request.d.ts +31 -2
  84. package/dist/events/ui-request.d.ts.map +1 -1
  85. package/dist/events/ui-response.d.ts +10 -2
  86. package/dist/events/ui-response.d.ts.map +1 -1
  87. package/dist/index.d.ts +214 -231
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +2616 -3527
  90. package/dist/inject.d.ts +6 -1
  91. package/dist/inject.d.ts.map +1 -1
  92. package/dist/lowLevelInject.d.ts.map +1 -1
  93. package/dist/protocols/protocol-v2/features.d.ts +0 -4
  94. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  95. package/dist/protocols/protocol-v2/resources.d.ts +30 -0
  96. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -0
  97. package/dist/protocols/protocol-v2/uiInteraction.d.ts +7 -4
  98. package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -1
  99. package/dist/protocols/protocol-v2/unlockPolicy.d.ts +1 -3
  100. package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -1
  101. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +22 -0
  102. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -0
  103. package/dist/protocols/protocol-v2/walletSession.d.ts +6 -1
  104. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  105. package/dist/topLevelInject.d.ts.map +1 -1
  106. package/dist/types/api/checkAllFirmwareRelease.d.ts +2 -4
  107. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  108. package/dist/types/api/detectDeviceConnectProtocol.d.ts +4 -0
  109. package/dist/types/api/detectDeviceConnectProtocol.d.ts.map +1 -0
  110. package/dist/types/api/deviceUnlock.d.ts +5 -1
  111. package/dist/types/api/deviceUnlock.d.ts.map +1 -1
  112. package/dist/types/api/deviceUpdateBootloader.d.ts +0 -6
  113. package/dist/types/api/deviceUpdateBootloader.d.ts.map +1 -1
  114. package/dist/types/api/export.d.ts +2 -4
  115. package/dist/types/api/export.d.ts.map +1 -1
  116. package/dist/types/api/firmwareUpdate.d.ts +2 -70
  117. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  118. package/dist/types/api/index.d.ts +7 -10
  119. package/dist/types/api/index.d.ts.map +1 -1
  120. package/dist/types/api/protocolV2.d.ts +3 -0
  121. package/dist/types/api/protocolV2.d.ts.map +1 -1
  122. package/dist/types/device.d.ts.map +1 -1
  123. package/dist/types/params.d.ts +1 -0
  124. package/dist/types/params.d.ts.map +1 -1
  125. package/dist/types/settings.d.ts +22 -13
  126. package/dist/types/settings.d.ts.map +1 -1
  127. package/dist/utils/deviceFeaturesCompat.d.ts +0 -1
  128. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  129. package/dist/utils/deviceFeaturesUtils.d.ts +4 -0
  130. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  131. package/dist/utils/homescreen.d.ts +4 -0
  132. package/dist/utils/homescreen.d.ts.map +1 -1
  133. package/dist/utils/index.d.ts +1 -1
  134. package/dist/utils/index.d.ts.map +1 -1
  135. package/dist/utils/patch.d.ts +1 -1
  136. package/dist/utils/patch.d.ts.map +1 -1
  137. package/dist/utils/pro2Nft.d.ts +31 -0
  138. package/dist/utils/pro2Nft.d.ts.map +1 -0
  139. package/dist/utils/pro2Wallpaper.d.ts +10 -0
  140. package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
  141. package/dist/utils/uiProgressThrottle.d.ts +3 -0
  142. package/dist/utils/uiProgressThrottle.d.ts.map +1 -0
  143. package/package.json +4 -4
  144. package/src/api/BaseMethod.ts +8 -10
  145. package/src/api/CheckAllFirmwareRelease.ts +54 -57
  146. package/src/api/DetectDeviceConnectProtocol.ts +18 -0
  147. package/src/api/FirmwareUpdate.ts +7 -15
  148. package/src/api/FirmwareUpdateV2.ts +126 -316
  149. package/src/api/FirmwareUpdateV3.ts +63 -265
  150. package/src/api/FirmwareUpdateV4.ts +569 -965
  151. package/src/api/GetPassphraseState.ts +7 -1
  152. package/src/api/OpenWalletSession.ts +70 -17
  153. package/src/api/SearchDevices.ts +3 -1
  154. package/src/api/allnetwork/AllNetworkGetAddress.ts +21 -21
  155. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +34 -11
  156. package/src/api/device/DeviceUnlock.ts +8 -3
  157. package/src/api/device/DeviceUpdateBootloader.ts +6 -122
  158. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +16 -83
  159. package/src/api/firmware/updateBootloader.ts +4 -19
  160. package/src/api/firmware/uploadFirmware.ts +39 -144
  161. package/src/api/helpers/protocolV2FileWrite.ts +11 -4
  162. package/src/api/index.ts +2 -0
  163. package/src/api/kaspa/KaspaSignTransaction.ts +1 -12
  164. package/src/api/protocol-v2/DeviceUploadNft.ts +189 -0
  165. package/src/api/protocol-v2/ProtocolInfoRequest.ts +3 -1
  166. package/src/api/protocol-v2/helpers.ts +1 -0
  167. package/src/core/deviceEventRegistration.ts +4 -0
  168. package/src/core/index.ts +167 -83
  169. package/src/core/uiPromiseRegistry.ts +41 -0
  170. package/src/data/messages/messages-protocol-v2.json +25 -0
  171. package/src/data-manager/DataManager.ts +71 -39
  172. package/src/data-manager/connectSettings.ts +0 -11
  173. package/src/device/Device.ts +259 -43
  174. package/src/device/DeviceCommands.ts +7 -1
  175. package/src/device/DevicePool.ts +7 -2
  176. package/src/events/device.ts +4 -0
  177. package/src/events/logBlockEvent.ts +1 -0
  178. package/src/events/ui-promise.ts +4 -2
  179. package/src/events/ui-request.ts +36 -2
  180. package/src/events/ui-response.ts +12 -2
  181. package/src/index.ts +0 -5
  182. package/src/inject.ts +60 -24
  183. package/src/lowLevelInject.ts +7 -8
  184. package/src/protocols/protocol-v2/features.ts +10 -7
  185. package/src/protocols/protocol-v2/resources.ts +359 -0
  186. package/src/protocols/protocol-v2/uiInteraction.ts +46 -6
  187. package/src/protocols/protocol-v2/unlockPolicy.ts +1 -105
  188. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +118 -0
  189. package/src/protocols/protocol-v2/walletSession.ts +214 -36
  190. package/src/topLevelInject.ts +7 -8
  191. package/src/types/api/checkAllFirmwareRelease.ts +2 -4
  192. package/src/types/api/detectDeviceConnectProtocol.ts +7 -0
  193. package/src/types/api/deviceUnlock.ts +12 -1
  194. package/src/types/api/deviceUpdateBootloader.ts +0 -6
  195. package/src/types/api/export.ts +1 -18
  196. package/src/types/api/firmwareUpdate.ts +3 -76
  197. package/src/types/api/index.ts +13 -14
  198. package/src/types/api/protocolV2.ts +13 -0
  199. package/src/types/device.ts +3 -0
  200. package/src/types/params.ts +7 -1
  201. package/src/types/settings.ts +42 -13
  202. package/src/utils/deviceFeaturesCompat.ts +0 -6
  203. package/src/utils/deviceFeaturesUtils.ts +8 -0
  204. package/src/utils/homescreen.ts +32 -6
  205. package/src/utils/index.ts +6 -1
  206. package/src/utils/pro2Nft.ts +142 -0
  207. package/src/utils/pro2Wallpaper.ts +35 -8
  208. package/src/utils/uiProgressThrottle.ts +63 -0
  209. package/__tests__/device-initialize-timeout.test.ts +0 -56
  210. package/__tests__/firmware-update/check-all-firmware-release.test.ts +0 -175
  211. package/__tests__/firmware-update/firmware-artifact-source.test.ts +0 -119
  212. package/__tests__/firmware-update/firmware-host-binding.test.ts +0 -70
  213. package/__tests__/firmware-update/firmware-preparation-error.test.ts +0 -27
  214. package/__tests__/firmware-update/firmware-update-bootloader-poll.test.ts +0 -200
  215. package/__tests__/firmware-update/firmware-update-capabilities.test.ts +0 -13
  216. package/__tests__/firmware-update/firmware-update-plan-bootloader-identity.test.ts +0 -294
  217. package/__tests__/firmware-update/firmware-update-plan.test.ts +0 -593
  218. package/__tests__/firmware-update/firmware-update-prepared-plan.test.ts +0 -213
  219. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +0 -58
  220. package/__tests__/firmware-update/firmware-upload-source.test.ts +0 -70
  221. package/dist/api/firmware/FirmwareArtifactSource.d.ts +0 -25
  222. package/dist/api/firmware/FirmwareArtifactSource.d.ts.map +0 -1
  223. package/dist/api/firmware/FirmwareHostBinding.d.ts +0 -6
  224. package/dist/api/firmware/FirmwareHostBinding.d.ts.map +0 -1
  225. package/dist/api/firmware/FirmwarePreparationError.d.ts +0 -3
  226. package/dist/api/firmware/FirmwarePreparationError.d.ts.map +0 -1
  227. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts +0 -3
  228. package/dist/api/firmware/FirmwareUpdateCapabilities.d.ts.map +0 -1
  229. package/dist/api/firmware/FirmwareUpdatePlan.d.ts +0 -25
  230. package/dist/api/firmware/FirmwareUpdatePlan.d.ts.map +0 -1
  231. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts +0 -32
  232. package/dist/api/firmware/FirmwareUpdatePreparedPlan.d.ts.map +0 -1
  233. package/dist/api/firmware/progressThrottle.d.ts +0 -3
  234. package/dist/api/firmware/progressThrottle.d.ts.map +0 -1
  235. package/dist/protocols/protocol-v2/lockedError.d.ts +0 -2
  236. package/dist/protocols/protocol-v2/lockedError.d.ts.map +0 -1
  237. package/dist/protocols/protocol-v2/unlockRetry.d.ts +0 -10
  238. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +0 -1
  239. package/dist/types/api/firmwareUpdateCapabilities.d.ts +0 -9
  240. package/dist/types/api/firmwareUpdateCapabilities.d.ts.map +0 -1
  241. package/dist/types/api/firmwareUpdatePlan.d.ts +0 -28
  242. package/dist/types/api/firmwareUpdatePlan.d.ts.map +0 -1
  243. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts +0 -42
  244. package/dist/types/api/firmwareUpdatePreparedPlan.d.ts.map +0 -1
  245. package/src/api/firmware/FirmwareArtifactSource.ts +0 -278
  246. package/src/api/firmware/FirmwareHostBinding.ts +0 -100
  247. package/src/api/firmware/FirmwarePreparationError.ts +0 -12
  248. package/src/api/firmware/FirmwareUpdateCapabilities.ts +0 -9
  249. package/src/api/firmware/FirmwareUpdatePlan.ts +0 -772
  250. package/src/api/firmware/FirmwareUpdatePreparedPlan.ts +0 -415
  251. package/src/api/firmware/progressThrottle.ts +0 -44
  252. package/src/protocols/protocol-v2/lockedError.ts +0 -10
  253. package/src/protocols/protocol-v2/unlockRetry.ts +0 -109
  254. package/src/types/api/firmwareUpdateCapabilities.ts +0 -9
  255. package/src/types/api/firmwareUpdatePlan.ts +0 -38
  256. package/src/types/api/firmwareUpdatePreparedPlan.ts +0 -53
@@ -2,7 +2,6 @@ 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,
6
5
  PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE,
7
6
  } from '@onekeyfe/hd-transport';
8
7
  import { sha256 } from '@noble/hashes/sha256';
@@ -19,7 +18,6 @@ import {
19
18
  getLogger,
20
19
  } from '../utils';
21
20
  import { getSysResourceBinary } from './firmware/getBinary';
22
- import { normalizeFirmwarePreparationError } from './firmware/FirmwarePreparationError';
23
21
  import { DataManager } from '../data-manager';
24
22
  import { FirmwareUpdateBaseMethod } from './firmware/FirmwareUpdateBaseMethod';
25
23
  import { DevicePool } from '../device/DevicePool';
@@ -28,41 +26,41 @@ import {
28
26
  ProtocolV2FirmwareTargetType,
29
27
  } from '../protocols/protocol-v2';
30
28
  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';
31
35
  import {
32
36
  getProtocolV2UnknownErrorText,
33
37
  isProtocolV2DeviceDisconnectedError,
34
38
  } 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';
41
39
 
42
- import type {
43
- FirmwareArtifactReference,
44
- FirmwareUpdateV4Params,
45
- FirmwareUpdateV4Target,
46
- } from '../types/api/firmwareUpdate';
40
+ import type { FirmwareUpdateV4Params, FirmwareUpdateV4Target } from '../types/api/firmwareUpdate';
47
41
  import type { EFirmwareType } from '@onekeyfe/hd-shared';
48
42
  import type { ProtocolV2DeviceInfo } from '@onekeyfe/hd-transport';
43
+ import type { PROTO } from '../constants';
49
44
  import type { TypedResponseMessage } from '../device/DeviceCommands';
50
45
  import type {
51
46
  Features,
52
47
  IFirmwareReleaseInfo,
48
+ IProtocolV2BootResources,
53
49
  IProtocolV2FirmwareComponent,
50
+ IProtocolV2Resource,
54
51
  IVersionArray,
55
52
  } from '../types';
56
- import type { FirmwareByteSource } from './firmware/FirmwareArtifactSource';
57
53
 
58
54
  const Log = getLogger(LoggerNames.Method);
59
55
 
60
56
  const SESSION_ERROR = 'session not found';
61
- const PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT = 60 * 1000;
62
- const PROTOCOL_V2_FINAL_RECONNECT_TIMEOUT = 2 * 60 * 1000;
57
+ const PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT = 90 * 1000;
58
+ const PROTOCOL_V2_FINAL_RECONNECT_TIMEOUT = 3 * 60 * 1000;
63
59
  const PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT = 5 * 1000;
60
+ const PROTOCOL_V2_FIRMWARE_STATUS_RESPONSE_TIMEOUT = 15 * 1000;
64
61
  const PROTOCOL_V2_START_UPDATE_TIMEOUT = 3 * 60 * 1000;
65
- const PROTOCOL_V2_INSTALL_TIMEOUT = 5 * 60 * 1000;
62
+ const PROTOCOL_V2_INSTALL_TIMEOUT = 8 * 60 * 1000;
63
+ const PROTOCOL_V2_MISSING_TARGET_STATUS_GRACE_TIMEOUT = 30 * 1000;
66
64
  const PROTOCOL_V2_TARGET_STATUS_PENDING = 0;
67
65
  const PROTOCOL_V2_TARGET_STATUS_IN_PROGRESS = 1;
68
66
  const PROTOCOL_V2_TARGET_STATUS_FINISHED = 2;
@@ -73,12 +71,13 @@ const PROTOCOL_V2_MIN_FILE_CHUNK_SIZE = 64;
73
71
  const PROTOCOL_V2_CONNECT_RETRY_COUNT = 10;
74
72
  const PROTOCOL_V2_CONNECT_POLL_INTERVAL = 500;
75
73
  const PROTOCOL_V2_CONNECT_SINGLE_TIMEOUT = 75 * 1000;
76
- const PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT = 60 * 1000;
74
+ const PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT = 30 * 1000;
77
75
  const PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT = 3;
78
76
  const PROTOCOL_V2_OKPP_HEADER_SIZE = 0x52a0;
79
77
  const PROTOCOL_V2_OKPP_PAYLOAD_HASH_OFFSET = 0x200;
80
78
  const PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET = 0x240;
81
79
  const PROTOCOL_V2_OKPP_HASH_SIZE = 64;
80
+ const PROTOCOL_V2_BOOT_RESOURCES_FILE_NAME = 'boot_resources.crate.okpkg';
82
81
 
83
82
  const getProtocolV2DeviceTransferProgress = (
84
83
  bytesBeforeChunk: number,
@@ -97,6 +96,11 @@ const getProtocolV2DeviceTransferProgress = (
97
96
  return Math.min(Math.max(Math.ceil((bytesAfterChunk / totalBytes) * 100), 1), 99);
98
97
  };
99
98
 
99
+ const formatProtocolV2TransferSpeed = (bytes: number, elapsedMs: number) => {
100
+ const safeElapsedMs = Math.max(elapsedMs, 1);
101
+ return (bytes / 1024 / (safeElapsedMs / 1000)).toFixed(2);
102
+ };
103
+
100
104
  type ProtocolV2FirmwareUpdateStatusTarget = {
101
105
  target_id: number | string;
102
106
  status?: number | string;
@@ -108,18 +112,24 @@ type ProtocolV2FirmwareUpdateStartResponse = TypedResponseMessage<'Success'>;
108
112
 
109
113
  type ProtocolV2TargetBinary = { fileName: string; binary: ArrayBuffer; targetId: number };
110
114
  type ProtocolV2InstallItem = ProtocolV2TargetBinary & {
111
- kind: ProtocolV2RemoteComponentTarget['kind'];
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;
112
126
  };
127
+
113
128
  /** RESC bundle okpkg written independently to devicePath through FileWrite. */
114
129
  type ProtocolV2ResourceBundleBinary = {
115
130
  name: string;
116
131
  binary: ArrayBuffer;
117
132
  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;
123
133
  };
124
134
 
125
135
  type ProtocolV2RemoteComponentBinary = ProtocolV2RemoteComponentTarget & {
@@ -132,35 +142,6 @@ type ProtocolV2RemoteComponentTarget = {
132
142
  kind: 'bootloader' | 'firmware';
133
143
  };
134
144
 
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
-
164
145
  type ProtocolV2OkppHeader = {
165
146
  type: string;
166
147
  version: IVersionArray;
@@ -300,18 +281,6 @@ const normalizeProtocolV2TargetStatus = (
300
281
 
301
282
  const normalizeProtocolV2Hex = (value?: string) => value?.replace(/^0x/i, '').toLowerCase();
302
283
 
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
-
315
284
  const bytesToHex = (bytes: Uint8Array) =>
316
285
  Array.from(bytes)
317
286
  .map(byte => byte.toString(16).padStart(2, '0'))
@@ -404,11 +373,10 @@ export const assertProtocolV2ReconnectIdentity = (
404
373
  expectedSerialNumber?: string,
405
374
  actualSerialNumber?: string
406
375
  ) => {
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.
407
378
  if (!expectedSerialNumber || !actualSerialNumber) {
408
- throw ERRORS.TypedError(
409
- HardwareErrorCode.DeviceNotFound,
410
- 'Protocol V2 reconnect physical identity is unavailable'
411
- );
379
+ return;
412
380
  }
413
381
  if (actualSerialNumber !== expectedSerialNumber) {
414
382
  throw ERRORS.TypedError(
@@ -433,16 +401,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
433
401
  return ['V2'] as const;
434
402
  }
435
403
 
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
-
446
404
  init() {
447
405
  this.allowDeviceMode = [UI_REQUEST.BOOTLOADER, UI_REQUEST.NOT_INITIALIZE];
448
406
  this.requireDeviceMode = [];
@@ -469,6 +427,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
469
427
  { name: 'chunkSize', type: 'number' },
470
428
  { name: 'forcedUpdateRes', type: 'boolean' },
471
429
  { name: 'bootloaderBinary', type: 'buffer' },
430
+ { name: 'bootResourcesBinary', type: 'buffer' },
472
431
  { name: 'romloaderBinary', type: 'buffer' },
473
432
  { name: 'applicationP1Binary', type: 'buffer' },
474
433
  { name: 'applicationP2Binary', type: 'buffer' },
@@ -480,65 +439,14 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
480
439
  { name: 'firmwareType', type: 'string' },
481
440
  { name: 'targetsToUpdate', type: 'array', allowEmpty: true },
482
441
  { name: 'platform', type: 'string' },
483
- { name: 'expectedDeviceId', type: 'string' },
484
442
  { name: 'resourceBundleFiles', type: 'array', allowEmpty: true },
485
443
  ]);
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
- }
536
444
 
537
445
  this.params = {
538
- preparedPlan: payload.preparedPlan,
539
446
  chunkSize: payload.chunkSize,
540
447
  forcedUpdateRes: payload.forcedUpdateRes,
541
448
  bootloaderBinary: payload.bootloaderBinary,
449
+ bootResourcesBinary: payload.bootResourcesBinary,
542
450
  romloaderBinary: payload.romloaderBinary,
543
451
  applicationP1Binary: payload.applicationP1Binary,
544
452
  applicationP2Binary: payload.applicationP2Binary,
@@ -550,25 +458,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
550
458
  resourceBundleFiles: payload.resourceBundleFiles,
551
459
  firmwareType: payload.firmwareType,
552
460
  targetsToUpdate: payload.targetsToUpdate,
553
- expectedTargetVersions: payload.expectedTargetVersions,
554
461
  platform: payload.platform,
555
- expectedDeviceId: payload.expectedDeviceId,
556
- artifactReader: hostBinding?.artifactReader ?? payload.artifactReader,
557
- componentArtifacts: payload.componentArtifacts,
558
- resourceBundleArtifacts: payload.resourceBundleArtifacts,
559
462
  };
560
463
  }
561
464
 
562
- private getProtocolV2FirmwareChunkSize(direction: 'read' | 'write' = 'write') {
465
+ private getProtocolV2FirmwareChunkSize() {
563
466
  const payloadChunkSize = Number(this.params?.chunkSize);
564
467
  const env = DataManager.getSettings('env');
565
468
  const isBle = this.params?.platform === 'native' || (env && DataManager.isBleConnect(env));
566
469
  let maxChunkSize = PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE;
567
470
  if (isBle) {
568
- maxChunkSize =
569
- direction === 'read'
570
- ? PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE
571
- : PROTOCOL_V2_BLE_FILE_CHUNK_SIZE;
471
+ maxChunkSize = PROTOCOL_V2_BLE_FILE_CHUNK_SIZE;
572
472
  }
573
473
  if (!Number.isFinite(payloadChunkSize) || payloadChunkSize <= 0) {
574
474
  return maxChunkSize;
@@ -589,36 +489,29 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
589
489
  const deviceFeatures = await this.getProtocolV2DeviceFeatures();
590
490
  const deviceFirmwareType = getFirmwareType(deviceFeatures);
591
491
  const firmwareType = this.params.firmwareType ?? deviceFirmwareType;
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
- }
492
+ const needsRemoteResources =
493
+ !this.params.resourceBundleFiles?.length &&
494
+ !!this.params.targetsToUpdate?.includes('resource');
600
495
 
601
496
  let fwBinaryMap: ProtocolV2TargetBinary[] = [];
602
497
  let bootloaderBinary: ArrayBuffer | null = null;
498
+ let bootResourcesInstallItem: ProtocolV2InstallItem | undefined;
603
499
  let installItems: ProtocolV2InstallItem[] | undefined;
604
500
  let resourceBundles: ProtocolV2ResourceBundleBinary[] | undefined;
605
501
  try {
606
502
  this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
503
+ resourceBundles = this.prepareExplicitProtocolV2ResourceBundles();
607
504
  fwBinaryMap = this.collectExplicitTargetBinaries();
608
505
  bootloaderBinary = this.prepareBootloaderBinary();
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
- }
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) {
622
515
  const remoteBinaries = await this.prepareRemoteProtocolV2Binaries(
623
516
  firmwareType,
624
517
  deviceFeatures
@@ -627,308 +520,72 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
627
520
  fwBinaryMap = remoteBinaries.fwBinaryMap;
628
521
  installItems = remoteBinaries.installItems;
629
522
  }
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);
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
+ }
638
533
  } catch (err) {
639
- throw normalizeFirmwarePreparationError(err);
534
+ if (err instanceof HardwareError && err.errorCode === HardwareErrorCode.NetworkError) {
535
+ throw err;
536
+ }
537
+ throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateDownloadFailed, err.message ?? err);
640
538
  }
641
539
 
642
- if (!bootloaderBinary && fwBinaryMap.length === 0 && !resourceBundles?.length) {
540
+ if (
541
+ !bootloaderBinary &&
542
+ fwBinaryMap.length === 0 &&
543
+ !installItems?.length &&
544
+ !resourceBundles?.length &&
545
+ !needsRemoteResources
546
+ ) {
643
547
  throw ERRORS.TypedError(
644
548
  HardwareErrorCode.FirmwareUpdateDownloadFailed,
645
549
  'No firmware to update'
646
550
  );
647
551
  }
648
552
 
649
- return this.executeProtocolV2Update({
650
- fwBinaryMap,
651
- bootloaderBinary,
652
- ...(installItems ? { installItems } : undefined),
653
- ...(resourceBundles?.length ? { resourceBundles } : undefined),
654
- });
655
- }
656
-
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
- }
674
-
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
- }
686
-
687
- private async closeProtocolV2PreparedSources() {
688
- const sources = this.protocolV2PreparedSources.splice(0);
689
- await Promise.all(sources.map(source => source.close().catch(() => undefined)));
690
- }
553
+ const enteredBootloader = await this.enterProtocolV2BootloaderMode();
691
554
 
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,
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
+ );
770
568
  }
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
- );
569
+ }
570
+ throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateDownloadFailed, err.message ?? err);
806
571
  }
807
- this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
808
-
809
- return await this.executeProtocolV2SourceUpdate({
810
- installSources,
811
- resourceSources,
812
- });
813
- } finally {
814
- await this.closeProtocolV2PreparedSources();
815
572
  }
816
- }
817
573
 
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
- }
574
+ await this.executeProtocolV2Update({
575
+ fwBinaryMap,
576
+ bootloaderBinary,
577
+ ...(installItems ? { installItems } : undefined),
578
+ ...(resourceBundles?.length ? { resourceBundles } : undefined),
579
+ });
843
580
 
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
- }
581
+ this.postTipMessage(FirmwareUpdateTipMessage.SwitchFirmwareReconnectDevice);
582
+ await this.exitProtocolV2BootloaderToNormal();
857
583
 
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
- }
584
+ const versions = await this.waitForProtocolV2FinalFeatures();
585
+ this.postTipMessage(FirmwareUpdateTipMessage.FirmwareUpdateCompleted);
586
+ DevicePool.resetState();
911
587
 
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);
588
+ return versions;
932
589
  }
933
590
 
934
591
  private async getProtocolV2DeviceFeatures(): Promise<Features> {
@@ -954,26 +611,23 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
954
611
  );
955
612
  }
956
613
 
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
+
957
620
  private async requestProtocolV2PhysicalIdentity() {
958
621
  return requestProtocolV2DeviceInfo({
959
622
  commands: this.device.getCommands(),
960
- timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
623
+ timeoutMs: this.getProtocolV2DeviceInfoTimeout(),
961
624
  });
962
625
  }
963
626
 
964
627
  private async captureProtocolV2PhysicalIdentity() {
965
628
  const deviceInfo = await this.requestProtocolV2PhysicalIdentity();
966
629
  const serialNumber = this.getProtocolV2SerialNumber(deviceInfo);
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
- }
630
+ assertProtocolV2ReconnectIdentity(serialNumber, serialNumber);
977
631
  this.protocolV2ExpectedSerialNumber = serialNumber;
978
632
  }
979
633
 
@@ -991,6 +645,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
991
645
  return (
992
646
  !!this.params.resourceBundleFiles?.length ||
993
647
  !!this.params.bootloaderBinary ||
648
+ !!this.params.bootResourcesBinary ||
994
649
  fwBinaryMap.length > 0
995
650
  );
996
651
  }
@@ -1052,57 +707,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1052
707
  return target;
1053
708
  }
1054
709
 
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
-
1106
710
  private async downloadRemoteProtocolV2Component(
1107
711
  key: string,
1108
712
  component: IProtocolV2FirmwareComponent
@@ -1191,155 +795,159 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1191
795
  };
1192
796
  }
1193
797
 
1194
- // ============================================================
1195
- // Incremental RESC bundle sync through FileRead and FilesystemFileWrite
1196
- // ============================================================
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
+ }
1197
820
 
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
- });
821
+ private async prepareProtocolV2BootResources(): Promise<ProtocolV2InstallItem | undefined> {
822
+ let binary = this.params.bootResourcesBinary;
823
+ let resource: IProtocolV2BootResources | undefined;
824
+
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));
1223
835
  }
1224
836
 
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
+ > {
1225
865
  if (!this.params.targetsToUpdate?.includes('resource')) {
1226
866
  return undefined;
1227
867
  }
1228
868
 
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
- }
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));
1301
895
  }
1302
896
  return bundles;
1303
897
  }
1304
898
 
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;
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
+ }
1316
913
 
1317
- try {
1318
- const header = await this.readProtocolV2DeviceFileHeader(bundle.devicePath);
1319
- if (!header) return false;
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();
1320
920
 
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;
921
+ // Write directly to the device through FileWrite.
922
+ let totalSize = 0;
923
+ for (const b of bundles) totalSize += b.binary.byteLength;
924
+
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
+ });
1337
937
  }
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 };
1338
946
  }
1339
947
 
1340
948
  private isProtocolV2BootloaderMode() {
1341
949
  if (typeof this.device.isBootloader === 'function') {
1342
- return this.device.isBootloader();
950
+ return !!this.device.isBootloader();
1343
951
  }
1344
952
  return (
1345
953
  this.device.features?.mode === 'bootloader' ||
@@ -1362,12 +970,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1362
970
  // It rejects DeviceRebootType.Bootloader, so reuse the current connection.
1363
971
  if (this.isProtocolV2RomloaderMode()) {
1364
972
  Log.debug('Protocol V2 device is in romloader mode; start firmware update directly');
1365
- this.protocolV2ExecutionInLoader = true;
1366
973
  return false;
1367
974
  }
1368
975
  if (this.isProtocolV2BootloaderMode()) {
1369
976
  Log.debug('Protocol V2 device is already in bootloader mode, skip reboot');
1370
- this.protocolV2ExecutionInLoader = true;
1371
977
  return false;
1372
978
  }
1373
979
 
@@ -1377,7 +983,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1377
983
  this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
1378
984
  await wait(1000);
1379
985
  await this.waitForProtocolV2BootloaderMode();
1380
- this.protocolV2ExecutionInLoader = true;
1381
986
  return true;
1382
987
  } catch (error) {
1383
988
  if (error instanceof HardwareError) {
@@ -1394,13 +999,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1394
999
  ) {
1395
1000
  const startTime = Date.now();
1396
1001
  let lastError: unknown;
1002
+ let shouldReconnect = true;
1397
1003
 
1398
1004
  while (Date.now() - startTime < timeout) {
1399
1005
  try {
1400
- await this.reconnectProtocolV2Device();
1006
+ if (shouldReconnect) {
1007
+ await this.reconnectProtocolV2Device();
1008
+ shouldReconnect = false;
1009
+ }
1401
1010
  const deviceInfo = await requestProtocolV2DeviceInfo({
1402
1011
  commands: this.device.getCommands(),
1403
- timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
1012
+ timeoutMs: this.getProtocolV2DeviceInfoTimeout(),
1404
1013
  });
1405
1014
  this.assertProtocolV2DeviceInfoIdentity(deviceInfo);
1406
1015
  const features = await this.device.probeProtocolV2RuntimeState(
@@ -1415,6 +1024,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1415
1024
  if (this.isProtocolV2ReconnectIdentityError(error)) {
1416
1025
  throw error;
1417
1026
  }
1027
+ shouldReconnect = true;
1418
1028
  lastError = error;
1419
1029
  Log.log('Protocol V2 bootloader mode not ready, polling reconnect: ', error);
1420
1030
  }
@@ -1467,98 +1077,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1467
1077
  return entries;
1468
1078
  }
1469
1079
 
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
-
1562
1080
  private async executeProtocolV2Update({
1563
1081
  fwBinaryMap,
1564
1082
  bootloaderBinary,
@@ -1570,158 +1088,104 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1570
1088
  installItems?: ProtocolV2InstallItem[];
1571
1089
  resourceBundles?: ProtocolV2ResourceBundleBinary[];
1572
1090
  }) {
1573
- const memoryInstallItems =
1091
+ const orderedInstallItems =
1574
1092
  installItems ??
1575
1093
  this.buildProtocolV2InstallItems({
1576
- fwBinaryMap: fwBinaryMap ?? [],
1577
1094
  bootloaderBinary: bootloaderBinary ?? null,
1095
+ fwBinaryMap: fwBinaryMap ?? [],
1578
1096
  });
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
- }))
1597
- );
1598
- return await this.executeProtocolV2Phases({
1599
- installSources,
1600
- resourceSources,
1601
- });
1602
- } finally {
1603
- await this.closeProtocolV2PreparedSources();
1604
- }
1605
- }
1606
1097
 
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;
1619
- }
1620
- }
1098
+ let firmwareSize = 0;
1099
+ for (const item of orderedInstallItems) firmwareSize += item.binary.byteLength;
1621
1100
 
1622
1101
  this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
1102
+
1623
1103
  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);
1104
+ let totalSize = firmwareSize;
1105
+ if (resourceBundles?.length) {
1106
+ const resourceTransfer = await this.syncProtocolV2ResourceBundles(
1107
+ resourceBundles,
1108
+ firmwareSize
1109
+ );
1110
+ processedSize = resourceTransfer.processedSize;
1111
+ totalSize = resourceTransfer.totalSize;
1632
1112
  }
1633
1113
 
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
- });
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');
1119
+ }
1120
+ return;
1648
1121
  }
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
+ );
1649
1132
 
1650
- if (totalSize > 0) {
1651
- this.postProgressMessage(100, 'transferData');
1652
- }
1653
- if (stagedInstallTargets.length === 0) {
1654
- return;
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
+
1151
+ stagedInstallTargets.push({
1152
+ ...item,
1153
+ path: filePath,
1154
+ });
1155
+ }
1156
+
1157
+ if (totalSize > 0) {
1158
+ this.postProgressMessage(100, 'transferData');
1159
+ }
1160
+
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;
1655
1175
  }
1656
1176
 
1657
1177
  this.postTipMessage(FirmwareUpdateTipMessage.ConfirmOnDevice);
1658
- const targets = stagedInstallTargets.map(item => ({
1178
+
1179
+ const allTargets = stagedInstallTargets.map(item => ({
1659
1180
  target_id: item.targetId,
1660
1181
  path: item.path,
1661
1182
  }));
1662
- await this.protocolV2StartFirmwareUpdate({ targets });
1663
- await this.waitForProtocolV2FirmwareUpdateComplete(targets);
1664
- }
1665
-
1666
- private async protocolV2SourceUpdateProcess({
1667
- source,
1668
- filePath,
1669
- processedSize,
1670
- totalSize,
1671
- }: {
1672
- source: FirmwareByteSource;
1673
- filePath: string;
1674
- processedSize: number;
1675
- totalSize: number;
1676
- }) {
1677
- let lastError: unknown;
1678
- for (let attempt = 1; attempt <= PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT; attempt += 1) {
1679
- try {
1680
- await writeFirmwareByteSource({
1681
- source,
1682
- chunkSize: this.getProtocolV2FirmwareChunkSize(),
1683
- write: async ({ data, sourceOffset, length, first }) => {
1684
- const chunkEnd = sourceOffset + length;
1685
- const progress = getProtocolV2DeviceTransferProgress(
1686
- processedSize + sourceOffset,
1687
- processedSize + chunkEnd,
1688
- totalSize
1689
- );
1690
- const response = await this.fileWriteChunk(
1691
- filePath,
1692
- source.size,
1693
- sourceOffset,
1694
- data,
1695
- first,
1696
- progress
1697
- );
1698
- const rawProcessedByte = response.message.processed_byte;
1699
- const nextOffset = rawProcessedByte === undefined ? chunkEnd : Number(rawProcessedByte);
1700
- if (!Number.isFinite(nextOffset) || nextOffset !== chunkEnd) {
1701
- throw ERRORS.TypedError(
1702
- HardwareErrorCode.EmmcFileWriteFirmwareError,
1703
- `invalid processed_byte ${rawProcessedByte} for offset ${sourceOffset}`
1704
- );
1705
- }
1706
- this.postProgressMessage(progress, 'transferData');
1707
- return length;
1708
- },
1709
- });
1710
- return processedSize + source.size;
1711
- } catch (error) {
1712
- lastError = error;
1713
- if (attempt < PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT) {
1714
- await this.recoverProtocolV2FileTransfer();
1715
- }
1716
- }
1717
- }
1718
- throw ERRORS.TypedError(
1719
- HardwareErrorCode.EmmcFileWriteFirmwareError,
1720
- `transfer data error: ${getProtocolV2UnknownErrorText(lastError)}`
1721
- );
1183
+ Log.log(`[FirmwareUpdateV4] DeviceFirmwareUpdateRequest targets=${JSON.stringify(allTargets)}`);
1184
+ await this.protocolV2StartFirmwareUpdate({ targets: allTargets });
1185
+ await this.waitForProtocolV2FirmwareUpdateComplete(allTargets);
1722
1186
  }
1723
1187
 
1724
- private getProtocolV2InstallItemStagingPath(item: Pick<ProtocolV2InstallItem, 'fileName'>) {
1188
+ private getProtocolV2InstallItemStagingPath(item: ProtocolV2InstallItem) {
1725
1189
  return `${PROTOCOL_V2_FIRMWARE_STAGING_VOLUME}${item.fileName}`;
1726
1190
  }
1727
1191
 
@@ -1740,8 +1204,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1740
1204
 
1741
1205
  private assertProtocolV2TargetStatus(
1742
1206
  statusTargets: ProtocolV2FirmwareUpdateStatusTarget[],
1743
- expectedTargetIds: Set<number>,
1744
- expectedPaths = new Map<number, string>()
1207
+ expectedTargetIds: Set<number>
1745
1208
  ) {
1746
1209
  Log.log(
1747
1210
  `[FirmwareUpdateV4] DeviceFirmwareUpdateStatus records=${JSON.stringify(statusTargets)}`
@@ -1772,54 +1235,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1772
1235
  const matchingTargets = statusTargets.filter(target =>
1773
1236
  expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1)
1774
1237
  );
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
- );
1793
1238
  const completedTargetIds = new Set<number>();
1794
- completedTargets.forEach(target => {
1239
+ matchingTargets.forEach(target => {
1795
1240
  const targetId = normalizeProtocolV2TargetId(target.target_id);
1796
- if (targetId !== undefined) {
1241
+ if (targetId !== undefined && isProtocolV2TargetStatusFinished(target.status)) {
1797
1242
  completedTargetIds.add(targetId);
1798
1243
  }
1799
1244
  });
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
- }
1245
+ const allExpectedTargetsCompleted =
1246
+ expectedTargetIds.size > 0 &&
1247
+ Array.from(expectedTargetIds).every(targetId => completedTargetIds.has(targetId));
1248
+ if (allExpectedTargetsCompleted) {
1823
1249
  this.postProgressMessage(100, 'installingFirmware');
1824
1250
  return true;
1825
1251
  }
@@ -1840,19 +1266,43 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1840
1266
  return false;
1841
1267
  }
1842
1268
 
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
+
1843
1283
  private async waitForProtocolV2FirmwareUpdateComplete(
1844
1284
  targets: Array<{ target_id: number; path: string }>
1845
1285
  ) {
1846
- this.protocolV2FinalStatusVerified = false;
1847
1286
  const expectedTargetIds = new Set(targets.map(target => target.target_id));
1848
- const expectedPaths = new Map(targets.map(target => [target.target_id, target.path]));
1849
1287
  const startTime = Date.now();
1850
1288
  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
+ };
1851
1297
 
1852
1298
  while (Date.now() - startTime < PROTOCOL_V2_INSTALL_TIMEOUT) {
1853
1299
  try {
1854
- await this.reconnectProtocolV2Device();
1855
- const deviceInfo = await this.verifyProtocolV2ReconnectIdentity();
1300
+ if (shouldReconnect) {
1301
+ await this.reconnectProtocolV2Device();
1302
+ deviceInfo = await this.verifyProtocolV2ReconnectIdentity();
1303
+ shouldReconnect = false;
1304
+ }
1305
+ const currentDeviceInfo = deviceInfo;
1856
1306
  try {
1857
1307
  const statusResponse = await this.device.getCommands().typedCall(
1858
1308
  'DeviceFirmwareUpdateStatusGet',
@@ -1864,19 +1314,57 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1864
1314
  path: true,
1865
1315
  },
1866
1316
  },
1867
- { timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT }
1317
+ { timeoutMs: PROTOCOL_V2_FIRMWARE_STATUS_RESPONSE_TIMEOUT }
1868
1318
  );
1319
+ const statusTargets = (statusResponse.message.records ??
1320
+ []) as ProtocolV2FirmwareUpdateStatusTarget[];
1321
+ if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds)) {
1322
+ return;
1323
+ }
1324
+
1869
1325
  if (
1870
- this.assertProtocolV2TargetStatus(
1871
- (statusResponse.message.records ?? []) as ProtocolV2FirmwareUpdateStatusTarget[],
1872
- expectedTargetIds,
1873
- expectedPaths
1874
- )
1326
+ statusTargets.length === 0 &&
1327
+ currentDeviceInfo &&
1328
+ (await this.probeProtocolV2NormalMode(currentDeviceInfo))
1875
1329
  ) {
1876
- this.protocolV2FinalStatusVerified = true;
1330
+ Log.log(
1331
+ '[FirmwareUpdateV4] empty firmware status after confirmed App reboot; update complete'
1332
+ );
1333
+ this.postProgressMessage(100, 'installingFirmware');
1877
1334
  return;
1878
1335
  }
1879
- lastError = new Error('Protocol V2 firmware targets are still installing');
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
+ }
1880
1368
  } catch (error) {
1881
1369
  if (
1882
1370
  error instanceof HardwareError &&
@@ -1884,19 +1372,31 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1884
1372
  ) {
1885
1373
  throw error;
1886
1374
  }
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();
1887
1378
  // App firmware does not register DeviceFirmwareUpdateStatusGet. Treat the
1888
1379
  // missing endpoint as completion only after the runtime probe confirms App mode.
1889
1380
  if (isProtocolV2FirmwareStatusEndpointUnavailable(error)) {
1890
- if (await this.probeProtocolV2NormalMode(deviceInfo)) {
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)) {
1891
1388
  Log.log(
1892
1389
  '[FirmwareUpdateV4] firmware status endpoint unavailable after confirmed App reboot'
1893
1390
  );
1391
+ this.postProgressMessage(100, 'installingFirmware');
1894
1392
  return;
1895
1393
  }
1896
1394
  lastError = new Error(
1897
1395
  'Protocol V2 firmware status endpoint is unavailable while the device remains in loader mode'
1898
1396
  );
1899
1397
  } else {
1398
+ shouldReconnect = true;
1399
+ deviceInfo = undefined;
1900
1400
  lastError = error;
1901
1401
  Log.log(
1902
1402
  '[FirmwareUpdateV4] DeviceFirmwareUpdateStatusGet unavailable during install: ',
@@ -1912,12 +1412,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1912
1412
  if (error instanceof HardwareError && error.errorCode === HardwareErrorCode.FirmwareError) {
1913
1413
  throw error;
1914
1414
  }
1915
- if (
1916
- error instanceof HardwareError &&
1917
- error.params?.firmwareUpdateCode === 'FirmwareInstallStatusUnavailable'
1918
- ) {
1919
- throw error;
1920
- }
1415
+ shouldReconnect = true;
1416
+ deviceInfo = undefined;
1417
+ resetMissingTargetStatusGrace();
1921
1418
  Log.log('Protocol V2 firmware install device readiness probe failed: ', error);
1922
1419
  }
1923
1420
  await wait(1000);
@@ -1925,17 +1422,24 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1925
1422
 
1926
1423
  throw ERRORS.TypedError(
1927
1424
  HardwareErrorCode.RuntimeError,
1928
- `Protocol V2 firmware update status timeout: ${this.normalizeErrorMessage(lastError)}`
1425
+ `Protocol V2 firmware update status not complete within ${
1426
+ PROTOCOL_V2_INSTALL_TIMEOUT / 1000
1427
+ }s: ${this.normalizeErrorMessage(lastError)}`
1929
1428
  );
1930
1429
  }
1931
1430
 
1932
1431
  private async exitProtocolV2BootloaderToNormal() {
1933
1432
  await this.reconnectProtocolV2Device();
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.
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
+ }
1937
1442
  await this.protocolV2Reboot(DeviceRebootType.Normal);
1938
- this.protocolV2ExecutionInLoader = false;
1939
1443
  }
1940
1444
 
1941
1445
  private async probeProtocolV2NormalMode(deviceInfo: ProtocolV2DeviceInfo) {
@@ -1951,10 +1455,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1951
1455
  PROTOCOL_V2_FINAL_RECONNECT_TIMEOUT
1952
1456
  );
1953
1457
 
1458
+ return this.getProtocolV2VersionResult(features);
1459
+ }
1460
+
1461
+ private getProtocolV2VersionResult(features: Features) {
1954
1462
  const bootloaderVersion = getDeviceBootloaderVersion(features).join('.');
1955
1463
  const bleVersion = getDeviceBLEFirmwareVersion(features).join('.');
1956
1464
  const firmwareVersion = getDeviceFirmwareVersion(features).join('.');
1957
- this.protocolV2LatestFinalFeatures = features;
1958
1465
  if (firmwareVersion === '0.0.0') {
1959
1466
  Log.warn(
1960
1467
  'Protocol V2 firmware update finished but app firmware version is still 0.0.0. This is allowed for Pro2 debug BLE-only update flows.'
@@ -1968,27 +1475,20 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1968
1475
  };
1969
1476
  }
1970
1477
 
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
-
1982
1478
  private async waitForProtocolV2ReconnectAndFeatures(timeout: number) {
1983
1479
  const startTime = Date.now();
1984
1480
  let lastError: unknown;
1481
+ let shouldReconnect = true;
1985
1482
 
1986
1483
  while (Date.now() - startTime < timeout) {
1987
1484
  try {
1988
- await this.reconnectProtocolV2Device();
1485
+ if (shouldReconnect) {
1486
+ await this.reconnectProtocolV2Device();
1487
+ shouldReconnect = false;
1488
+ }
1989
1489
  const deviceInfo = await requestProtocolV2DeviceInfo({
1990
1490
  commands: this.device.getCommands(),
1991
- timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
1491
+ timeoutMs: this.getProtocolV2DeviceInfoTimeout(),
1992
1492
  // Completion needs target versions only; keep scope aligned with the request.
1993
1493
  request: PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST,
1994
1494
  });
@@ -1997,21 +1497,22 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1997
1497
  deviceInfo,
1998
1498
  PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT
1999
1499
  );
2000
- if (features.mode !== 'normal' || features.bootloaderMode) {
2001
- throw ERRORS.TypedError(
2002
- HardwareErrorCode.DeviceNotFound,
2003
- 'Protocol V2 device is still in bootloader mode'
2004
- );
1500
+ if (features.mode === 'normal' && !features.bootloaderMode) {
1501
+ return features;
2005
1502
  }
2006
- return features;
1503
+ lastError = ERRORS.TypedError(
1504
+ HardwareErrorCode.DeviceNotFound,
1505
+ 'Protocol V2 device is still in bootloader mode'
1506
+ );
2007
1507
  } catch (error) {
2008
1508
  if (this.isProtocolV2ReconnectIdentityError(error)) {
2009
1509
  throw error;
2010
1510
  }
1511
+ shouldReconnect = true;
2011
1512
  lastError = error;
2012
1513
  Log.log('Protocol V2 normal mode not ready, polling Ping: ', error);
2013
- await wait(1000);
2014
1514
  }
1515
+ await wait(1000);
2015
1516
  }
2016
1517
 
2017
1518
  throw ERRORS.TypedError(
@@ -2048,20 +1549,33 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2048
1549
  return;
2049
1550
  }
2050
1551
 
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.
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.
2053
1554
  const { deviceList } = await DevicePool.getDevices(devicesDescriptor, undefined, {
2054
1555
  connectProtocol: PROTOCOL_V2_CONNECT_PROTOCOL,
2055
1556
  });
2056
- if (deviceList.length !== 1) {
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) {
2057
1571
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
2058
1572
  }
2059
1573
 
2060
1574
  Log.debug(
2061
- 'Protocol V2 firmware reconnect using single enumerated device:',
2062
- deviceList[0].getConnectId()
1575
+ 'Protocol V2 firmware reconnect using matched device:',
1576
+ reconnectDevice.getConnectId()
2063
1577
  );
2064
- this.device.updateFromCache(deviceList[0]);
1578
+ this.device.updateFromCache(reconnectDevice);
2065
1579
  await this.ensureProtocolV2DeviceAcquired();
2066
1580
  this.device.commands.disposed = false;
2067
1581
  this.device.getCommands().mainId = this.device.mainId ?? '';
@@ -2085,6 +1599,96 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2085
1599
  return this.normalizeErrorMessage(error).includes('Protocol V2 reconnect physical identity');
2086
1600
  }
2087
1601
 
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
+
2088
1692
  private async fileWriteChunk(
2089
1693
  filePath: string,
2090
1694
  totalFileSize: number,