@onekeyfe/hd-core 1.2.0-alpha.67 → 1.2.0-alpha.68

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 (254) hide show
  1. package/__tests__/AllNetworkGetAddressBase.tracing.test.ts +313 -0
  2. package/__tests__/DeviceCommands.test.ts +59 -13
  3. package/__tests__/DeviceEventRegistration.test.ts +6 -0
  4. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +198 -3
  5. package/__tests__/core-initialization.test.ts +23 -0
  6. package/__tests__/device-connector-protocol.test.ts +81 -0
  7. package/__tests__/device-lifecycle-events.test.ts +105 -3
  8. package/__tests__/device-pool-state.test.ts +22 -0
  9. package/__tests__/device-settings.test.ts +39 -18
  10. package/__tests__/device-state-contract.test.ts +1 -0
  11. package/__tests__/device-state-mapper.test.ts +13 -1
  12. package/__tests__/device-utils.test.ts +48 -1
  13. package/__tests__/deviceSettings.test.ts +11 -1
  14. package/__tests__/deviceUploadNft.test.ts +316 -0
  15. package/__tests__/firmware-update/firmware-update-v3-reconnect.test.ts +5 -0
  16. package/__tests__/get-device-state.test.ts +132 -12
  17. package/__tests__/homescreen.test.ts +41 -1
  18. package/__tests__/kaspa-use-tweak-pro2.test.ts +2 -2
  19. package/__tests__/kaspaSignTransaction.test.ts +3 -26
  20. package/__tests__/logBlockEvent.test.ts +1 -1
  21. package/__tests__/method-protocol-support.test.ts +19 -0
  22. package/__tests__/open-wallet-session.test.ts +568 -37
  23. package/__tests__/pro2Nft.test.ts +108 -0
  24. package/__tests__/protocol-binding.test.ts +89 -0
  25. package/__tests__/protocol-v2-resources.test.ts +340 -0
  26. package/__tests__/protocol-v2-ui-lifecycle.test.ts +91 -0
  27. package/__tests__/protocol-v2-unlock-policy.test.ts +300 -37
  28. package/__tests__/protocol-v2.test.ts +1449 -867
  29. package/__tests__/protocolV2FileWrite.test.ts +152 -0
  30. package/__tests__/protocolV2UiInteraction.test.ts +63 -0
  31. package/__tests__/public-pro2-api-boundary.test.ts +1 -0
  32. package/__tests__/search-devices.test.ts +12 -3
  33. package/__tests__/tron-sign-message-init.test.ts +2 -2
  34. package/__tests__/uiProgressThrottle.test.ts +74 -0
  35. package/__tests__/uiPromiseRegistry.test.ts +115 -0
  36. package/dist/api/BaseMethod.d.ts +3 -1
  37. package/dist/api/BaseMethod.d.ts.map +1 -1
  38. package/dist/api/CheckAllFirmwareRelease.d.ts +2 -1
  39. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  40. package/dist/api/DetectDeviceConnectProtocol.d.ts +7 -0
  41. package/dist/api/DetectDeviceConnectProtocol.d.ts.map +1 -0
  42. package/dist/api/FirmwareUpdate.d.ts.map +1 -1
  43. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  44. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  45. package/dist/api/FirmwareUpdateV4.d.ts +11 -4
  46. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  47. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  48. package/dist/api/OpenWalletSession.d.ts.map +1 -1
  49. package/dist/api/SearchDevices.d.ts.map +1 -1
  50. package/dist/api/allnetwork/AllNetworkGetAddressBase.d.ts.map +1 -1
  51. package/dist/api/device/DeviceSettings.d.ts.map +1 -1
  52. package/dist/api/device/DeviceUnlock.d.ts +2 -2
  53. package/dist/api/device/DeviceUnlock.d.ts.map +1 -1
  54. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +4 -3
  55. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  56. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  57. package/dist/api/helpers/protocolV2FileWrite.d.ts +9 -0
  58. package/dist/api/helpers/protocolV2FileWrite.d.ts.map +1 -1
  59. package/dist/api/index.d.ts +2 -0
  60. package/dist/api/index.d.ts.map +1 -1
  61. package/dist/api/kaspa/KaspaGetAddress.d.ts +1 -1
  62. package/dist/api/kaspa/KaspaSignTransaction.d.ts +1 -1
  63. package/dist/api/kaspa/KaspaSignTransaction.d.ts.map +1 -1
  64. package/dist/api/polkadot/PolkadotGetAddress.d.ts +1 -1
  65. package/dist/api/polkadot/networks.d.ts +1 -1
  66. package/dist/api/protocol-v2/DeviceUploadNft.d.ts +31 -0
  67. package/dist/api/protocol-v2/DeviceUploadNft.d.ts.map +1 -0
  68. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -1
  69. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  70. package/dist/api/solana/SolSignMessage.d.ts +1 -1
  71. package/dist/api/solana/SolSignOffchainMessage.d.ts +1 -1
  72. package/dist/api/solana/SolSignTransaction.d.ts +2 -2
  73. package/dist/api/sui/SuiGetAddress.d.ts +1 -1
  74. package/dist/api/sui/SuiGetPublicKey.d.ts +1 -1
  75. package/dist/api/sui/SuiSignMessage.d.ts +1 -1
  76. package/dist/api/sui/SuiSignTransaction.d.ts +1 -1
  77. package/dist/api/ton/TonGetAddress.d.ts +1 -1
  78. package/dist/api/ton/TonSignMessage.d.ts +1 -1
  79. package/dist/api/ton/TonSignProof.d.ts +1 -1
  80. package/dist/api/tron/TronSignMessage.d.ts +2 -2
  81. package/dist/api/tron/TronSignTransaction.d.ts +1 -1
  82. package/dist/core/deviceEventRegistration.d.ts +2 -0
  83. package/dist/core/deviceEventRegistration.d.ts.map +1 -1
  84. package/dist/core/index.d.ts.map +1 -1
  85. package/dist/core/uiPromiseRegistry.d.ts +6 -0
  86. package/dist/core/uiPromiseRegistry.d.ts.map +1 -0
  87. package/dist/data-manager/DataManager.d.ts +9 -3
  88. package/dist/data-manager/DataManager.d.ts.map +1 -1
  89. package/dist/data-manager/TransportManager.d.ts.map +1 -1
  90. package/dist/device/Device.d.ts +29 -5
  91. package/dist/device/Device.d.ts.map +1 -1
  92. package/dist/device/DeviceCommands.d.ts +4 -4
  93. package/dist/device/DeviceCommands.d.ts.map +1 -1
  94. package/dist/device/DeviceConnector.d.ts +2 -1
  95. package/dist/device/DeviceConnector.d.ts.map +1 -1
  96. package/dist/device/DevicePool.d.ts.map +1 -1
  97. package/dist/deviceProfile/protocolV2DeviceIdentity.d.ts.map +1 -1
  98. package/dist/events/device.d.ts +4 -0
  99. package/dist/events/device.d.ts.map +1 -1
  100. package/dist/events/logBlockEvent.d.ts.map +1 -1
  101. package/dist/events/ui-promise.d.ts +4 -2
  102. package/dist/events/ui-promise.d.ts.map +1 -1
  103. package/dist/events/ui-request.d.ts +31 -2
  104. package/dist/events/ui-request.d.ts.map +1 -1
  105. package/dist/events/ui-response.d.ts +10 -2
  106. package/dist/events/ui-response.d.ts.map +1 -1
  107. package/dist/index.d.ts +243 -69
  108. package/dist/index.js +2563 -1217
  109. package/dist/inject.d.ts +6 -1
  110. package/dist/inject.d.ts.map +1 -1
  111. package/dist/lowLevelInject.d.ts.map +1 -1
  112. package/dist/protocols/protocol-v2/features.d.ts +0 -4
  113. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  114. package/dist/protocols/protocol-v2/firmware.d.ts.map +1 -1
  115. package/dist/protocols/protocol-v2/resources.d.ts +30 -0
  116. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -0
  117. package/dist/protocols/protocol-v2/uiInteraction.d.ts +7 -4
  118. package/dist/protocols/protocol-v2/uiInteraction.d.ts.map +1 -1
  119. package/dist/protocols/protocol-v2/unlockPolicy.d.ts +1 -3
  120. package/dist/protocols/protocol-v2/unlockPolicy.d.ts.map +1 -1
  121. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts +22 -0
  122. package/dist/protocols/protocol-v2/unlockPolicyRunner.d.ts.map +1 -0
  123. package/dist/protocols/protocol-v2/walletSession.d.ts +6 -1
  124. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -1
  125. package/dist/topLevelInject.d.ts.map +1 -1
  126. package/dist/types/api/checkAllFirmwareRelease.d.ts +3 -1
  127. package/dist/types/api/checkAllFirmwareRelease.d.ts.map +1 -1
  128. package/dist/types/api/detectDeviceConnectProtocol.d.ts +4 -0
  129. package/dist/types/api/detectDeviceConnectProtocol.d.ts.map +1 -0
  130. package/dist/types/api/deviceUnlock.d.ts +5 -1
  131. package/dist/types/api/deviceUnlock.d.ts.map +1 -1
  132. package/dist/types/api/export.d.ts +1 -0
  133. package/dist/types/api/export.d.ts.map +1 -1
  134. package/dist/types/api/firmwareUpdate.d.ts +4 -2
  135. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  136. package/dist/types/api/index.d.ts +6 -1
  137. package/dist/types/api/index.d.ts.map +1 -1
  138. package/dist/types/api/protocolV2.d.ts +3 -0
  139. package/dist/types/api/protocolV2.d.ts.map +1 -1
  140. package/dist/types/device.d.ts +2 -2
  141. package/dist/types/device.d.ts.map +1 -1
  142. package/dist/types/params.d.ts +1 -0
  143. package/dist/types/params.d.ts.map +1 -1
  144. package/dist/types/settings.d.ts +38 -8
  145. package/dist/types/settings.d.ts.map +1 -1
  146. package/dist/utils/deviceFeaturesCompat.d.ts.map +1 -1
  147. package/dist/utils/deviceFeaturesUtils.d.ts +4 -0
  148. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  149. package/dist/utils/deviceInfoUtils.d.ts.map +1 -1
  150. package/dist/utils/deviceSettings.d.ts.map +1 -1
  151. package/dist/utils/homescreen.d.ts +4 -0
  152. package/dist/utils/homescreen.d.ts.map +1 -1
  153. package/dist/utils/index.d.ts +1 -1
  154. package/dist/utils/index.d.ts.map +1 -1
  155. package/dist/utils/patch.d.ts +1 -1
  156. package/dist/utils/patch.d.ts.map +1 -1
  157. package/dist/utils/pro2Nft.d.ts +31 -0
  158. package/dist/utils/pro2Nft.d.ts.map +1 -0
  159. package/dist/utils/pro2Wallpaper.d.ts +10 -0
  160. package/dist/utils/pro2Wallpaper.d.ts.map +1 -1
  161. package/dist/utils/uiProgressThrottle.d.ts +3 -0
  162. package/dist/utils/uiProgressThrottle.d.ts.map +1 -0
  163. package/package.json +4 -4
  164. package/src/api/BaseMethod.ts +8 -10
  165. package/src/api/CheckAllFirmwareRelease.ts +61 -11
  166. package/src/api/DetectDeviceConnectProtocol.ts +18 -0
  167. package/src/api/FirmwareUpdate.ts +6 -2
  168. package/src/api/FirmwareUpdateV2.ts +5 -2
  169. package/src/api/FirmwareUpdateV3.ts +6 -2
  170. package/src/api/FirmwareUpdateV4.ts +463 -258
  171. package/src/api/GetPassphraseState.ts +7 -1
  172. package/src/api/OpenWalletSession.ts +71 -17
  173. package/src/api/SearchDevices.ts +3 -1
  174. package/src/api/allnetwork/AllNetworkGetAddress.ts +21 -21
  175. package/src/api/allnetwork/AllNetworkGetAddressBase.ts +36 -11
  176. package/src/api/device/DeviceSettings.ts +5 -4
  177. package/src/api/device/DeviceUnlock.ts +8 -3
  178. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +27 -12
  179. package/src/api/firmware/uploadFirmware.ts +17 -4
  180. package/src/api/helpers/protocolV2FileWrite.ts +202 -63
  181. package/src/api/index.ts +2 -0
  182. package/src/api/kaspa/KaspaGetAddress.ts +1 -1
  183. package/src/api/kaspa/KaspaSignTransaction.ts +2 -13
  184. package/src/api/polkadot/networks.ts +3 -3
  185. package/src/api/protocol-v2/DeviceUploadNft.ts +197 -0
  186. package/src/api/protocol-v2/ProtocolInfoRequest.ts +3 -1
  187. package/src/api/protocol-v2/helpers.ts +1 -1
  188. package/src/api/solana/SolSignMessage.ts +1 -1
  189. package/src/api/solana/SolSignOffchainMessage.ts +1 -1
  190. package/src/api/solana/SolSignTransaction.ts +2 -2
  191. package/src/api/sui/SuiGetAddress.ts +1 -1
  192. package/src/api/sui/SuiGetPublicKey.ts +1 -1
  193. package/src/api/sui/SuiSignMessage.ts +1 -1
  194. package/src/api/sui/SuiSignTransaction.ts +1 -1
  195. package/src/api/ton/TonGetAddress.ts +1 -1
  196. package/src/api/ton/TonSignMessage.ts +1 -1
  197. package/src/api/ton/TonSignProof.ts +1 -1
  198. package/src/api/tron/TronSignMessage.ts +2 -2
  199. package/src/api/tron/TronSignTransaction.ts +1 -1
  200. package/src/core/deviceEventRegistration.ts +4 -0
  201. package/src/core/index.ts +213 -111
  202. package/src/core/uiPromiseRegistry.ts +63 -0
  203. package/src/data/messages/messages-protocol-v2.json +25 -0
  204. package/src/data-manager/DataManager.ts +88 -11
  205. package/src/data-manager/TransportManager.ts +6 -0
  206. package/src/device/Device.ts +273 -43
  207. package/src/device/DeviceCommands.ts +31 -4
  208. package/src/device/DeviceConnector.ts +33 -13
  209. package/src/device/DevicePool.ts +21 -7
  210. package/src/deviceProfile/buildDeviceFeatures.ts +2 -2
  211. package/src/deviceProfile/protocolV2DeviceIdentity.ts +3 -0
  212. package/src/events/device.ts +4 -0
  213. package/src/events/logBlockEvent.ts +1 -0
  214. package/src/events/ui-promise.ts +4 -2
  215. package/src/events/ui-request.ts +36 -2
  216. package/src/events/ui-response.ts +12 -2
  217. package/src/inject.ts +58 -2
  218. package/src/lowLevelInject.ts +6 -3
  219. package/src/protocols/protocol-v2/features.ts +10 -7
  220. package/src/protocols/protocol-v2/firmware.ts +2 -0
  221. package/src/protocols/protocol-v2/resources.ts +390 -0
  222. package/src/protocols/protocol-v2/uiInteraction.ts +46 -6
  223. package/src/protocols/protocol-v2/unlockPolicy.ts +1 -105
  224. package/src/protocols/protocol-v2/unlockPolicyRunner.ts +131 -0
  225. package/src/protocols/protocol-v2/walletSession.ts +214 -36
  226. package/src/topLevelInject.ts +6 -3
  227. package/src/types/api/checkAllFirmwareRelease.ts +3 -1
  228. package/src/types/api/detectDeviceConnectProtocol.ts +6 -0
  229. package/src/types/api/deviceUnlock.ts +12 -1
  230. package/src/types/api/export.ts +1 -0
  231. package/src/types/api/firmwareUpdate.ts +6 -3
  232. package/src/types/api/index.ts +13 -0
  233. package/src/types/api/protocolV2.ts +13 -0
  234. package/src/types/device.ts +15 -3
  235. package/src/types/params.ts +7 -1
  236. package/src/types/settings.ts +60 -9
  237. package/src/utils/deviceFeaturesCompat.ts +5 -0
  238. package/src/utils/deviceFeaturesUtils.ts +14 -3
  239. package/src/utils/deviceInfoUtils.ts +2 -0
  240. package/src/utils/deviceSettings.ts +3 -0
  241. package/src/utils/homescreen.ts +32 -6
  242. package/src/utils/index.ts +6 -1
  243. package/src/utils/pro2Nft.ts +142 -0
  244. package/src/utils/pro2Wallpaper.ts +35 -8
  245. package/src/utils/uiProgressThrottle.ts +63 -0
  246. package/dist/api/firmware/progressThrottle.d.ts +0 -3
  247. package/dist/api/firmware/progressThrottle.d.ts.map +0 -1
  248. package/dist/protocols/protocol-v2/lockedError.d.ts +0 -2
  249. package/dist/protocols/protocol-v2/lockedError.d.ts.map +0 -1
  250. package/dist/protocols/protocol-v2/unlockRetry.d.ts +0 -10
  251. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +0 -1
  252. package/src/api/firmware/progressThrottle.ts +0 -44
  253. package/src/protocols/protocol-v2/lockedError.ts +0 -10
  254. package/src/protocols/protocol-v2/unlockRetry.ts +0 -109
