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

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