@@ -1,8 +1,8 @@
1
1
  import { EDeviceType, ERRORS, HardwareError, HardwareErrorCode, wait } from '@onekeyfe/hd-shared';
2
2
  import {
3
3
  DeviceRebootType,
4
- PROTOCOL_V2_BLE_FILE_CHUNK_SIZE,
5
4
  PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE,
5
+ PROTOCOL_V2_BLE_FILE_CHUNK_SIZE,
6
6
  PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE,
7
7
  } from '@onekeyfe/hd-transport';
8
8
  import { sha256 } from '@noble/hashes/sha256';
@@ -28,6 +28,12 @@ import {
28
28
  ProtocolV2FirmwareTargetType,
29
29
  } from '../protocols/protocol-v2';
30
30
  import { requestProtocolV2DeviceInfo } from '../protocols/protocol-v2/features';
31
+ import {
32
+ PROTOCOL_V2_RESOURCE_DEVICE_PATHS,
33
+ buildProtocolV2ResourceUpdatePlan,
34
+ isProtocolV2ResourceFileValid,
35
+ readProtocolV2ResourceInventory,
36
+ } from '../protocols/protocol-v2/resources';
31
37
  import {
32
38
  getProtocolV2UnknownErrorText,
33
39
  isProtocolV2DeviceDisconnectedError,
@@ -51,18 +57,21 @@ import type {
51
57
  Features,
52
58
  IFirmwareReleaseInfo,
53
59
  IProtocolV2FirmwareComponent,
60
+ IProtocolV2Resource,
61
+ IProtocolV2ResourceFile,
54
62
  IVersionArray,
55
63
  } from '../types';
56
64
  import type { FirmwareByteSource } from './firmware/FirmwareArtifactSource';
57
65
 
58
66
  const Log = getLogger(LoggerNames.Method);
59
67
 
60
- 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;
68
+ const PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT = 90 * 1000;
69
+ const PROTOCOL_V2_FINAL_RECONNECT_TIMEOUT = 3 * 60 * 1000;
63
70
  const PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT = 5 * 1000;
71
+ const PROTOCOL_V2_FIRMWARE_STATUS_RESPONSE_TIMEOUT = 15 * 1000;
64
72
  const PROTOCOL_V2_START_UPDATE_TIMEOUT = 3 * 60 * 1000;
65
- const PROTOCOL_V2_INSTALL_TIMEOUT = 5 * 60 * 1000;
73
+ const PROTOCOL_V2_INSTALL_TIMEOUT = 8 * 60 * 1000;
74
+ const PROTOCOL_V2_MISSING_TARGET_STATUS_GRACE_TIMEOUT = 30 * 1000;
66
75
  const PROTOCOL_V2_TARGET_STATUS_PENDING = 0;
67
76
  const PROTOCOL_V2_TARGET_STATUS_IN_PROGRESS = 1;
68
77
  const PROTOCOL_V2_TARGET_STATUS_FINISHED = 2;
@@ -73,7 +82,7 @@ const PROTOCOL_V2_MIN_FILE_CHUNK_SIZE = 64;
73
82
  const PROTOCOL_V2_CONNECT_RETRY_COUNT = 10;
74
83
  const PROTOCOL_V2_CONNECT_POLL_INTERVAL = 500;
75
84
  const PROTOCOL_V2_CONNECT_SINGLE_TIMEOUT = 75 * 1000;
76
- const PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT = 60 * 1000;
85
+ const PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT = 30 * 1000;
77
86
  const PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT = 3;
78
87
  const PROTOCOL_V2_INSTALL_STATUS_CONFLICT_CODE = 'FirmwareInstallStatusConflict';
79
88
  const PROTOCOL_V2_OKPP_HEADER_SIZE = 0x52a0;
@@ -111,16 +120,11 @@ type ProtocolV2TargetBinary = { fileName: string; binary: ArrayBuffer; targetId:
111
120
  type ProtocolV2InstallItem = ProtocolV2TargetBinary & {
112
121
  kind: ProtocolV2RemoteComponentTarget['kind'];
113
122
  };
114
- /** RESC bundle okpkg written independently to devicePath through FileWrite. */
123
+ /** Protocol V2 resource file written independently to devicePath through FileWrite. */
115
124
  type ProtocolV2ResourceBundleBinary = {
116
125
  name: string;
117
126
  binary: ArrayBuffer;
118
127
  devicePath: string;
119
- /** Download URL for remote-config mode; omitted in manual mode. */
120
- url?: string;
121
- version?: IVersionArray;
122
- payloadHash?: string;
123
- headerHash?: string;
124
128
  };
125
129
 
126
130
  type ProtocolV2RemoteComponentBinary = ProtocolV2RemoteComponentTarget & {
@@ -409,20 +413,30 @@ export const isProtocolV2FirmwareFingerprintValid = (
409
413
 
410
414
  export const assertProtocolV2ReconnectIdentity = (
411
415
  expectedSerialNumber?: string,
412
- actualSerialNumber?: string
416
+ actualSerialNumber?: string,
417
+ expectedPath?: string,
418
+ actualPath?: string
413
419
  ) => {
414
- if (!expectedSerialNumber || !actualSerialNumber) {
415
- throw ERRORS.TypedError(
416
- HardwareErrorCode.DeviceNotFound,
417
- 'Protocol V2 reconnect physical identity is unavailable'
418
- );
420
+ if (expectedSerialNumber && actualSerialNumber) {
421
+ if (actualSerialNumber !== expectedSerialNumber) {
422
+ throw ERRORS.TypedError(
423
+ HardwareErrorCode.DeviceNotFound,
424
+ `Protocol V2 reconnect physical identity mismatch: expected ${expectedSerialNumber}, received ${actualSerialNumber}`
425
+ );
426
+ }
427
+ return;
419
428
  }
420
- if (actualSerialNumber !== expectedSerialNumber) {
421
- throw ERRORS.TypedError(
422
- HardwareErrorCode.DeviceNotFound,
423
- `Protocol V2 reconnect physical identity mismatch: expected ${expectedSerialNumber}, received ${actualSerialNumber}`
424
- );
429
+
430
+ if (expectedPath && actualPath && expectedPath === actualPath) {
431
+ return;
425
432
  }
433
+
434
+ throw ERRORS.TypedError(
435
+ HardwareErrorCode.DeviceNotFound,
436
+ `Protocol V2 reconnect physical identity unavailable: expected path ${
437
+ expectedPath ?? 'unknown'
438
+ }, received ${actualPath ?? 'unknown'}`
439
+ );
426
440
  };
427
441
 
428
442
  /**
@@ -436,6 +450,8 @@ export const assertProtocolV2ReconnectIdentity = (
436
450
  export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareUpdateV4Params> {
437
451
  private protocolV2ExpectedSerialNumber?: string;
438
452
 
453
+ private protocolV2ExpectedPath?: string;
454
+
439
455
  getSupportedProtocols() {
440
456
  return ['V2'] as const;
441
457
  }
@@ -488,7 +504,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
488
504
  { name: 'targetsToUpdate', type: 'array', allowEmpty: true },
489
505
  { name: 'platform', type: 'string' },
490
506
  { name: 'expectedDeviceId', type: 'string' },
491
- { name: 'resourceBundleFiles', type: 'array', allowEmpty: true },
507
+ { name: 'resourceFiles', type: 'array', allowEmpty: true },
492
508
  ]);
493
509
  if (
494
510
  payload.expectedDeviceId !== undefined &&
@@ -554,7 +570,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
554
570
  se02Binary: payload.se02Binary,
555
571
  se03Binary: payload.se03Binary,
556
572
  se04Binary: payload.se04Binary,
557
- resourceBundleFiles: payload.resourceBundleFiles,
573
+ resourceFiles: payload.resourceFiles,
558
574
  firmwareType: payload.firmwareType,
559
575
  targetsToUpdate: payload.targetsToUpdate,
560
576
  expectedTargetVersions: payload.expectedTargetVersions,
@@ -604,6 +620,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
604
620
  ) {
605
621
  return this.runProtocolV2PreparedArtifacts(deviceFeatures, firmwareType);
606
622
  }
623
+ const needsRemoteResources =
624
+ !this.params.resourceFiles?.length && !!this.params.targetsToUpdate?.includes('resource');
625
+ const needsRemoteBootResources = !!this.params.targetsToUpdate?.includes('boot_resources');
607
626
 
608
627
  let fwBinaryMap: ProtocolV2TargetBinary[] = [];
609
628
  let bootloaderBinary: ArrayBuffer | null = null;
@@ -611,21 +630,30 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
611
630
  let resourceBundles: ProtocolV2ResourceBundleBinary[] | undefined;
612
631
  try {
613
632
  this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
633
+ resourceBundles = this.prepareExplicitProtocolV2ResourceFiles();
614
634
  fwBinaryMap = this.collectExplicitTargetBinaries();
615
635
  bootloaderBinary = this.prepareBootloaderBinary();
616
- if (!this.hasExplicitProtocolV2Payload(fwBinaryMap)) {
617
- if (
618
- this.params.artifactReader ||
619
- DataManager.getSettings('firmwareManifestMode') === 'external-only'
620
- ) {
621
- throw ERRORS.TypedError(
622
- HardwareErrorCode.RuntimeError,
623
- 'Protocol V2 firmware artifacts must be prepared by the external firmware host',
624
- {
625
- firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
626
- }
627
- );
628
- }
636
+ const needsRemoteFirmware = !this.hasExplicitProtocolV2Payload(fwBinaryMap);
637
+ if (
638
+ (needsRemoteFirmware || needsRemoteResources || needsRemoteBootResources) &&
639
+ (this.params.artifactReader ||
640
+ DataManager.getSettings('firmwareManifestMode') === 'external-only')
641
+ ) {
642
+ throw ERRORS.TypedError(
643
+ HardwareErrorCode.RuntimeError,
644
+ 'Protocol V2 firmware artifacts must be prepared by the external firmware host',
645
+ {
646
+ firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
647
+ }
648
+ );
649
+ }
650
+ if (needsRemoteFirmware || needsRemoteResources || needsRemoteBootResources) {
651
+ // Remote updates must use a freshly fetched config before any reboot or file write.
652
+ await DataManager.forceReloadData({
653
+ requireResources: needsRemoteResources || needsRemoteBootResources,
654
+ });
655
+ }
656
+ if (needsRemoteFirmware) {
629
657
  const remoteBinaries = await this.prepareRemoteProtocolV2Binaries(
630
658
  firmwareType,
631
659
  deviceFeatures
@@ -634,25 +662,54 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
634
662
  fwBinaryMap = remoteBinaries.fwBinaryMap;
635
663
  installItems = remoteBinaries.installItems;
636
664
  }
637
- const resourceDescriptors = this.prepareProtocolV2ResourceBundles(
638
- firmwareType,
639
- deviceFeatures
640
- );
641
- resourceBundles = resourceDescriptors?.length
642
- ? await this.materializeProtocolV2ResourceBundles(resourceDescriptors)
643
- : undefined;
644
- this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
665
+ const bootResourceFiles = await this.prepareProtocolV2BootResources();
666
+ if (bootResourceFiles?.length) {
667
+ resourceBundles = [...(resourceBundles ?? []), ...bootResourceFiles];
668
+ }
669
+ if (!needsRemoteResources) {
670
+ this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
671
+ }
645
672
  } catch (err) {
673
+ if (err instanceof HardwareError && err.errorCode === HardwareErrorCode.NetworkError) {
674
+ throw err;
675
+ }
646
676
  throw normalizeFirmwarePreparationError(err);
647
677
  }
648
678
 
649
- if (!bootloaderBinary && fwBinaryMap.length === 0 && !resourceBundles?.length) {
679
+ if (
680
+ !bootloaderBinary &&
681
+ fwBinaryMap.length === 0 &&
682
+ !installItems?.length &&
683
+ !resourceBundles?.length &&
684
+ !needsRemoteResources
685
+ ) {
650
686
  throw ERRORS.TypedError(
651
687
  HardwareErrorCode.FirmwareUpdateDownloadFailed,
652
688
  'No firmware to update'
653
689
  );
654
690
  }
655
691
 
692
+ if (needsRemoteResources) {
693
+ const enteredBootloader = await this.enterProtocolV2BootloaderMode();
694
+ try {
695
+ const stableResources = await this.prepareProtocolV2ResourceBundles();
696
+ resourceBundles = [...(resourceBundles ?? []), ...(stableResources ?? [])];
697
+ this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
698
+ } catch (err) {
699
+ if (enteredBootloader) {
700
+ try {
701
+ await this.exitProtocolV2BootloaderToNormal();
702
+ } catch (restoreError) {
703
+ Log.warn(
704
+ '[FirmwareUpdateV4] failed to restore App mode after resource preparation error:',
705
+ restoreError
706
+ );
707
+ }
708
+ }
709
+ throw normalizeFirmwarePreparationError(err);
710
+ }
711
+ }
712
+
656
713
  return this.executeProtocolV2Update({
657
714
  fwBinaryMap,
658
715
  bootloaderBinary,
@@ -924,7 +981,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
924
981
  Object.keys(this.params.componentArtifacts ?? {}).forEach(target =>
925
982
  targets.add(target as FirmwareUpdateV4Target)
926
983
  );
927
- if (this.params.resourceBundleArtifacts?.length || this.params.resourceBundleFiles?.length) {
984
+ if (this.params.resourceBundleArtifacts?.length || this.params.resourceFiles?.length) {
928
985
  targets.add('resource');
929
986
  }
930
987
  if (this.params.bootloaderBinary) targets.add('boot');
@@ -957,20 +1014,29 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
957
1014
  private assertProtocolV2DeviceInfoIdentity(deviceInfo: ProtocolV2DeviceInfo) {
958
1015
  assertProtocolV2ReconnectIdentity(
959
1016
  this.protocolV2ExpectedSerialNumber,
960
- this.getProtocolV2SerialNumber(deviceInfo)
1017
+ this.getProtocolV2SerialNumber(deviceInfo),
1018
+ this.protocolV2ExpectedPath,
1019
+ this.device.originalDescriptor.path
961
1020
  );
962
1021
  }
963
1022
 
1023
+ private getProtocolV2DeviceInfoTimeout() {
1024
+ return this.isBleReconnect()
1025
+ ? this.payload.protocolV2DeviceInfoTimeoutMs ?? PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT
1026
+ : PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT;
1027
+ }
1028
+
964
1029
  private async requestProtocolV2PhysicalIdentity() {
965
1030
  return requestProtocolV2DeviceInfo({
966
1031
  commands: this.device.getCommands(),
967
- timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
1032
+ timeoutMs: this.getProtocolV2DeviceInfoTimeout(),
968
1033
  });
969
1034
  }
970
1035
 
971
1036
  private async captureProtocolV2PhysicalIdentity() {
972
1037
  const deviceInfo = await this.requestProtocolV2PhysicalIdentity();
973
1038
  const serialNumber = this.getProtocolV2SerialNumber(deviceInfo);
1039
+ const path = this.device.originalDescriptor?.path?.trim() || undefined;
974
1040
  if (this.params?.preparedPlan) {
975
1041
  assertFirmwareUpdatePreparedPlanDeviceIdentity({
976
1042
  preparedPlan: this.params.preparedPlan,
@@ -979,9 +1045,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
979
1045
  } else if (this.params?.expectedDeviceId) {
980
1046
  assertProtocolV2ReconnectIdentity(this.params?.expectedDeviceId, serialNumber);
981
1047
  } else {
982
- assertProtocolV2ReconnectIdentity(serialNumber, serialNumber);
1048
+ assertProtocolV2ReconnectIdentity(serialNumber, serialNumber, path, path);
983
1049
  }
984
1050
  this.protocolV2ExpectedSerialNumber = serialNumber;
1051
+ this.protocolV2ExpectedPath = path;
985
1052
  }
986
1053
 
987
1054
  private async verifyProtocolV2ReconnectIdentity() {
@@ -996,7 +1063,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
996
1063
 
997
1064
  private hasExplicitProtocolV2Payload(fwBinaryMap: ProtocolV2TargetBinary[]) {
998
1065
  return (
999
- !!this.params.resourceBundleFiles?.length ||
1066
+ !!this.params.resourceFiles?.length ||
1000
1067
  !!this.params.bootloaderBinary ||
1001
1068
  fwBinaryMap.length > 0
1002
1069
  );
@@ -1059,57 +1126,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1059
1126
  return target;
1060
1127
  }
1061
1128
 
1062
- private getProtocolV2ResourceFilePath(path: string) {
1063
- if (path.startsWith('vol')) return path;
1064
- if (path.startsWith('/')) return `vol0:${path}`;
1065
- return `vol0:/${path}`;
1066
- }
1067
-
1068
- private async readProtocolV2DeviceFileHeader(path: string) {
1069
- const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
1070
- const filePath = this.getProtocolV2ResourceFilePath(path);
1071
- const pathInfoRes = await typedCall('FilesystemPathInfoQuery', 'FilesystemPathInfo', {
1072
- path: filePath,
1073
- });
1074
- const fileSize = toProtocolV2FiniteNumber(pathInfoRes.message?.size);
1075
- if (
1076
- !pathInfoRes.message?.exist ||
1077
- pathInfoRes.message?.directory ||
1078
- fileSize === undefined ||
1079
- fileSize < PROTOCOL_V2_OKPP_HEADER_SIZE
1080
- ) {
1081
- return null;
1082
- }
1083
-
1084
- const chunkSize = this.getProtocolV2FirmwareChunkSize('read');
1085
- const chunks: Uint8Array[] = [];
1086
- let offset = 0;
1087
- while (offset < PROTOCOL_V2_OKPP_HEADER_SIZE) {
1088
- const readLen = Math.min(chunkSize, PROTOCOL_V2_OKPP_HEADER_SIZE - offset);
1089
- const res = await typedCall('FilesystemFileRead', 'FilesystemFile', {
1090
- file: {
1091
- path: filePath,
1092
- offset,
1093
- total_size: 0,
1094
- },
1095
- chunk_len: readLen,
1096
- ui_percentage: undefined,
1097
- });
1098
- const data = toProtocolV2Bytes(res.message?.data);
1099
- if (data.byteLength === 0) return null;
1100
- chunks.push(data);
1101
- offset += data.byteLength;
1102
- }
1103
-
1104
- const headerBytes = new Uint8Array(offset);
1105
- let cursor = 0;
1106
- chunks.forEach(chunk => {
1107
- headerBytes.set(chunk, cursor);
1108
- cursor += chunk.byteLength;
1109
- });
1110
- return parseProtocolV2OkppHeader(headerBytes);
1111
- }
1112
-
1113
1129
  private async downloadRemoteProtocolV2Component(
1114
1130
  key: string,
1115
1131
  component: IProtocolV2FirmwareComponent
@@ -1198,123 +1214,177 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1198
1214
  };
1199
1215
  }
1200
1216
 
1201
- // ============================================================
1202
- // Incremental RESC bundle sync through FileRead and FilesystemFileWrite
1203
- // ============================================================
1217
+ private getProtocolV2DeviceType(): EDeviceType.Pro2 | EDeviceType.Neo {
1218
+ const deviceType = this.device.getCurrentDeviceType();
1219
+ if (deviceType === EDeviceType.Pro2 || deviceType === EDeviceType.Neo) return deviceType;
1220
+ throw new Error(`Unsupported Protocol V2 device type: ${deviceType}`);
1221
+ }
1204
1222
 
1205
- /**
1206
- * Prepare the RESC bundle list.
1207
- *
1208
- * Two modes, matching FirmwareUpdateV3 binary-versus-version behavior:
1209
- * - resourceBundleFiles supplied: use binaries directly without version checks.
1210
- * - Otherwise load release.resourceBundles from remote config.json; the binaries
1211
- * are prefetched before reboot and synchronization skips matching device headers.
1212
- */
1213
- private prepareProtocolV2ResourceBundles(
1214
- firmwareType: EFirmwareType,
1215
- features: Features
1216
- ): ProtocolV2ResourceBundleBinary[] | undefined {
1217
- // Manual binaries are installed directly without comparison.
1218
- if (this.params.resourceBundleFiles?.length) {
1219
- return this.params.resourceBundleFiles.map((file, index) => {
1220
- const devicePath = validateProtocolV2FilesystemPath(
1221
- file.devicePath,
1222
- `resourceBundleFiles[${index}].devicePath`
1223
- );
1224
- return {
1225
- name: devicePath.split('/').pop() ?? devicePath,
1226
- binary: file.binary,
1227
- devicePath,
1228
- };
1223
+ private async prepareProtocolV2BootResources(): Promise<
1224
+ ProtocolV2ResourceBundleBinary[] | undefined
1225
+ > {
1226
+ if (!this.params.targetsToUpdate?.includes('boot_resources')) return undefined;
1227
+ const resource = DataManager.getProtocolV2BootResources(this.getProtocolV2DeviceType());
1228
+ if (!resource) throw new Error('Missing Protocol V2 boot resources configuration');
1229
+
1230
+ const files: ProtocolV2ResourceBundleBinary[] = [];
1231
+ for (const file of resource.files) {
1232
+ Log.log(`[FirmwareUpdateV4] downloading boot resource ${file.devicePath}`);
1233
+ const { binary } = await getSysResourceBinary(file.url);
1234
+ if (!isProtocolV2ResourceFileValid(binary, file)) {
1235
+ throw new Error(`Boot resource file verification failed: ${file.devicePath}`);
1236
+ }
1237
+ files.push({
1238
+ name: file.name ?? file.devicePath.split('/').pop() ?? file.devicePath,
1239
+ binary,
1240
+ devicePath: file.devicePath,
1229
1241
  });
1230
1242
  }
1243
+ return files;
1244
+ }
1231
1245
 
1246
+ private prepareExplicitProtocolV2ResourceFiles(): ProtocolV2ResourceBundleBinary[] | undefined {
1247
+ const files = this.params.resourceFiles ?? [];
1248
+ if (!files?.length) return undefined;
1249
+
1250
+ const prepared = files.map((file, index) => {
1251
+ const devicePath = validateProtocolV2FilesystemPath(
1252
+ file.devicePath,
1253
+ `resourceFiles[${index}].devicePath`
1254
+ );
1255
+ const descriptor = file as typeof file &
1256
+ Pick<Partial<IProtocolV2ResourceFile>, 'size' | 'fileHash'>;
1257
+ if (descriptor.size !== undefined && descriptor.size !== file.binary.byteLength) {
1258
+ throw new Error(`resourceFiles[${index}] size mismatch`);
1259
+ }
1260
+ if (
1261
+ descriptor.fileHash &&
1262
+ !isProtocolV2ResourceFileValid(file.binary, {
1263
+ size: file.binary.byteLength,
1264
+ fileHash: descriptor.fileHash,
1265
+ })
1266
+ ) {
1267
+ throw new Error(`resourceFiles[${index}] SHA-256 mismatch`);
1268
+ }
1269
+ return {
1270
+ name: devicePath.split('/').pop() ?? devicePath,
1271
+ binary: file.binary,
1272
+ devicePath,
1273
+ };
1274
+ });
1275
+ if (new Set(prepared.map(file => file.devicePath)).size !== prepared.length) {
1276
+ throw new Error('resourceFiles contain duplicate devicePath values');
1277
+ }
1278
+ return prepared;
1279
+ }
1280
+
1281
+ private async prepareProtocolV2ResourceBundles(): Promise<
1282
+ ProtocolV2ResourceBundleBinary[] | undefined
1283
+ > {
1232
1284
  if (!this.params.targetsToUpdate?.includes('resource')) {
1233
1285
  return undefined;
1234
1286
  }
1235
1287
 
1236
- // Remote-config bundles are downloaded and compared later.
1237
- const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
1238
- if (!release?.resourceBundles?.length) return undefined;
1239
-
1240
- return release.resourceBundles.map((bundle, index) => ({
1241
- name: bundle.name,
1242
- binary: new ArrayBuffer(0),
1243
- devicePath: validateProtocolV2FilesystemPath(
1244
- bundle.devicePath,
1245
- `resourceBundles[${index}].devicePath`
1246
- ),
1247
- url: bundle.url,
1248
- version: bundle.version,
1249
- payloadHash: bundle.payloadHash,
1250
- headerHash: bundle.headerHash,
1251
- })) as ProtocolV2ResourceBundleBinary[];
1252
- }
1253
-
1254
- private async materializeProtocolV2ResourceBundles(
1255
- bundles: ProtocolV2ResourceBundleBinary[]
1256
- ): Promise<ProtocolV2ResourceBundleBinary[]> {
1257
- for (const bundle of bundles) {
1258
- let { binary } = bundle;
1259
- let downloadedFromRemote = false;
1260
- if (binary.byteLength === 0) {
1261
- if (!bundle.url) {
1262
- throw ERRORS.TypedError(
1263
- HardwareErrorCode.FirmwareUpdateDownloadFailed,
1264
- `Missing Protocol V2 RESC bundle binary: ${bundle.name}`
1265
- );
1266
- }
1267
- if (
1268
- this.params.artifactReader ||
1269
- DataManager.getSettings('firmwareManifestMode') === 'external-only'
1270
- ) {
1271
- throw ERRORS.TypedError(
1272
- HardwareErrorCode.RuntimeError,
1273
- `Firmware resource bundle ${bundle.name} is not prepared`,
1274
- {
1275
- firmwareUpdateCode: 'FirmwareArtifactsNotPrepared',
1276
- artifactName: bundle.name,
1277
- }
1278
- );
1279
- }
1280
- Log.log(`[FirmwareUpdateV4] downloading remote RESC bundle ${bundle.name}`);
1281
- ({ binary } = await getSysResourceBinary(bundle.url));
1282
- bundle.binary = binary;
1283
- downloadedFromRemote = true;
1284
- }
1285
- if (bundle.binary.byteLength === 0) {
1286
- throw ERRORS.TypedError(
1287
- HardwareErrorCode.FirmwareUpdateDownloadFailed,
1288
- `Protocol V2 RESC bundle is empty: ${bundle.name}`
1289
- );
1290
- }
1291
- if (downloadedFromRemote) {
1292
- const header = parseProtocolV2OkppHeader(toProtocolV2Bytes(binary));
1293
- if (!header || header.type !== 'RESC') {
1294
- throw new Error(`Invalid Protocol V2 RESC bundle header: ${bundle.name}`);
1295
- }
1296
- if (bundle.version && compareProtocolV2Versions(header.version, bundle.version) !== 0) {
1297
- throw new Error(`Protocol V2 RESC bundle version mismatch: ${bundle.name}`);
1298
- }
1299
- const expectedPayloadHash = normalizeProtocolV2Hex(bundle.payloadHash);
1300
- if (expectedPayloadHash && header.payloadHash !== expectedPayloadHash) {
1301
- throw new Error(`Protocol V2 RESC bundle payload hash mismatch: ${bundle.name}`);
1302
- }
1303
- const expectedHeaderHash = normalizeProtocolV2Hex(bundle.headerHash);
1304
- if (expectedHeaderHash && header.headerHash !== expectedHeaderHash) {
1305
- throw new Error(`Protocol V2 RESC bundle header hash mismatch: ${bundle.name}`);
1306
- }
1307
- }
1288
+ const resources = DataManager.getProtocolV2Resources(this.getProtocolV2DeviceType());
1289
+ if (!resources?.length) {
1290
+ throw new Error('Missing Pro2 stable resource configuration');
1291
+ }
1292
+
1293
+ const inventory = this.params.forcedUpdateRes
1294
+ ? undefined
1295
+ : await readProtocolV2ResourceInventory({
1296
+ commands: this.device.getCommands(),
1297
+ resources,
1298
+ chunkSize: this.getProtocolV2FirmwareChunkSize(),
1299
+ timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
1300
+ });
1301
+ const plan = buildProtocolV2ResourceUpdatePlan({
1302
+ resources,
1303
+ inventory,
1304
+ mode: 'bootloader-recovery',
1305
+ forced: this.params.forcedUpdateRes,
1306
+ });
1307
+ Log.log(
1308
+ `[FirmwareUpdateV4] Protocol V2 resource plan mode=bootloader-recovery status=${plan.status} count=${plan.resources.length}`
1309
+ );
1310
+
1311
+ const bundles: ProtocolV2ResourceBundleBinary[] = [];
1312
+ for (const resource of plan.resources) {
1313
+ bundles.push(await this.downloadProtocolV2Resource(resource));
1308
1314
  }
1309
1315
  return bundles;
1310
1316
  }
1311
1317
 
1312
- /**
1313
- * Compare the existing okpkg OKPP header in remote-config mode.
1314
- */
1318
+ private async downloadProtocolV2Resource(
1319
+ resource: IProtocolV2Resource
1320
+ ): Promise<ProtocolV2ResourceBundleBinary> {
1321
+ Log.log(`[FirmwareUpdateV4] downloading Pro2 resource ${resource.type}`);
1322
+ const { binary } = await getSysResourceBinary(resource.url);
1323
+ if (!isProtocolV2ResourceFileValid(binary, resource)) {
1324
+ throw new Error(`Pro2 resource file verification failed: ${resource.type}`);
1325
+ }
1326
+ return {
1327
+ name: `${resource.type}.okpkg`,
1328
+ binary,
1329
+ devicePath: PROTOCOL_V2_RESOURCE_DEVICE_PATHS[resource.type],
1330
+ };
1331
+ }
1332
+
1333
+ private getProtocolV2ResourceFilePath(path: string) {
1334
+ if (path.startsWith('vol')) return path;
1335
+ if (path.startsWith('/')) return `vol0:${path}`;
1336
+ return `vol0:/${path}`;
1337
+ }
1338
+
1339
+ private async readProtocolV2DeviceFileHeader(path: string) {
1340
+ const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
1341
+ const filePath = this.getProtocolV2ResourceFilePath(path);
1342
+ const pathInfoRes = await typedCall('FilesystemPathInfoQuery', 'FilesystemPathInfo', {
1343
+ path: filePath,
1344
+ });
1345
+ const fileSize = toProtocolV2FiniteNumber(pathInfoRes.message?.size);
1346
+ if (
1347
+ !pathInfoRes.message?.exist ||
1348
+ pathInfoRes.message?.directory ||
1349
+ fileSize === undefined ||
1350
+ fileSize < PROTOCOL_V2_OKPP_HEADER_SIZE
1351
+ ) {
1352
+ return null;
1353
+ }
1354
+
1355
+ const chunkSize = this.getProtocolV2FirmwareChunkSize('read');
1356
+ const chunks: Uint8Array[] = [];
1357
+ let offset = 0;
1358
+ while (offset < PROTOCOL_V2_OKPP_HEADER_SIZE) {
1359
+ const readLen = Math.min(chunkSize, PROTOCOL_V2_OKPP_HEADER_SIZE - offset);
1360
+ const res = await typedCall('FilesystemFileRead', 'FilesystemFile', {
1361
+ file: {
1362
+ path: filePath,
1363
+ offset,
1364
+ total_size: 0,
1365
+ },
1366
+ chunk_len: readLen,
1367
+ ui_percentage: undefined,
1368
+ });
1369
+ const data = toProtocolV2Bytes(res.message?.data);
1370
+ if (data.byteLength === 0) return null;
1371
+ chunks.push(data);
1372
+ offset += data.byteLength;
1373
+ }
1374
+
1375
+ const headerBytes = new Uint8Array(offset);
1376
+ let cursor = 0;
1377
+ chunks.forEach(chunk => {
1378
+ headerBytes.set(chunk, cursor);
1379
+ cursor += chunk.byteLength;
1380
+ });
1381
+ return parseProtocolV2OkppHeader(headerBytes);
1382
+ }
1383
+
1384
+ /** Compare a prepared okpkg header when its manifest supplies version or hash metadata. */
1315
1385
  private async isProtocolV2ResourceBundleUpToDate(
1316
1386
  bundle: Pick<
1317
- ProtocolV2ResourceBundleBinary,
1387
+ ProtocolV2ResourceBundleSource,
1318
1388
  'name' | 'devicePath' | 'version' | 'payloadHash' | 'headerHash'
1319
1389
  >
1320
1390
  ): Promise<boolean> {
@@ -1346,7 +1416,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1346
1416
 
1347
1417
  private isProtocolV2BootloaderMode() {
1348
1418
  if (typeof this.device.isBootloader === 'function') {
1349
- return this.device.isBootloader();
1419
+ return !!this.device.isBootloader();
1350
1420
  }
1351
1421
  return (
1352
1422
  this.device.features?.mode === 'bootloader' ||
@@ -1355,7 +1425,11 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1355
1425
  }
1356
1426
 
1357
1427
  private isProtocolV2RomloaderMode() {
1358
- if (!this.device.isProtocolV2() || this.device.getCurrentDeviceType() !== EDeviceType.Pro2) {
1428
+ const deviceType = this.device.getCurrentDeviceType();
1429
+ if (
1430
+ !this.device.isProtocolV2() ||
1431
+ (deviceType !== EDeviceType.Pro2 && deviceType !== EDeviceType.Neo)
1432
+ ) {
1359
1433
  return false;
1360
1434
  }
1361
1435
  if (typeof this.device.isRomloader === 'function') {
@@ -1375,16 +1449,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1375
1449
  if (this.isProtocolV2BootloaderMode()) {
1376
1450
  Log.debug('Protocol V2 device is already in bootloader mode, skip reboot');
1377
1451
  this.protocolV2ExecutionInLoader = true;
1452
+ this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
1378
1453
  return false;
1379
1454
  }
1380
1455
 
1381
1456
  try {
1382
1457
  this.postTipMessage(FirmwareUpdateTipMessage.AutoRebootToBootloader);
1383
1458
  await this.protocolV2Reboot(DeviceRebootType.Bootloader);
1384
- this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
1385
1459
  await wait(1000);
1386
1460
  await this.waitForProtocolV2BootloaderMode();
1387
1461
  this.protocolV2ExecutionInLoader = true;
1462
+ this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
1388
1463
  return true;
1389
1464
  } catch (error) {
1390
1465
  if (error instanceof HardwareError) {
@@ -1401,13 +1476,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1401
1476
  ) {
1402
1477
  const startTime = Date.now();
1403
1478
  let lastError: unknown;
1479
+ let shouldReconnect = true;
1404
1480
 
1405
1481
  while (Date.now() - startTime < timeout) {
1406
1482
  try {
1407
- await this.reconnectProtocolV2Device();
1483
+ if (shouldReconnect) {
1484
+ await this.reconnectProtocolV2Device();
1485
+ shouldReconnect = false;
1486
+ }
1408
1487
  const deviceInfo = await requestProtocolV2DeviceInfo({
1409
1488
  commands: this.device.getCommands(),
1410
- timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
1489
+ timeoutMs: this.getProtocolV2DeviceInfoTimeout(),
1411
1490
  });
1412
1491
  this.assertProtocolV2DeviceInfoIdentity(deviceInfo);
1413
1492
  const features = await this.device.probeProtocolV2RuntimeState(
@@ -1422,6 +1501,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1422
1501
  if (this.isProtocolV2ReconnectIdentityError(error)) {
1423
1502
  throw error;
1424
1503
  }
1504
+ shouldReconnect = true;
1425
1505
  lastError = error;
1426
1506
  Log.log('Protocol V2 bootloader mode not ready, polling reconnect: ', error);
1427
1507
  }
@@ -1597,9 +1677,6 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1597
1677
  name: bundle.name,
1598
1678
  source: await this.openProtocolV2MemorySource(bundle.binary),
1599
1679
  devicePath: bundle.devicePath,
1600
- version: bundle.version,
1601
- payloadHash: bundle.payloadHash,
1602
- headerHash: bundle.headerHash,
1603
1680
  }))
1604
1681
  );
1605
1682
  return await this.executeProtocolV2Phases({
@@ -1684,12 +1761,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1684
1761
  let lastError: unknown;
1685
1762
  for (let attempt = 1; attempt <= PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT; attempt += 1) {
1686
1763
  try {
1764
+ const transferStartedAt = Date.now();
1687
1765
  await writeFirmwareByteSource({
1688
1766
  source,
1689
1767
  chunkSize: this.getProtocolV2FirmwareChunkSize(),
1690
1768
  write: async ({ data, sourceOffset, length, first }) => {
1691
1769
  const chunkEnd = sourceOffset + length;
1692
- const progress = getProtocolV2DeviceTransferProgress(
1770
+ const deviceProgress = getProtocolV2DeviceTransferProgress(
1693
1771
  processedSize + sourceOffset,
1694
1772
  processedSize + chunkEnd,
1695
1773
  totalSize
@@ -1700,7 +1778,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1700
1778
  sourceOffset,
1701
1779
  data,
1702
1780
  first,
1703
- progress
1781
+ deviceProgress
1704
1782
  );
1705
1783
  const rawProcessedByte = response.message.processed_byte;
1706
1784
  const nextOffset = rawProcessedByte === undefined ? chunkEnd : Number(rawProcessedByte);
@@ -1710,7 +1788,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1710
1788
  `invalid processed_byte ${rawProcessedByte} for offset ${sourceOffset}`
1711
1789
  );
1712
1790
  }
1713
- this.postProgressMessage(progress, 'transferData');
1791
+ const transferredBytes = processedSize + chunkEnd;
1792
+ const elapsedMs = Math.max(Date.now() - transferStartedAt, 0);
1793
+ this.postProgressMessage(
1794
+ Math.min(Math.ceil((transferredBytes / totalSize) * 100), 99),
1795
+ 'transferData',
1796
+ {
1797
+ transferredBytes,
1798
+ totalBytes: totalSize,
1799
+ rateBytesPerSecond:
1800
+ elapsedMs > 0 ? Math.round((chunkEnd / elapsedMs) * 1000) : undefined,
1801
+ elapsedMs,
1802
+ }
1803
+ );
1714
1804
  return length;
1715
1805
  },
1716
1806
  });
@@ -1850,6 +1940,20 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1850
1940
  return false;
1851
1941
  }
1852
1942
 
1943
+ private getProtocolV2MissingTargetIds(
1944
+ statusTargets: ProtocolV2FirmwareUpdateStatusTarget[],
1945
+ expectedTargetIds: Set<number>
1946
+ ) {
1947
+ const reportedTargetIds = new Set<number>();
1948
+ statusTargets.forEach(target => {
1949
+ const targetId = normalizeProtocolV2TargetId(target.target_id);
1950
+ if (targetId !== undefined) {
1951
+ reportedTargetIds.add(targetId);
1952
+ }
1953
+ });
1954
+ return Array.from(expectedTargetIds).filter(targetId => !reportedTargetIds.has(targetId));
1955
+ }
1956
+
1853
1957
  private async waitForProtocolV2FirmwareUpdateComplete(
1854
1958
  targets: Array<{ target_id: number; path: string }>
1855
1959
  ) {
@@ -1858,11 +1962,23 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1858
1962
  const expectedPaths = new Map(targets.map(target => [target.target_id, target.path]));
1859
1963
  const startTime = Date.now();
1860
1964
  let lastError: unknown;
1965
+ let shouldReconnect = true;
1966
+ let deviceInfo: ProtocolV2DeviceInfo | undefined;
1967
+ let missingTargetStatusSince: number | undefined;
1968
+ let missingTargetStatusKey: string | undefined;
1969
+ const resetMissingTargetStatusGrace = () => {
1970
+ missingTargetStatusSince = undefined;
1971
+ missingTargetStatusKey = undefined;
1972
+ };
1861
1973
 
1862
1974
  while (Date.now() - startTime < PROTOCOL_V2_INSTALL_TIMEOUT) {
1863
1975
  try {
1864
- await this.reconnectProtocolV2Device();
1865
- const deviceInfo = await this.verifyProtocolV2ReconnectIdentity();
1976
+ if (shouldReconnect) {
1977
+ await this.reconnectProtocolV2Device();
1978
+ deviceInfo = await this.verifyProtocolV2ReconnectIdentity();
1979
+ shouldReconnect = false;
1980
+ }
1981
+ const currentDeviceInfo = deviceInfo;
1866
1982
  try {
1867
1983
  const statusResponse = await this.device.getCommands().typedCall(
1868
1984
  'DeviceFirmwareUpdateStatusGet',
@@ -1874,36 +1990,87 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1874
1990
  path: true,
1875
1991
  },
1876
1992
  },
1877
- { timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT }
1993
+ { timeoutMs: PROTOCOL_V2_FIRMWARE_STATUS_RESPONSE_TIMEOUT }
1878
1994
  );
1995
+ const statusTargets = (statusResponse.message.records ??
1996
+ []) as ProtocolV2FirmwareUpdateStatusTarget[];
1997
+ if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds, expectedPaths)) {
1998
+ this.protocolV2FinalStatusVerified = true;
1999
+ return;
2000
+ }
2001
+
1879
2002
  if (
1880
- this.assertProtocolV2TargetStatus(
1881
- (statusResponse.message.records ?? []) as ProtocolV2FirmwareUpdateStatusTarget[],
1882
- expectedTargetIds,
1883
- expectedPaths
1884
- )
2003
+ statusTargets.length === 0 &&
2004
+ currentDeviceInfo &&
2005
+ (await this.probeProtocolV2NormalMode(currentDeviceInfo))
1885
2006
  ) {
1886
- this.protocolV2FinalStatusVerified = true;
2007
+ Log.log(
2008
+ '[FirmwareUpdateV4] empty firmware status after confirmed App reboot; update complete'
2009
+ );
2010
+ this.postProgressMessage(100, 'installingFirmware');
1887
2011
  return;
1888
2012
  }
1889
- lastError = new Error('Protocol V2 firmware targets are still installing');
2013
+
2014
+ const missingTargetIds = this.getProtocolV2MissingTargetIds(
2015
+ statusTargets,
2016
+ expectedTargetIds
2017
+ );
2018
+ if (missingTargetIds.length > 0) {
2019
+ const now = Date.now();
2020
+ const missingKey = missingTargetIds.join(',');
2021
+ if (missingTargetStatusSince === undefined || missingTargetStatusKey !== missingKey) {
2022
+ missingTargetStatusSince = now;
2023
+ missingTargetStatusKey = missingKey;
2024
+ } else if (
2025
+ now - missingTargetStatusSince >=
2026
+ PROTOCOL_V2_MISSING_TARGET_STATUS_GRACE_TIMEOUT
2027
+ ) {
2028
+ const reportedTargetIds = statusTargets
2029
+ .map(target => normalizeProtocolV2TargetId(target.target_id))
2030
+ .filter((targetId): targetId is number => targetId !== undefined);
2031
+ throw ERRORS.TypedError(
2032
+ HardwareErrorCode.FirmwareError,
2033
+ `Protocol V2 firmware status is missing requested records: targetIds=${missingKey} reportedTargetIds=${reportedTargetIds.join(
2034
+ ','
2035
+ )}`
2036
+ );
2037
+ }
2038
+ lastError = new Error(
2039
+ `Protocol V2 firmware status is temporarily missing targetIds=${missingKey}`
2040
+ );
2041
+ } else {
2042
+ resetMissingTargetStatusGrace();
2043
+ lastError = new Error('Protocol V2 firmware targets are still installing');
2044
+ }
1890
2045
  } catch (error) {
1891
2046
  if (isProtocolV2TerminalInstallStatusError(error)) {
1892
2047
  throw error;
1893
2048
  }
2049
+ // Only consecutive full status dumps can prove that a record is absent.
2050
+ // A reboot or transport interruption starts a fresh grace window.
2051
+ resetMissingTargetStatusGrace();
1894
2052
  // App firmware does not register DeviceFirmwareUpdateStatusGet. Treat the
1895
2053
  // missing endpoint as completion only after the runtime probe confirms App mode.
1896
2054
  if (isProtocolV2FirmwareStatusEndpointUnavailable(error)) {
1897
- if (await this.probeProtocolV2NormalMode(deviceInfo)) {
2055
+ if (!currentDeviceInfo) {
2056
+ throw ERRORS.TypedError(
2057
+ HardwareErrorCode.RuntimeError,
2058
+ 'Protocol V2 device identity is unavailable during install polling'
2059
+ );
2060
+ }
2061
+ if (await this.probeProtocolV2NormalMode(currentDeviceInfo)) {
1898
2062
  Log.log(
1899
2063
  '[FirmwareUpdateV4] firmware status endpoint unavailable after confirmed App reboot'
1900
2064
  );
2065
+ this.postProgressMessage(100, 'installingFirmware');
1901
2066
  return;
1902
2067
  }
1903
2068
  lastError = new Error(
1904
2069
  'Protocol V2 firmware status endpoint is unavailable while the device remains in loader mode'
1905
2070
  );
1906
2071
  } else {
2072
+ shouldReconnect = true;
2073
+ deviceInfo = undefined;
1907
2074
  lastError = error;
1908
2075
  Log.log(
1909
2076
  '[FirmwareUpdateV4] DeviceFirmwareUpdateStatusGet unavailable during install: ',
@@ -1925,6 +2092,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1925
2092
  ) {
1926
2093
  throw error;
1927
2094
  }
2095
+ shouldReconnect = true;
2096
+ deviceInfo = undefined;
2097
+ resetMissingTargetStatusGrace();
1928
2098
  Log.log('Protocol V2 firmware install device readiness probe failed: ', error);
1929
2099
  }
1930
2100
  await wait(1000);
@@ -1932,15 +2102,23 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1932
2102
 
1933
2103
  throw ERRORS.TypedError(
1934
2104
  HardwareErrorCode.RuntimeError,
1935
- `Protocol V2 firmware update status timeout: ${this.normalizeErrorMessage(lastError)}`
2105
+ `Protocol V2 firmware update status not complete within ${
2106
+ PROTOCOL_V2_INSTALL_TIMEOUT / 1000
2107
+ }s: ${this.normalizeErrorMessage(lastError)}`
1936
2108
  );
1937
2109
  }
1938
2110
 
1939
2111
  private async exitProtocolV2BootloaderToNormal() {
1940
2112
  await this.reconnectProtocolV2Device();
1941
- await this.verifyProtocolV2ReconnectIdentity();
1942
- // The connection may still be in bootloader. Request a Normal reboot directly;
1943
- // repeating it after an automatic App reboot is idempotent.
2113
+ const deviceInfo = await this.verifyProtocolV2ReconnectIdentity();
2114
+ try {
2115
+ if (await this.probeProtocolV2NormalMode(deviceInfo)) {
2116
+ Log.log('[FirmwareUpdateV4] device already returned to App mode; skip Normal reboot');
2117
+ return;
2118
+ }
2119
+ } catch (error) {
2120
+ Log.log('[FirmwareUpdateV4] unable to confirm App mode before Normal reboot: ', error);
2121
+ }
1944
2122
  await this.protocolV2Reboot(DeviceRebootType.Normal);
1945
2123
  this.protocolV2ExecutionInLoader = false;
1946
2124
  }
@@ -1958,6 +2136,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1958
2136
  PROTOCOL_V2_FINAL_RECONNECT_TIMEOUT
1959
2137
  );
1960
2138
 
2139
+ return this.getProtocolV2VersionResult(features);
2140
+ }
2141
+
2142
+ private getProtocolV2VersionResult(features: Features) {
1961
2143
  const bootloaderVersion = getDeviceBootloaderVersion(features).join('.');
1962
2144
  const bleVersion = getDeviceBLEFirmwareVersion(features).join('.');
1963
2145
  const firmwareVersion = getDeviceFirmwareVersion(features).join('.');
@@ -1989,13 +2171,17 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
1989
2171
  private async waitForProtocolV2ReconnectAndFeatures(timeout: number) {
1990
2172
  const startTime = Date.now();
1991
2173
  let lastError: unknown;
2174
+ let shouldReconnect = true;
1992
2175
 
1993
2176
  while (Date.now() - startTime < timeout) {
1994
2177
  try {
1995
- await this.reconnectProtocolV2Device();
2178
+ if (shouldReconnect) {
2179
+ await this.reconnectProtocolV2Device();
2180
+ shouldReconnect = false;
2181
+ }
1996
2182
  const deviceInfo = await requestProtocolV2DeviceInfo({
1997
2183
  commands: this.device.getCommands(),
1998
- timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
2184
+ timeoutMs: this.getProtocolV2DeviceInfoTimeout(),
1999
2185
  // Completion needs target versions only; keep scope aligned with the request.
2000
2186
  request: PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST,
2001
2187
  });
@@ -2004,21 +2190,22 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2004
2190
  deviceInfo,
2005
2191
  PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT
2006
2192
  );
2007
- if (features.mode !== 'normal' || features.bootloaderMode) {
2008
- throw ERRORS.TypedError(
2009
- HardwareErrorCode.DeviceNotFound,
2010
- 'Protocol V2 device is still in bootloader mode'
2011
- );
2193
+ if (features.mode === 'normal' && !features.bootloaderMode) {
2194
+ return features;
2012
2195
  }
2013
- return features;
2196
+ lastError = ERRORS.TypedError(
2197
+ HardwareErrorCode.DeviceNotFound,
2198
+ 'Protocol V2 device is still in bootloader mode'
2199
+ );
2014
2200
  } catch (error) {
2015
2201
  if (this.isProtocolV2ReconnectIdentityError(error)) {
2016
2202
  throw error;
2017
2203
  }
2204
+ shouldReconnect = true;
2018
2205
  lastError = error;
2019
2206
  Log.log('Protocol V2 normal mode not ready, polling Ping: ', error);
2020
- await wait(1000);
2021
2207
  }
2208
+ await wait(1000);
2022
2209
  }
2023
2210
 
2024
2211
  throw ERRORS.TypedError(
@@ -2042,9 +2229,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2042
2229
  DataManager.isBrowserWebUsb(DataManager.getSettings('env')) &&
2043
2230
  devicesDescriptor.length === 1
2044
2231
  ) {
2232
+ const descriptor = devicesDescriptor[0];
2233
+ if (!this.protocolV2ExpectedSerialNumber && descriptor.path !== this.protocolV2ExpectedPath) {
2234
+ throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
2235
+ }
2045
2236
  this.device.updateDescriptor(
2046
2237
  {
2047
- ...devicesDescriptor[0],
2238
+ ...descriptor,
2048
2239
  protocolType: PROTOCOL_V2_CONNECT_PROTOCOL,
2049
2240
  },
2050
2241
  true
@@ -2055,20 +2246,29 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2055
2246
  return;
2056
2247
  }
2057
2248
 
2058
- // App and bootloader serials may differ temporarily. During V4 reconnect, accept
2059
- // only a uniquely enumerated device instead of repeatedly using the old App path.
2249
+ // Reinitialize every USB descriptor so DeviceInfo can provide the physical serial.
2250
+ // Enumeration order is not stable and must never decide the reconnect target.
2060
2251
  const { deviceList } = await DevicePool.getDevices(devicesDescriptor, undefined, {
2061
2252
  connectProtocol: PROTOCOL_V2_CONNECT_PROTOCOL,
2062
2253
  });
2063
- if (deviceList.length !== 1) {
2254
+ const expectedSerialNumber = this.protocolV2ExpectedSerialNumber?.trim();
2255
+ const expectedPath = this.protocolV2ExpectedPath?.trim();
2256
+ const reconnectDevice = deviceList.find(candidate => {
2257
+ const candidateSerialNumber = candidate.getCurrentSerialNo?.().trim();
2258
+ if (expectedSerialNumber && candidateSerialNumber) {
2259
+ return candidateSerialNumber === expectedSerialNumber;
2260
+ }
2261
+ return !!expectedPath && candidate.getConnectId() === expectedPath;
2262
+ });
2263
+ if (!reconnectDevice) {
2064
2264
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
2065
2265
  }
2066
2266
 
2067
2267
  Log.debug(
2068
- 'Protocol V2 firmware reconnect using single enumerated device:',
2069
- deviceList[0].getConnectId()
2268
+ 'Protocol V2 firmware reconnect using matched device:',
2269
+ reconnectDevice.getConnectId()
2070
2270
  );
2071
- this.device.updateFromCache(deviceList[0]);
2271
+ this.device.updateFromCache(reconnectDevice);
2072
2272
  await this.ensureProtocolV2DeviceAcquired();
2073
2273
  this.device.commands.disposed = false;
2074
2274
  this.device.getCommands().mainId = this.device.mainId ?? '';
@@ -2101,17 +2301,22 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
2101
2301
  progress: number | null
2102
2302
  ): Promise<TypedResponseMessage<'FilesystemFile'>> {
2103
2303
  const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
2104
- const writeRes = await typedCall('FilesystemFileWrite', 'FilesystemFile', {
2105
- file: {
2106
- path: filePath,
2107
- offset,
2108
- total_size: totalFileSize,
2109
- data: chunk,
2304
+ const writeRes = await typedCall(
2305
+ 'FilesystemFileWrite',
2306
+ 'FilesystemFile',
2307
+ {
2308
+ file: {
2309
+ path: filePath,
2310
+ offset,
2311
+ total_size: totalFileSize,
2312
+ data: chunk,
2313
+ },
2314
+ overwrite,
2315
+ append: false,
2316
+ ui_percentage: progress ?? undefined,
2110
2317
  },
2111
- overwrite,
2112
- append: false,
2113
- ui_percentage: progress ?? undefined,
2114
- });
2318
+ { writeWithResponse: true }
2319
+ );
2115
2320
  if (writeRes.type !== 'FilesystemFile') {
2116
2321
  if ((writeRes as any).type === 'CallMethodError') {
2117
2322
  if (((writeRes as any).message.error ?? '').indexOf(SESSION_ERROR) > -1) {