@onekeyfe/hd-core 1.2.0-alpha.1 → 1.2.0-alpha.11

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 (210) hide show
  1. package/__tests__/DeviceCommands.test.ts +25 -0
  2. package/__tests__/RequestQueue.test.ts +34 -0
  3. package/__tests__/connectSettings.test.ts +19 -0
  4. package/__tests__/device-wallet-session-store.test.ts +131 -0
  5. package/__tests__/preInitialize.test.ts +33 -0
  6. package/__tests__/pro2Wallpaper.test.ts +50 -0
  7. package/__tests__/protocol-v2-firmware-targets.test.ts +19 -0
  8. package/__tests__/protocol-v2.test.ts +3253 -1605
  9. package/dist/api/BaseMethod.d.ts +4 -0
  10. package/dist/api/BaseMethod.d.ts.map +1 -1
  11. package/dist/api/ClearSessionCache.d.ts +9 -0
  12. package/dist/api/ClearSessionCache.d.ts.map +1 -0
  13. package/dist/api/FileRead.d.ts.map +1 -1
  14. package/dist/api/FileWrite.d.ts.map +1 -1
  15. package/dist/api/FirmwareUpdateV4.d.ts +20 -3
  16. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  17. package/dist/api/GetPassphraseState.d.ts +1 -6
  18. package/dist/api/GetPassphraseState.d.ts.map +1 -1
  19. package/dist/api/UploadPortfolio.d.ts +18 -0
  20. package/dist/api/UploadPortfolio.d.ts.map +1 -0
  21. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +1 -0
  22. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  23. package/dist/api/firmware/getBinary.d.ts +4 -0
  24. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  25. package/dist/api/firmware/progressThrottle.d.ts +3 -0
  26. package/dist/api/firmware/progressThrottle.d.ts.map +1 -0
  27. package/dist/api/index.d.ts +13 -10
  28. package/dist/api/index.d.ts.map +1 -1
  29. package/dist/api/protocol-v2/DeviceFactoryInfoGet.d.ts +6 -0
  30. package/dist/api/protocol-v2/DeviceFactoryInfoGet.d.ts.map +1 -0
  31. package/dist/api/protocol-v2/DeviceFactoryInfoSet.d.ts +7 -0
  32. package/dist/api/protocol-v2/DeviceFactoryInfoSet.d.ts.map +1 -0
  33. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts +1 -1
  34. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts.map +1 -1
  35. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts +3 -2
  36. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts.map +1 -1
  37. package/dist/api/protocol-v2/{DeviceGetDeviceInfo.d.ts → DeviceInfoGet.d.ts} +10 -10
  38. package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -0
  39. package/dist/api/protocol-v2/DeviceSessionGet.d.ts +6 -0
  40. package/dist/api/protocol-v2/DeviceSessionGet.d.ts.map +1 -0
  41. package/dist/api/protocol-v2/DeviceSettingsGet.d.ts +6 -0
  42. package/dist/api/protocol-v2/DeviceSettingsGet.d.ts.map +1 -0
  43. package/dist/api/protocol-v2/DeviceSettingsPageShow.d.ts +16 -0
  44. package/dist/api/protocol-v2/DeviceSettingsPageShow.d.ts.map +1 -0
  45. package/dist/api/protocol-v2/DeviceSettingsSet.d.ts +12 -0
  46. package/dist/api/protocol-v2/DeviceSettingsSet.d.ts.map +1 -0
  47. package/dist/api/protocol-v2/DeviceStatusGet.d.ts +6 -0
  48. package/dist/api/protocol-v2/DeviceStatusGet.d.ts.map +1 -0
  49. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts +26 -0
  50. package/dist/api/protocol-v2/DeviceUploadWallpaper.d.ts.map +1 -0
  51. package/dist/api/protocol-v2/FilesystemFormat.d.ts.map +1 -1
  52. package/dist/api/protocol-v2/{FilesystemFixPermission.d.ts → FilesystemPermissionFix.d.ts} +2 -2
  53. package/dist/api/protocol-v2/FilesystemPermissionFix.d.ts.map +1 -0
  54. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts +6 -0
  55. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -0
  56. package/dist/api/protocol-v2/helpers.d.ts +27 -18
  57. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  58. package/dist/api/ton/TonSignData.d.ts +1 -5
  59. package/dist/api/ton/TonSignData.d.ts.map +1 -1
  60. package/dist/api/tron/TronSignMessage.d.ts +0 -1
  61. package/dist/api/tron/TronSignMessage.d.ts.map +1 -1
  62. package/dist/api/tron/TronSignTransaction.d.ts +0 -1
  63. package/dist/api/tron/TronSignTransaction.d.ts.map +1 -1
  64. package/dist/core/RequestQueue.d.ts +2 -0
  65. package/dist/core/RequestQueue.d.ts.map +1 -1
  66. package/dist/core/index.d.ts +1 -0
  67. package/dist/core/index.d.ts.map +1 -1
  68. package/dist/data-manager/DataManager.d.ts +1 -1
  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 +6 -3
  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/DeviceWalletSessionStore.d.ts +15 -0
  76. package/dist/device/DeviceWalletSessionStore.d.ts.map +1 -0
  77. package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
  78. package/dist/deviceProfile/buildDeviceProfile.d.ts.map +1 -1
  79. package/dist/events/call.d.ts +7 -0
  80. package/dist/events/call.d.ts.map +1 -1
  81. package/dist/events/core.d.ts +2 -2
  82. package/dist/events/core.d.ts.map +1 -1
  83. package/dist/events/iframe.d.ts +1 -0
  84. package/dist/events/iframe.d.ts.map +1 -1
  85. package/dist/index.d.ts +166 -70
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +2659 -1750
  88. package/dist/inject.d.ts +3 -2
  89. package/dist/inject.d.ts.map +1 -1
  90. package/dist/lowLevelInject.d.ts +2 -1
  91. package/dist/lowLevelInject.d.ts.map +1 -1
  92. package/dist/protocols/protocol-v2/features.d.ts +12 -14
  93. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  94. package/dist/protocols/protocol-v2/firmware.d.ts +11 -9
  95. package/dist/protocols/protocol-v2/firmware.d.ts.map +1 -1
  96. package/dist/protocols/protocol-v2/index.d.ts +1 -0
  97. package/dist/protocols/protocol-v2/index.d.ts.map +1 -1
  98. package/dist/protocols/protocol-v2/unlockRetry.d.ts +9 -0
  99. package/dist/protocols/protocol-v2/unlockRetry.d.ts.map +1 -0
  100. package/dist/protocols/protocol-v2/walletSession.d.ts +13 -0
  101. package/dist/protocols/protocol-v2/walletSession.d.ts.map +1 -0
  102. package/dist/topLevelInject.d.ts.map +1 -1
  103. package/dist/types/api/firmwareUpdate.d.ts +6 -6
  104. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  105. package/dist/types/api/getDeviceInfo.d.ts +2 -0
  106. package/dist/types/api/getDeviceInfo.d.ts.map +1 -1
  107. package/dist/types/api/getPassphraseState.d.ts +2 -10
  108. package/dist/types/api/getPassphraseState.d.ts.map +1 -1
  109. package/dist/types/api/index.d.ts +18 -12
  110. package/dist/types/api/index.d.ts.map +1 -1
  111. package/dist/types/api/protocolV2.d.ts +31 -17
  112. package/dist/types/api/protocolV2.d.ts.map +1 -1
  113. package/dist/types/api/sessionCache.d.ts +10 -0
  114. package/dist/types/api/sessionCache.d.ts.map +1 -0
  115. package/dist/types/device.d.ts +1 -0
  116. package/dist/types/device.d.ts.map +1 -1
  117. package/dist/types/params.d.ts +1 -0
  118. package/dist/types/params.d.ts.map +1 -1
  119. package/dist/types/settings.d.ts +21 -2
  120. package/dist/types/settings.d.ts.map +1 -1
  121. package/dist/utils/deviceFeaturesUtils.d.ts +2 -2
  122. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  123. package/dist/utils/index.d.ts +2 -0
  124. package/dist/utils/index.d.ts.map +1 -1
  125. package/dist/utils/patch.d.ts +1 -1
  126. package/dist/utils/patch.d.ts.map +1 -1
  127. package/dist/utils/pro2Wallpaper.d.ts +12 -0
  128. package/dist/utils/pro2Wallpaper.d.ts.map +1 -0
  129. package/package.json +4 -4
  130. package/src/api/BaseMethod.ts +18 -0
  131. package/src/api/ClearSessionCache.ts +28 -0
  132. package/src/api/FileRead.ts +16 -3
  133. package/src/api/FileWrite.ts +22 -1
  134. package/src/api/FirmwareUpdateV4.ts +1006 -265
  135. package/src/api/GetOnekeyFeatures.ts +1 -1
  136. package/src/api/GetPassphraseState.ts +9 -15
  137. package/src/api/UploadPortfolio.ts +37 -0
  138. package/src/api/device/DeviceRebootToBoardloader.ts +4 -4
  139. package/src/api/device/DeviceRebootToBootloader.ts +4 -4
  140. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +18 -4
  141. package/src/api/firmware/progressThrottle.ts +44 -0
  142. package/src/api/index.ts +13 -10
  143. package/src/api/protocol-v2/{FactoryGetDeviceInfo.ts → DeviceFactoryInfoGet.ts} +3 -3
  144. package/src/api/protocol-v2/DeviceFactoryInfoSet.ts +32 -0
  145. package/src/api/protocol-v2/DeviceFirmwareUpdate.ts +3 -5
  146. package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +39 -5
  147. package/src/api/protocol-v2/{DeviceGetDeviceInfo.ts → DeviceInfoGet.ts} +52 -22
  148. package/src/api/protocol-v2/DeviceReboot.ts +1 -1
  149. package/src/api/protocol-v2/DeviceSessionGet.ts +15 -0
  150. package/src/api/protocol-v2/{DeviceGetOnboardingStatus.ts → DeviceSettingsGet.ts} +3 -3
  151. package/src/api/protocol-v2/DeviceSettingsPageShow.ts +59 -0
  152. package/src/api/protocol-v2/DeviceSettingsSet.ts +39 -0
  153. package/src/api/protocol-v2/DeviceStatusGet.ts +14 -0
  154. package/src/api/protocol-v2/DeviceUploadWallpaper.ts +160 -0
  155. package/src/api/protocol-v2/FilesystemFormat.ts +4 -1
  156. package/src/api/protocol-v2/{FilesystemFixPermission.ts → FilesystemPermissionFix.ts} +2 -2
  157. package/src/api/protocol-v2/{GetProtoVersion.ts → ProtocolInfoRequest.ts} +2 -2
  158. package/src/api/protocol-v2/helpers.ts +71 -47
  159. package/src/api/ton/TonSignData.ts +1 -5
  160. package/src/api/tron/TronSignMessage.ts +0 -1
  161. package/src/api/tron/TronSignTransaction.ts +0 -1
  162. package/src/core/RequestQueue.ts +19 -0
  163. package/src/core/index.ts +29 -6
  164. package/src/data/messages/messages-protocol-v2.json +760 -1110
  165. package/src/data/messages/messages.json +8 -0
  166. package/src/data-manager/DataManager.ts +7 -0
  167. package/src/data-manager/connectSettings.ts +2 -4
  168. package/src/device/Device.ts +103 -70
  169. package/src/device/DeviceCommands.ts +9 -3
  170. package/src/device/DeviceWalletSessionStore.ts +78 -0
  171. package/src/deviceProfile/buildDeviceFeatures.ts +60 -63
  172. package/src/deviceProfile/buildDeviceProfile.ts +77 -58
  173. package/src/events/call.ts +6 -0
  174. package/src/events/core.ts +2 -0
  175. package/src/events/iframe.ts +1 -0
  176. package/src/index.ts +4 -0
  177. package/src/inject.ts +26 -16
  178. package/src/lowLevelInject.ts +3 -0
  179. package/src/protocols/protocol-v2/features.ts +26 -63
  180. package/src/protocols/protocol-v2/firmware.ts +12 -36
  181. package/src/protocols/protocol-v2/index.ts +1 -0
  182. package/src/protocols/protocol-v2/unlockRetry.ts +46 -0
  183. package/src/protocols/protocol-v2/walletSession.ts +92 -0
  184. package/src/topLevelInject.ts +2 -0
  185. package/src/types/api/firmwareUpdate.ts +28 -20
  186. package/src/types/api/getDeviceInfo.ts +2 -0
  187. package/src/types/api/getPassphraseState.ts +2 -11
  188. package/src/types/api/index.ts +29 -21
  189. package/src/types/api/protocolV2.ts +74 -35
  190. package/src/types/api/sessionCache.ts +14 -0
  191. package/src/types/device.ts +1 -0
  192. package/src/types/params.ts +4 -0
  193. package/src/types/settings.ts +42 -35
  194. package/src/utils/deviceFeaturesUtils.ts +46 -20
  195. package/src/utils/index.ts +6 -0
  196. package/src/utils/pro2Wallpaper.ts +111 -0
  197. package/dist/api/protocol-v2/DevReboot.d.ts +0 -7
  198. package/dist/api/protocol-v2/DevReboot.d.ts.map +0 -1
  199. package/dist/api/protocol-v2/DeviceGetDeviceInfo.d.ts.map +0 -1
  200. package/dist/api/protocol-v2/DeviceGetOnboardingStatus.d.ts +0 -6
  201. package/dist/api/protocol-v2/DeviceGetOnboardingStatus.d.ts.map +0 -1
  202. package/dist/api/protocol-v2/FactoryDeviceInfoSettings.d.ts +0 -7
  203. package/dist/api/protocol-v2/FactoryDeviceInfoSettings.d.ts.map +0 -1
  204. package/dist/api/protocol-v2/FactoryGetDeviceInfo.d.ts +0 -6
  205. package/dist/api/protocol-v2/FactoryGetDeviceInfo.d.ts.map +0 -1
  206. package/dist/api/protocol-v2/FilesystemFixPermission.d.ts.map +0 -1
  207. package/dist/api/protocol-v2/GetProtoVersion.d.ts +0 -6
  208. package/dist/api/protocol-v2/GetProtoVersion.d.ts.map +0 -1
  209. package/src/api/protocol-v2/DevReboot.ts +0 -24
  210. package/src/api/protocol-v2/FactoryDeviceInfoSettings.ts +0 -29
@@ -1,6 +1,6 @@
1
1
  import { ERRORS, HardwareError, HardwareErrorCode, wait } from '@onekeyfe/hd-shared';
2
2
  import {
3
- DevRebootType,
3
+ DeviceRebootType,
4
4
  PROTOCOL_V2_BLE_FILE_CHUNK_SIZE,
5
5
  PROTOCOL_V2_WEBUSB_FILE_CHUNK_SIZE,
6
6
  } from '@onekeyfe/hd-transport';
@@ -15,14 +15,13 @@ import {
15
15
  getFirmwareType,
16
16
  getLogger,
17
17
  } from '../utils';
18
- import { getBinary, getSysResourceBinary } from './firmware/getBinary';
18
+ import { getSysResourceBinary } from './firmware/getBinary';
19
19
  import { DataManager } from '../data-manager';
20
20
  import { FirmwareUpdateBaseMethod } from './firmware/FirmwareUpdateBaseMethod';
21
21
  import { DevicePool } from '../device/DevicePool';
22
22
  import {
23
23
  PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST,
24
24
  ProtocolV2FirmwareTargetType,
25
- protocolV2FileNameToTargetId,
26
25
  } from '../protocols/protocol-v2';
27
26
  import { requestProtocolV2DeviceInfo } from '../protocols/protocol-v2/features';
28
27
  import {
@@ -31,35 +30,197 @@ import {
31
30
  isProtocolV2DeviceDisconnectedError,
32
31
  } from './protocol-v2/helpers';
33
32
 
34
- import type { FirmwareUpdateV4Params } from '../types/api/firmwareUpdate';
33
+ import type { FirmwareUpdateV4Params, FirmwareUpdateV4Target } from '../types/api/firmwareUpdate';
35
34
  import type { EFirmwareType } from '@onekeyfe/hd-shared';
36
35
  import type { PROTO } from '../constants';
37
36
  import type { TypedResponseMessage } from '../device/DeviceCommands';
38
- import type { Features } from '../types';
37
+ import type {
38
+ Features,
39
+ IFirmwareReleaseInfo,
40
+ IProtocolV2FirmwareComponent,
41
+ IVersionArray,
42
+ } from '../types';
39
43
 
40
44
  const Log = getLogger(LoggerNames.Method);
41
45
 
42
46
  const SESSION_ERROR = 'session not found';
43
47
  const PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT = 60 * 1000;
44
48
  const PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT = 5 * 1000;
49
+ const PROTOCOL_V2_START_UPDATE_TIMEOUT = 3 * 60 * 1000;
45
50
  const PROTOCOL_V2_INSTALL_TIMEOUT = 5 * 60 * 1000;
46
- const PROTOCOL_V2_TARGET_STATUS_FINISHED = 0;
51
+ const PROTOCOL_V2_TARGET_STATUS_PENDING = 0;
47
52
  const PROTOCOL_V2_TARGET_STATUS_IN_PROGRESS = 1;
48
- const PROTOCOL_V2_TARGET_STATUS_FAILED = 2;
53
+ const PROTOCOL_V2_TARGET_STATUS_FINISHED = 2;
54
+ const PROTOCOL_V2_TARGET_STATUS_FAILED_MIN = 3;
49
55
  const PROTOCOL_V2_CONNECT_PROTOCOL = 'V2';
50
- const PROTOCOL_V2_FIRMWARE_STAGING_VOLUME = 'vol1:';
56
+ const PROTOCOL_V2_FIRMWARE_STAGING_VOLUME = 'vol0:/';
51
57
  const PROTOCOL_V2_MIN_FILE_CHUNK_SIZE = 64;
58
+ const PROTOCOL_V2_CONNECT_RETRY_COUNT = 10;
59
+ const PROTOCOL_V2_CONNECT_POLL_INTERVAL = 500;
60
+ const PROTOCOL_V2_CONNECT_SINGLE_TIMEOUT = 75 * 1000;
61
+ const PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT = 60 * 1000;
62
+ const PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT = 3;
63
+ const PROTOCOL_V2_OKPP_HEADER_SIZE = 0x52a0;
64
+ const PROTOCOL_V2_OKPP_PAYLOAD_HASH_OFFSET = 0x200;
65
+ const PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET = 0x240;
66
+ const PROTOCOL_V2_OKPP_HASH_SIZE = 64;
67
+
68
+ const getProtocolV2DeviceTransferProgress = (
69
+ bytesBeforeChunk: number,
70
+ bytesAfterChunk: number,
71
+ totalBytes: number
72
+ ) => {
73
+ if (!Number.isFinite(totalBytes) || totalBytes <= 0) {
74
+ return 100;
75
+ }
76
+ if (bytesBeforeChunk <= 0 && bytesAfterChunk < totalBytes) {
77
+ return 0;
78
+ }
79
+ if (bytesAfterChunk >= totalBytes) {
80
+ return 100;
81
+ }
82
+ return Math.min(Math.max(Math.ceil((bytesAfterChunk / totalBytes) * 100), 1), 99);
83
+ };
84
+
85
+ const formatProtocolV2TransferSpeed = (bytes: number, elapsedMs: number) => {
86
+ const safeElapsedMs = Math.max(elapsedMs, 1);
87
+ return (bytes / 1024 / (safeElapsedMs / 1000)).toFixed(2);
88
+ };
52
89
 
53
90
  type ProtocolV2FirmwareUpdateStatusTarget = {
54
- target_id: number;
55
- status: number;
91
+ target_id: number | string;
92
+ status?: number | string;
93
+ payload_version?: number;
94
+ path?: string;
56
95
  };
57
96
 
58
97
  type ProtocolV2FirmwareUpdateStartResponse =
59
98
  | TypedResponseMessage<'Success'>
60
- | TypedResponseMessage<'DevFirmwareUpdateStatus'>
99
+ | TypedResponseMessage<'DeviceFirmwareUpdateStatus'>
61
100
  | undefined;
62
101
 
102
+ type ProtocolV2TargetBinary = { fileName: string; binary: ArrayBuffer; targetId: number };
103
+ type ProtocolV2InstallItem = ProtocolV2TargetBinary & {
104
+ kind: ProtocolV2RemoteComponentTarget['kind'];
105
+ };
106
+ type ProtocolV2InstallTarget = ProtocolV2InstallItem & {
107
+ path: string;
108
+ };
109
+
110
+ type ProtocolV2FileTransferParams = PROTO.FirmwareUpload & {
111
+ filePath: string;
112
+ processedSize?: number;
113
+ totalSize?: number;
114
+ onTransferredBytes?: (transferredBytes: number) => void;
115
+ };
116
+
117
+ /** RESC bundle okpkg(FileWrite 直写模式),每个独立同步到 devicePath */
118
+ type ProtocolV2ResourceBundleBinary = {
119
+ name: string;
120
+ binary: ArrayBuffer;
121
+ devicePath: string;
122
+ /** 远端配置模式下的下载 URL(手动模式不填) */
123
+ url?: string;
124
+ version?: IVersionArray;
125
+ payloadHash?: string;
126
+ headerHash?: string;
127
+ };
128
+
129
+ type ProtocolV2RemoteComponentBinary = ProtocolV2RemoteComponentTarget & {
130
+ binary: ArrayBuffer;
131
+ };
132
+
133
+ type ProtocolV2RemoteComponentTarget = {
134
+ fileName: string;
135
+ targetId: number;
136
+ kind: 'bootloader' | 'firmware';
137
+ };
138
+
139
+ type ProtocolV2OkppHeader = {
140
+ type: string;
141
+ version: IVersionArray;
142
+ payloadHash: string;
143
+ headerHash: string;
144
+ };
145
+
146
+ const PROTOCOL_V2_REMOTE_COMPONENT_TARGETS: Readonly<
147
+ Record<string, ProtocolV2RemoteComponentTarget>
148
+ > = {
149
+ BOOTLOADER: {
150
+ fileName: 'bootloader.bin',
151
+ targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER,
152
+ kind: 'bootloader',
153
+ },
154
+ APPLICATION_P1: {
155
+ fileName: 'application_p1.bin',
156
+ targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1,
157
+ kind: 'firmware',
158
+ },
159
+ APPLICATION_P2: {
160
+ fileName: 'application_p2.bin',
161
+ targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2,
162
+ kind: 'firmware',
163
+ },
164
+ COPROCESSOR: {
165
+ fileName: 'coprocessor.bin',
166
+ targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_COPROCESSOR,
167
+ kind: 'firmware',
168
+ },
169
+ SE01: {
170
+ fileName: 'se01.bin',
171
+ targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE01,
172
+ kind: 'firmware',
173
+ },
174
+ SE02: {
175
+ fileName: 'se02.bin',
176
+ targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE02,
177
+ kind: 'firmware',
178
+ },
179
+ SE03: {
180
+ fileName: 'se03.bin',
181
+ targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE03,
182
+ kind: 'firmware',
183
+ },
184
+ SE04: {
185
+ fileName: 'se04.bin',
186
+ targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE04,
187
+ kind: 'firmware',
188
+ },
189
+ };
190
+
191
+ const PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID = new Map<number, FirmwareUpdateV4Target>([
192
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER, 'boot'],
193
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1, 'app_v1'],
194
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2, 'app_v2'],
195
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_COPROCESSOR, 'coprocessor'],
196
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE01, 'se01'],
197
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE02, 'se02'],
198
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE03, 'se03'],
199
+ [ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE04, 'se04'],
200
+ ]);
201
+
202
+ const PROTOCOL_V2_ROMLOADER_UNSUPPORTED_MESSAGE =
203
+ 'FW_MGMT_TARGET_ROMLOADER is not accepted by the current Pro2 bootloader update request. Flash romloader with the loader-specific flow instead of firmwareUpdateV4.';
204
+
205
+ // hd-transport 的历史 decode 行为会把单值 enum 输出为枚举名字符串;
206
+ // Protocol V2 沿用这个 SDK 语义,内部比较前再映射回固件协议数值。
207
+ const PROTOCOL_V2_TARGET_ID_BY_DECODED_NAME = new Map<string, number>(
208
+ Object.entries(ProtocolV2FirmwareTargetType).map(([key, value]) => [key, value])
209
+ );
210
+ const PROTOCOL_V2_TARGET_STATUS_BY_DECODED_NAME = new Map<string, number>([
211
+ ['FW_MGMT_UPDATER_TASK_STATUS_PENDING', PROTOCOL_V2_TARGET_STATUS_PENDING],
212
+ ['FW_MGMT_UPDATER_TASK_STATUS_IN_PROGRESS', PROTOCOL_V2_TARGET_STATUS_IN_PROGRESS],
213
+ ['FW_MGMT_UPDATER_TASK_STATUS_FINISHED', PROTOCOL_V2_TARGET_STATUS_FINISHED],
214
+ ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_FILE_NOT_FOUND', 3],
215
+ ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_FILE_READ', 4],
216
+ ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_FILE_WRITE', 5],
217
+ ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_VERIFY', 6],
218
+ ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_INSTALL', 7],
219
+ ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_ABORT', 8],
220
+ ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_BUSY', 9],
221
+ ['FW_MGMT_UPDATER_TASK_STATUS_FAILED_ENTRY_OUT_OF_BOUNDS', 10],
222
+ ]);
223
+
63
224
  const isProtocolV2ReconnectProbeError = (error: unknown) => {
64
225
  const message = getProtocolV2UnknownErrorText(error).toLowerCase();
65
226
  return (
@@ -73,19 +234,158 @@ const isProtocolV2PollingTransientError = (error: unknown) => {
73
234
  return (
74
235
  isProtocolV2DeviceDisconnectedError(error) ||
75
236
  isProtocolV2ReconnectProbeError(error) ||
76
- (message.includes('response timeout') && message.includes('devicegetfirmwareupdatestatus')) ||
237
+ message.includes('libusb_transfer_timed_out') ||
238
+ (message.includes('response timeout') && message.includes('devicefirmwareupdatestatusget')) ||
77
239
  message.includes('device not found') ||
78
240
  message.includes('transportnotfound')
79
241
  );
80
242
  };
81
243
 
244
+ const isProtocolV2StartUpdateTransientError = (error: unknown) => {
245
+ const message = getProtocolV2UnknownErrorText(error).toLowerCase();
246
+ return (
247
+ isProtocolV2DeviceDisconnectedError(error) ||
248
+ isProtocolV2ReconnectProbeError(error) ||
249
+ message.includes('libusb_transfer_timed_out') ||
250
+ (message.includes('response timeout') && message.includes('devicefirmwareupdaterequest'))
251
+ );
252
+ };
253
+
254
+ const isProtocolV2TargetStatusFinished = (status: ProtocolV2FirmwareUpdateStatusTarget['status']) =>
255
+ normalizeProtocolV2TargetStatus(status) === PROTOCOL_V2_TARGET_STATUS_FINISHED;
256
+
257
+ const isProtocolV2TargetStatusInProgress = (
258
+ status: ProtocolV2FirmwareUpdateStatusTarget['status']
259
+ ) =>
260
+ normalizeProtocolV2TargetStatus(status) === PROTOCOL_V2_TARGET_STATUS_PENDING ||
261
+ normalizeProtocolV2TargetStatus(status) === PROTOCOL_V2_TARGET_STATUS_IN_PROGRESS;
262
+
263
+ const isProtocolV2TargetStatusFailed = (status: ProtocolV2FirmwareUpdateStatusTarget['status']) => {
264
+ const normalizedStatus = normalizeProtocolV2TargetStatus(status);
265
+ return (
266
+ typeof normalizedStatus === 'number' && normalizedStatus >= PROTOCOL_V2_TARGET_STATUS_FAILED_MIN
267
+ );
268
+ };
269
+
270
+ const normalizeProtocolV2TargetId = (targetId: number | string) => {
271
+ if (typeof targetId === 'number') {
272
+ return targetId;
273
+ }
274
+ return PROTOCOL_V2_TARGET_ID_BY_DECODED_NAME.get(targetId);
275
+ };
276
+
277
+ const normalizeProtocolV2TargetStatus = (
278
+ status: ProtocolV2FirmwareUpdateStatusTarget['status']
279
+ ) => {
280
+ if (typeof status === 'number') {
281
+ return status;
282
+ }
283
+ if (typeof status === 'string') {
284
+ return PROTOCOL_V2_TARGET_STATUS_BY_DECODED_NAME.get(status);
285
+ }
286
+ return undefined;
287
+ };
288
+
289
+ const normalizeProtocolV2Hex = (value?: string) => value?.replace(/^0x/i, '').toLowerCase();
290
+
291
+ const versionArrayToNumber = (version?: IVersionArray) => {
292
+ if (!version) return undefined;
293
+ return version[0] * 0x10000 + version[1] * 0x100 + version[2];
294
+ };
295
+
296
+ const compareProtocolV2Versions = (current?: IVersionArray, target?: IVersionArray) => {
297
+ const currentNumber = versionArrayToNumber(current);
298
+ const targetNumber = versionArrayToNumber(target);
299
+ if (currentNumber === undefined || targetNumber === undefined) return undefined;
300
+ return currentNumber - targetNumber;
301
+ };
302
+
303
+ const bytesToHex = (bytes: Uint8Array) =>
304
+ Array.from(bytes)
305
+ .map(byte => byte.toString(16).padStart(2, '0'))
306
+ .join('');
307
+
308
+ const hexToProtocolV2Bytes = (hex: string) => {
309
+ const normalized = hex.replace(/^0x/i, '');
310
+ if (!normalized || normalized.length % 2 !== 0 || /[^0-9a-f]/i.test(normalized)) {
311
+ return new Uint8Array(0);
312
+ }
313
+ const bytes = new Uint8Array(normalized.length / 2);
314
+ for (let i = 0; i < bytes.length; i++) {
315
+ bytes[i] = Number.parseInt(normalized.slice(i * 2, i * 2 + 2), 16);
316
+ }
317
+ return bytes;
318
+ };
319
+
320
+ const toProtocolV2Bytes = (value: unknown): Uint8Array => {
321
+ if (value instanceof Uint8Array) return value;
322
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
323
+ if (ArrayBuffer.isView(value)) {
324
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
325
+ }
326
+ if (typeof value === 'string') return hexToProtocolV2Bytes(value);
327
+ return new Uint8Array(0);
328
+ };
329
+
330
+ const toProtocolV2FiniteNumber = (value: unknown) => {
331
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
332
+ if (typeof value === 'string') {
333
+ const numeric = Number(value);
334
+ return Number.isFinite(numeric) ? numeric : undefined;
335
+ }
336
+ if (value && typeof value === 'object') {
337
+ const longLike = value as { toNumber?: () => number };
338
+ if (typeof longLike.toNumber === 'function') {
339
+ const numeric = longLike.toNumber();
340
+ return Number.isFinite(numeric) ? numeric : undefined;
341
+ }
342
+ }
343
+ return undefined;
344
+ };
345
+
346
+ const readProtocolV2Ascii = (bytes: Uint8Array, offset: number, length: number) =>
347
+ Array.from(bytes.slice(offset, offset + length))
348
+ .map(byte => String.fromCharCode(byte))
349
+ .join('');
350
+
351
+ const parseProtocolV2OkppHeader = (bytes: Uint8Array): ProtocolV2OkppHeader | null => {
352
+ if (bytes.byteLength < PROTOCOL_V2_OKPP_HEADER_SIZE) return null;
353
+ if (readProtocolV2Ascii(bytes, 0, 4) !== 'OKPP') return null;
354
+
355
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
356
+ const headerLen = view.getUint32(0x0c, true);
357
+ if (headerLen !== PROTOCOL_V2_OKPP_HEADER_SIZE) return null;
358
+
359
+ const packedVersion = view.getUint32(0x10, true);
360
+ return {
361
+ type: readProtocolV2Ascii(bytes, 0x08, 4),
362
+ version: [
363
+ Math.floor(packedVersion / 0x10000) % 0x100,
364
+ Math.floor(packedVersion / 0x100) % 0x100,
365
+ packedVersion % 0x100,
366
+ ],
367
+ payloadHash: bytesToHex(
368
+ bytes.slice(
369
+ PROTOCOL_V2_OKPP_PAYLOAD_HASH_OFFSET,
370
+ PROTOCOL_V2_OKPP_PAYLOAD_HASH_OFFSET + PROTOCOL_V2_OKPP_HASH_SIZE
371
+ )
372
+ ),
373
+ headerHash: bytesToHex(
374
+ bytes.slice(
375
+ PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET,
376
+ PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET + PROTOCOL_V2_OKPP_HASH_SIZE
377
+ )
378
+ ),
379
+ };
380
+ };
381
+
82
382
  /**
83
383
  * FirmwareUpdateV4 is the complete Protocol V2 firmware update flow.
84
384
  *
85
385
  * It intentionally does not fall back to FirmwareUpdateV3/V1 behavior:
86
386
  * - upload uses FilesystemFileWrite
87
- * - install uses DevFirmwareUpdate
88
- * - completion reboots to normal, then polls Ping
387
+ * - install uses DeviceFirmwareUpdateRequest
388
+ * - completion waits for target status to finish, reboots to normal, then polls DeviceInfo
89
389
  */
90
390
  export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareUpdateV4Params> {
91
391
  init() {
@@ -96,15 +396,22 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
96
396
 
97
397
  const { payload } = this;
98
398
 
399
+ if (typeof payload.retryCount !== 'number') {
400
+ payload.retryCount = PROTOCOL_V2_CONNECT_RETRY_COUNT;
401
+ }
402
+ if (typeof payload.pollIntervalTime !== 'number') {
403
+ payload.pollIntervalTime = PROTOCOL_V2_CONNECT_POLL_INTERVAL;
404
+ }
405
+ if (typeof payload.timeout !== 'number') {
406
+ payload.timeout = PROTOCOL_V2_CONNECT_SINGLE_TIMEOUT;
407
+ }
408
+ if (typeof payload.protocolV2DeviceInfoTimeoutMs !== 'number') {
409
+ payload.protocolV2DeviceInfoTimeoutMs = PROTOCOL_V2_DEVICE_INFO_READY_TIMEOUT;
410
+ }
411
+
99
412
  validateParams(payload, [
100
- { name: 'bleVersion', type: 'array' },
101
- { name: 'bleBinary', type: 'buffer' },
102
413
  { name: 'chunkSize', type: 'number' },
103
- { name: 'firmwareVersion', type: 'array' },
104
- { name: 'firmwareBinary', type: 'buffer' },
105
- { name: 'resourceBinary', type: 'buffer' },
106
414
  { name: 'forcedUpdateRes', type: 'boolean' },
107
- { name: 'bootloaderVersion', type: 'array' },
108
415
  { name: 'bootloaderBinary', type: 'buffer' },
109
416
  { name: 'romloaderBinary', type: 'buffer' },
110
417
  { name: 'applicationP1Binary', type: 'buffer' },
@@ -115,16 +422,14 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
115
422
  { name: 'se03Binary', type: 'buffer' },
116
423
  { name: 'se04Binary', type: 'buffer' },
117
424
  { name: 'firmwareType', type: 'string' },
425
+ { name: 'targetsToUpdate', type: 'array', allowEmpty: true },
118
426
  { name: 'platform', type: 'string' },
427
+ { name: 'resourceBundleFiles', type: 'array', allowEmpty: true },
119
428
  ]);
120
429
 
121
430
  this.params = {
122
- bleBinary: payload.bleBinary,
123
431
  chunkSize: payload.chunkSize,
124
- firmwareBinary: payload.firmwareBinary,
125
432
  forcedUpdateRes: payload.forcedUpdateRes,
126
- bleVersion: payload.bleVersion,
127
- bootloaderVersion: payload.bootloaderVersion,
128
433
  bootloaderBinary: payload.bootloaderBinary,
129
434
  romloaderBinary: payload.romloaderBinary,
130
435
  applicationP1Binary: payload.applicationP1Binary,
@@ -134,9 +439,9 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
134
439
  se02Binary: payload.se02Binary,
135
440
  se03Binary: payload.se03Binary,
136
441
  se04Binary: payload.se04Binary,
137
- firmwareVersion: payload.firmwareVersion,
138
- resourceBinary: payload.resourceBinary,
442
+ resourceBundleFiles: payload.resourceBundleFiles,
139
443
  firmwareType: payload.firmwareType,
444
+ targetsToUpdate: payload.targetsToUpdate,
140
445
  platform: payload.platform,
141
446
  };
142
447
  }
@@ -174,35 +479,43 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
174
479
  const deviceFirmwareType = getFirmwareType(deviceFeatures);
175
480
  const firmwareType = this.params.firmwareType ?? deviceFirmwareType;
176
481
 
177
- let resourceBinary: ArrayBuffer | null = null;
178
- let fwBinaryMap: { fileName: string; binary: ArrayBuffer; targetId?: number }[] = [];
482
+ let fwBinaryMap: ProtocolV2TargetBinary[] = [];
179
483
  let bootloaderBinary: ArrayBuffer | null = null;
484
+ let installItems: ProtocolV2InstallItem[] | undefined;
180
485
  try {
181
486
  this.postTipMessage(FirmwareUpdateTipMessage.StartDownloadFirmware);
182
- resourceBinary = await this.prepareResourceBinary(firmwareType, deviceFeatures);
183
- fwBinaryMap = await this.prepareFirmwareAndBleBinary(firmwareType, deviceFeatures);
184
- bootloaderBinary = await this.prepareBootloaderBinary(firmwareType, deviceFeatures);
185
- // DevFirmwareTargetType 拆分的目标二进制(显式 target_id,不走文件名推断)
186
- fwBinaryMap.push(...this.collectExplicitTargetBinaries());
487
+ fwBinaryMap = this.collectExplicitTargetBinaries();
488
+ bootloaderBinary = this.prepareBootloaderBinary();
489
+ if (!this.hasExplicitProtocolV2Payload(fwBinaryMap)) {
490
+ const remoteBinaries = await this.prepareRemoteProtocolV2Binaries(
491
+ firmwareType,
492
+ deviceFeatures
493
+ );
494
+ bootloaderBinary = remoteBinaries.bootloaderBinary;
495
+ fwBinaryMap = remoteBinaries.fwBinaryMap;
496
+ installItems = remoteBinaries.installItems;
497
+ }
187
498
  this.postTipMessage(FirmwareUpdateTipMessage.FinishDownloadFirmware);
188
499
  } catch (err) {
189
500
  throw ERRORS.TypedError(HardwareErrorCode.FirmwareUpdateDownloadFailed, err.message ?? err);
190
501
  }
191
502
 
192
- if (!resourceBinary && !bootloaderBinary && fwBinaryMap.length === 0) {
503
+ const resourceBundles = this.prepareProtocolV2ResourceBundles(firmwareType, deviceFeatures);
504
+
505
+ if (!bootloaderBinary && fwBinaryMap.length === 0 && !resourceBundles?.length) {
193
506
  throw ERRORS.TypedError(
194
507
  HardwareErrorCode.FirmwareUpdateDownloadFailed,
195
508
  'No firmware to update'
196
509
  );
197
510
  }
198
511
 
199
- // TODO: 当前 firmware-pro2 子模块的 reboot/bootloader 流程还未稳定,先暂停自动进 bootloader。
200
- // await this.enterProtocolV2BootloaderMode();
512
+ await this.enterProtocolV2BootloaderMode();
201
513
 
202
514
  await this.executeProtocolV2Update({
203
- resourceBinary,
204
515
  fwBinaryMap,
205
516
  bootloaderBinary,
517
+ ...(installItems ? { installItems } : undefined),
518
+ ...(resourceBundles?.length ? { resourceBundles } : undefined),
206
519
  });
207
520
 
208
521
  await this.exitProtocolV2BootloaderToNormal();
@@ -215,50 +528,349 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
215
528
  }
216
529
 
217
530
  private async getProtocolV2DeviceFeatures(): Promise<Features> {
531
+ if (this.device.features) {
532
+ return this.device.features;
533
+ }
218
534
  if (typeof this.device.getFeatures === 'function') {
219
535
  const features = await this.device.getFeatures();
220
536
  if (features) return features;
221
537
  }
222
- if (this.device.features) {
223
- return this.device.features;
224
- }
225
538
  throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Device features not available');
226
539
  }
227
540
 
228
- private async prepareResourceBinary(firmwareType: EFirmwareType, features: Features) {
229
- if (this.params.resourceBinary) {
230
- return this.params.resourceBinary;
541
+ private prepareBootloaderBinary(): ArrayBuffer | null {
542
+ return this.params.bootloaderBinary ?? null;
543
+ }
544
+
545
+ private hasExplicitProtocolV2Payload(fwBinaryMap: ProtocolV2TargetBinary[]) {
546
+ return (
547
+ !!this.params.resourceBundleFiles?.length ||
548
+ !!this.params.bootloaderBinary ||
549
+ fwBinaryMap.length > 0
550
+ );
551
+ }
552
+
553
+ private buildProtocolV2InstallItems({
554
+ bootloaderBinary,
555
+ fwBinaryMap,
556
+ }: {
557
+ bootloaderBinary: ArrayBuffer | null;
558
+ fwBinaryMap: ProtocolV2TargetBinary[];
559
+ }): ProtocolV2InstallItem[] {
560
+ const installItems: ProtocolV2InstallItem[] = [];
561
+
562
+ if (bootloaderBinary) {
563
+ installItems.push({
564
+ fileName: 'bootloader.bin',
565
+ binary: bootloaderBinary,
566
+ targetId: ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_BOOTLOADER,
567
+ kind: 'bootloader',
568
+ });
569
+ }
570
+
571
+ installItems.push(...fwBinaryMap.map(item => ({ ...item, kind: 'firmware' as const })));
572
+ return installItems;
573
+ }
574
+
575
+ private getRemoteComponentEntries(release: IFirmwareReleaseInfo) {
576
+ const { components } = release;
577
+ if (!components) return [];
578
+
579
+ const orderedKeys = [
580
+ ...(release.installOrder ?? []),
581
+ ...Object.keys(components).filter(key => !release.installOrder?.includes(key)),
582
+ ];
583
+
584
+ return orderedKeys
585
+ .map(key => {
586
+ const component = components[key];
587
+ return component ? ([key, component] as const) : undefined;
588
+ })
589
+ .filter((entry): entry is readonly [string, IProtocolV2FirmwareComponent] => !!entry);
590
+ }
591
+
592
+ private getRemoteComponentTarget(key: string, component: IProtocolV2FirmwareComponent) {
593
+ const targetName = component.target?.toUpperCase();
594
+ if (targetName === 'ROMLOADER') {
595
+ throw ERRORS.TypedError(
596
+ HardwareErrorCode.RuntimeError,
597
+ PROTOCOL_V2_ROMLOADER_UNSUPPORTED_MESSAGE
598
+ );
231
599
  }
232
- const resourceUrl = DataManager.getSysResourcesLatestRelease({
233
- features,
234
- forcedUpdateRes: this.params.forcedUpdateRes,
235
- firmwareType,
600
+ const target = PROTOCOL_V2_REMOTE_COMPONENT_TARGETS[targetName];
601
+ if (!target) {
602
+ throw ERRORS.TypedError(
603
+ HardwareErrorCode.RuntimeError,
604
+ `Unsupported Protocol V2 firmware component target: ${key}/${component.target}`
605
+ );
606
+ }
607
+ return target;
608
+ }
609
+
610
+ private getProtocolV2ResourceFilePath(path: string) {
611
+ if (path.startsWith('vol')) return path;
612
+ if (path.startsWith('/')) return `vol0:${path}`;
613
+ return `vol0:/${path}`;
614
+ }
615
+
616
+ private async readProtocolV2DeviceFileHeader(path: string) {
617
+ const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
618
+ const filePath = this.getProtocolV2ResourceFilePath(path);
619
+ const pathInfoRes = await typedCall('FilesystemPathInfoQuery', 'FilesystemPathInfo', {
620
+ path: filePath,
236
621
  });
622
+ const fileSize = toProtocolV2FiniteNumber(pathInfoRes.message?.size);
623
+ if (
624
+ !pathInfoRes.message?.exist ||
625
+ pathInfoRes.message?.directory ||
626
+ fileSize === undefined ||
627
+ fileSize < PROTOCOL_V2_OKPP_HEADER_SIZE
628
+ ) {
629
+ return null;
630
+ }
631
+
632
+ const chunkSize = this.getProtocolV2FirmwareChunkSize();
633
+ const chunks: Uint8Array[] = [];
634
+ let offset = 0;
635
+ while (offset < PROTOCOL_V2_OKPP_HEADER_SIZE) {
636
+ const readLen = Math.min(chunkSize, PROTOCOL_V2_OKPP_HEADER_SIZE - offset);
637
+ const res = await typedCall('FilesystemFileRead', 'FilesystemFile', {
638
+ file: {
639
+ path: filePath,
640
+ offset,
641
+ total_size: 0,
642
+ },
643
+ chunk_len: readLen,
644
+ ui_percentage: undefined,
645
+ });
646
+ const data = toProtocolV2Bytes(res.message?.data);
647
+ if (data.byteLength === 0) return null;
648
+ chunks.push(data);
649
+ offset += data.byteLength;
650
+ }
651
+
652
+ const headerBytes = new Uint8Array(offset);
653
+ let cursor = 0;
654
+ chunks.forEach(chunk => {
655
+ headerBytes.set(chunk, cursor);
656
+ cursor += chunk.byteLength;
657
+ });
658
+ return parseProtocolV2OkppHeader(headerBytes);
659
+ }
660
+
661
+ private async downloadRemoteProtocolV2Component(
662
+ key: string,
663
+ component: IProtocolV2FirmwareComponent
664
+ ): Promise<ProtocolV2RemoteComponentBinary> {
665
+ const target = this.getRemoteComponentTarget(key, component);
666
+ if (!component.url) {
667
+ throw ERRORS.TypedError(
668
+ HardwareErrorCode.RuntimeError,
669
+ `Missing Protocol V2 firmware component url: ${key}/${component.target}`
670
+ );
671
+ }
672
+
673
+ const { binary } = await getSysResourceBinary(component.url);
674
+ return {
675
+ ...target,
676
+ binary,
677
+ };
678
+ }
679
+
680
+ private async prepareRemoteProtocolV2Binaries(firmwareType: EFirmwareType, features: Features) {
681
+ const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
237
682
 
238
- if (resourceUrl) {
239
- const resource = (await getSysResourceBinary(resourceUrl)).binary;
240
- return resource;
683
+ let bootloaderBinary: ArrayBuffer | null = null;
684
+ const fwBinaryMap: ProtocolV2TargetBinary[] = [];
685
+ const installItems: ProtocolV2InstallItem[] = [];
686
+
687
+ if (!release) {
688
+ return {
689
+ bootloaderBinary,
690
+ fwBinaryMap,
691
+ installItems,
692
+ };
693
+ }
694
+
695
+ const entries = this.getRemoteComponentEntries(release);
696
+ const targetsToUpdate = new Set(this.params.targetsToUpdate ?? []);
697
+
698
+ for (const [key, component] of entries) {
699
+ const target = this.getRemoteComponentTarget(key, component);
700
+ const updateTarget = PROTOCOL_V2_UPDATE_TARGET_BY_TARGET_ID.get(target.targetId);
701
+ const shouldInstall = updateTarget ? targetsToUpdate.has(updateTarget) : false;
702
+ if (shouldInstall) {
703
+ const remoteBinary = await this.downloadRemoteProtocolV2Component(key, component);
704
+ if (remoteBinary.kind === 'bootloader') {
705
+ bootloaderBinary = remoteBinary.binary;
706
+ installItems.push({
707
+ fileName: remoteBinary.fileName,
708
+ binary: remoteBinary.binary,
709
+ targetId: remoteBinary.targetId,
710
+ kind: remoteBinary.kind,
711
+ });
712
+ } else {
713
+ const binaryEntry = {
714
+ fileName: remoteBinary.fileName,
715
+ binary: remoteBinary.binary,
716
+ targetId: remoteBinary.targetId,
717
+ };
718
+ fwBinaryMap.push(binaryEntry);
719
+ installItems.push({ ...binaryEntry, kind: remoteBinary.kind });
720
+ }
721
+ }
241
722
  }
242
- Log.warn('No resource url found');
243
- return null;
723
+
724
+ return {
725
+ bootloaderBinary,
726
+ fwBinaryMap,
727
+ installItems,
728
+ };
244
729
  }
245
730
 
246
- private async prepareBootloaderBinary(
731
+ // ============================================================
732
+ // RESC bundle 直写增量同步(FileRead 比对 + FilesystemFileWrite 直写)
733
+ // ============================================================
734
+
735
+ /**
736
+ * 准备 RESC bundle 列表。
737
+ *
738
+ * 两种模式(同 FirmwareUpdateV3 的 binary vs version 模式):
739
+ * - 用户传了 resourceBundleFiles(binary 数组):直接用,不做版本比对。
740
+ * - 用户没传:从远端 config.json 的 release.resourceBundles 拉取,
741
+ * 此时 syncProtocolV2ResourceBundles 会按需下载 + 比对设备已有 header 跳过。
742
+ */
743
+ private prepareProtocolV2ResourceBundles(
247
744
  firmwareType: EFirmwareType,
248
745
  features: Features
249
- ): Promise<ArrayBuffer | null> {
250
- if (this.params.bootloaderBinary) {
251
- return this.params.bootloaderBinary;
746
+ ): ProtocolV2ResourceBundleBinary[] | undefined {
747
+ // 模式1:用户手动传入 binary,直接安装,不比对
748
+ if (this.params.resourceBundleFiles?.length) {
749
+ return this.params.resourceBundleFiles.map(file => ({
750
+ name: file.devicePath.split('/').pop() ?? file.devicePath,
751
+ binary: file.binary,
752
+ devicePath: file.devicePath,
753
+ }));
754
+ }
755
+
756
+ if (!this.params.targetsToUpdate?.includes('resource')) {
757
+ return undefined;
252
758
  }
253
759
 
254
- if (this.params.bootloaderVersion) {
255
- const bootResourceUrl = DataManager.getBootloaderResource(features, firmwareType);
256
- if (bootResourceUrl) {
257
- const bootBinary = (await getSysResourceBinary(bootResourceUrl)).binary;
258
- return bootBinary;
760
+ // 模式2:远端配置模式,后续按需下载 + 比对
761
+ const release = DataManager.getFirmwareLatestRelease(features, firmwareType);
762
+ if (!release?.resourceBundles?.length) return undefined;
763
+
764
+ return release.resourceBundles.map(bundle => ({
765
+ name: bundle.name,
766
+ binary: new ArrayBuffer(0),
767
+ devicePath: bundle.devicePath,
768
+ url: bundle.url,
769
+ version: bundle.version,
770
+ payloadHash: bundle.payloadHash,
771
+ headerHash: bundle.headerHash,
772
+ })) as ProtocolV2ResourceBundleBinary[];
773
+ }
774
+
775
+ /**
776
+ * 同步 RESC bundles 到设备。
777
+ *
778
+ * 手动传入模式(有 binary):直接 FileWrite 直写,不比对。
779
+ * 远端配置模式(无 binary,有 url):按需下载 + FileRead 比对跳过已更新的。
780
+ */
781
+ private async syncProtocolV2ResourceBundles(
782
+ bundles: ProtocolV2ResourceBundleBinary[],
783
+ firmwareSize: number
784
+ ): Promise<{ processedSize: number; totalSize: number }> {
785
+ const transferStartTime = Date.now();
786
+ const transferTransport = this.getProtocolV2FirmwareTransferTransport();
787
+
788
+ const isManualMode = bundles.every(b => b.binary.byteLength > 0);
789
+ let bundlesToSync = bundles;
790
+
791
+ // 远端配置模式:按需下载 + 比对跳过
792
+ if (!isManualMode) {
793
+ const filtered: ProtocolV2ResourceBundleBinary[] = [];
794
+ for (const bundle of bundles) {
795
+ // 下载 binary
796
+ if (bundle.binary.byteLength === 0 && bundle.url) {
797
+ Log.log(`[FirmwareUpdateV4] downloading RESC bundle ${bundle.name} from ${bundle.url}`);
798
+ const { binary } = await getSysResourceBinary(bundle.url);
799
+ bundle.binary = binary;
800
+ }
801
+ // 比对设备上已有 header
802
+ const upToDate = await this.isProtocolV2ResourceBundleUpToDate(bundle);
803
+ if (upToDate) {
804
+ Log.log(`[FirmwareUpdateV4] skip RESC bundle ${bundle.name}; already up to date`);
805
+ } else {
806
+ filtered.push(bundle);
807
+ }
259
808
  }
809
+ bundlesToSync = filtered;
810
+ }
811
+
812
+ if (bundlesToSync.length === 0) {
813
+ Log.log('[FirmwareUpdateV4] all RESC bundles up to date, nothing to sync');
814
+ return { processedSize: 0, totalSize: firmwareSize };
815
+ }
816
+
817
+ // FileWrite 直写到设备
818
+ let totalSize = 0;
819
+ for (const b of bundlesToSync) totalSize += b.binary.byteLength;
820
+
821
+ const transferTotalSize = totalSize + firmwareSize;
822
+ let processedSize = 0;
823
+ for (const bundle of bundlesToSync) {
824
+ Log.log(
825
+ `[FirmwareUpdateV4] syncing RESC bundle ${bundle.name} -> ${bundle.devicePath} bytes=${bundle.binary.byteLength}`
826
+ );
827
+ processedSize = await this.protocolV2CommonUpdateProcess({
828
+ payload: bundle.binary,
829
+ filePath: bundle.devicePath,
830
+ processedSize,
831
+ totalSize: transferTotalSize,
832
+ });
833
+ }
834
+
835
+ const elapsedMs = Date.now() - transferStartTime;
836
+ Log.log(
837
+ `[FirmwareUpdateV4] RESC bundle sync finished transport=${transferTransport} bytes=${totalSize} elapsed=${(
838
+ elapsedMs / 1000
839
+ ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(totalSize, elapsedMs)} KB/s`
840
+ );
841
+ return { processedSize, totalSize: transferTotalSize };
842
+ }
843
+
844
+ /**
845
+ * 比对设备上已有 okpkg 的 OKPP header(仅远端配置模式使用)。
846
+ */
847
+ private async isProtocolV2ResourceBundleUpToDate(
848
+ bundle: ProtocolV2ResourceBundleBinary
849
+ ): Promise<boolean> {
850
+ if (this.params.forcedUpdateRes) return false;
851
+ if (!bundle.version && !bundle.payloadHash) return false;
852
+
853
+ try {
854
+ const header = await this.readProtocolV2DeviceFileHeader(bundle.devicePath);
855
+ if (!header) return false;
856
+
857
+ if (bundle.version) {
858
+ const cmp = compareProtocolV2Versions(header.version, bundle.version);
859
+ if (cmp === undefined || cmp !== 0) return false;
860
+ }
861
+ if (bundle.payloadHash) {
862
+ const expected = normalizeProtocolV2Hex(bundle.payloadHash);
863
+ if (expected && header.payloadHash !== expected) return false;
864
+ }
865
+ if (bundle.headerHash) {
866
+ const expected = normalizeProtocolV2Hex(bundle.headerHash);
867
+ if (expected && header.headerHash !== expected) return false;
868
+ }
869
+ return true;
870
+ } catch (error) {
871
+ Log.log(`[FirmwareUpdateV4] RESC bundle ${bundle.name} header check failed: `, error);
872
+ return false;
260
873
  }
261
- return null;
262
874
  }
263
875
 
264
876
  private isProtocolV2BootloaderMode() {
@@ -270,18 +882,16 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
270
882
 
271
883
  async enterProtocolV2BootloaderMode() {
272
884
  if (this.isProtocolV2BootloaderMode()) {
885
+ Log.debug('Protocol V2 device is already in bootloader mode, skip reboot to bootloader');
273
886
  return false;
274
887
  }
275
888
 
276
889
  try {
277
890
  this.postTipMessage(FirmwareUpdateTipMessage.AutoRebootToBootloader);
278
- await this.protocolV2Reboot(DevRebootType.Bootloader);
891
+ await this.protocolV2Reboot(DeviceRebootType.Bootloader);
279
892
  this.postTipMessage(FirmwareUpdateTipMessage.GoToBootloaderSuccess);
280
- this.checkDeviceToBootloader(this.payload.connectId);
281
- await this.checkPromise?.promise;
282
- this.checkPromise = null;
283
- await wait(1500);
284
- await this.device.acquire?.();
893
+ await wait(1000);
894
+ await this.waitForProtocolV2BootloaderMode();
285
895
  return true;
286
896
  } catch (error) {
287
897
  if (error instanceof HardwareError) {
@@ -292,160 +902,208 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
292
902
  }
293
903
  }
294
904
 
905
+ private async waitForProtocolV2BootloaderMode(
906
+ timeout = PROTOCOL_V2_BOOTLOADER_RECONNECT_TIMEOUT,
907
+ retryInterval = 1000
908
+ ) {
909
+ const startTime = Date.now();
910
+ let lastError: unknown;
911
+
912
+ while (Date.now() - startTime < timeout) {
913
+ try {
914
+ await this.reconnectProtocolV2Device();
915
+ const deviceInfo = await requestProtocolV2DeviceInfo({
916
+ commands: this.device.getCommands(),
917
+ timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
918
+ });
919
+ const features = this.device.updateProtocolV2Features(deviceInfo);
920
+ if (features?.bootloaderMode) {
921
+ return features;
922
+ }
923
+ lastError = new Error('Protocol V2 device is reachable but is not in bootloader mode');
924
+ } catch (error) {
925
+ lastError = error;
926
+ Log.log('Protocol V2 bootloader mode not ready, polling reconnect: ', error);
927
+ }
928
+ await wait(retryInterval);
929
+ }
930
+
931
+ throw ERRORS.TypedError(
932
+ HardwareErrorCode.FirmwareUpdateAutoEnterBootFailure,
933
+ `Protocol V2 bootloader not ready within ${timeout / 1000}s: ${this.normalizeErrorMessage(
934
+ lastError
935
+ )}`
936
+ );
937
+ }
938
+
295
939
  /**
296
- * 收集按 DevFirmwareTargetType 拆分的显式目标二进制。
940
+ * 收集按 DeviceFirmwareTargetType 拆分的显式目标二进制。
297
941
  * 文件名仅用于 staging 路径展示,target_id 已显式给定。
298
942
  */
299
943
  private collectExplicitTargetBinaries() {
300
- const entries: { fileName: string; binary: ArrayBuffer; targetId: number }[] = [];
944
+ const entries: ProtocolV2TargetBinary[] = [];
301
945
  const push = (binary: ArrayBuffer | undefined, fileName: string, targetId: number) => {
302
946
  if (binary) entries.push({ fileName, binary, targetId });
303
947
  };
304
948
 
305
- push(
306
- this.params.romloaderBinary,
307
- 'romloader.bin',
308
- ProtocolV2FirmwareTargetType.TARGET_MAIN_BOOT
309
- );
949
+ if (this.params.romloaderBinary) {
950
+ throw ERRORS.TypedError(
951
+ HardwareErrorCode.RuntimeError,
952
+ PROTOCOL_V2_ROMLOADER_UNSUPPORTED_MESSAGE
953
+ );
954
+ }
310
955
  push(
311
956
  this.params.applicationP1Binary,
312
957
  'application_p1.bin',
313
- ProtocolV2FirmwareTargetType.TARGET_MAIN_APP
958
+ ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P1
314
959
  );
315
960
  push(
316
961
  this.params.applicationP2Binary,
317
962
  'application_p2.bin',
318
- ProtocolV2FirmwareTargetType.TARGET_MAIN_APP
963
+ ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_APPLICATION_P2
319
964
  );
320
- push(this.params.coprocessorBinary, 'coprocessor.bin', ProtocolV2FirmwareTargetType.TARGET_BT);
321
- push(this.params.se01Binary, 'se01.bin', ProtocolV2FirmwareTargetType.TARGET_SE1);
322
- push(this.params.se02Binary, 'se02.bin', ProtocolV2FirmwareTargetType.TARGET_SE2);
323
- push(this.params.se03Binary, 'se03.bin', ProtocolV2FirmwareTargetType.TARGET_SE3);
324
- push(this.params.se04Binary, 'se04.bin', ProtocolV2FirmwareTargetType.TARGET_SE4);
965
+ push(
966
+ this.params.coprocessorBinary,
967
+ 'coprocessor.bin',
968
+ ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_COPROCESSOR
969
+ );
970
+ push(this.params.se01Binary, 'se01.bin', ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE01);
971
+ push(this.params.se02Binary, 'se02.bin', ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE02);
972
+ push(this.params.se03Binary, 'se03.bin', ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE03);
973
+ push(this.params.se04Binary, 'se04.bin', ProtocolV2FirmwareTargetType.FW_MGMT_TARGET_SE04);
325
974
  return entries;
326
975
  }
327
976
 
328
- private async prepareFirmwareAndBleBinary(firmwareType: EFirmwareType, features: Features) {
329
- const fwBinaryMap: { fileName: string; binary: ArrayBuffer; targetId?: number }[] = [];
330
- if (this.params.firmwareBinary) {
331
- fwBinaryMap.push({
332
- fileName: 'firmware.bin',
333
- binary: this.params.firmwareBinary,
334
- });
335
- } else if (this.params.firmwareVersion) {
336
- const firmwareBinary = (
337
- await getBinary({
338
- features,
339
- version: this.params.firmwareVersion,
340
- updateType: 'firmware',
341
- isUpdateBootloader: false,
342
- firmwareType,
343
- })
344
- ).binary;
345
- fwBinaryMap.push({
346
- fileName: 'firmware.bin',
347
- binary: firmwareBinary,
348
- });
349
- }
350
-
351
- if (this.params.bleBinary) {
352
- fwBinaryMap.push({
353
- fileName: 'ble-firmware.bin',
354
- binary: this.params.bleBinary,
355
- });
356
- } else if (this.params.bleVersion) {
357
- const bleBinary = await getBinary({
358
- features,
359
- version: this.params.bleVersion,
360
- updateType: 'ble',
361
- firmwareType,
362
- });
363
- fwBinaryMap.push({
364
- fileName: 'ble-firmware.bin',
365
- binary: bleBinary.binary,
366
- });
367
- }
368
-
369
- return fwBinaryMap;
370
- }
371
-
372
977
  private async executeProtocolV2Update({
373
- resourceBinary,
374
978
  fwBinaryMap,
375
979
  bootloaderBinary,
980
+ installItems,
981
+ resourceBundles,
376
982
  }: {
377
- resourceBinary: ArrayBuffer | null;
378
- fwBinaryMap: { fileName: string; binary: ArrayBuffer; targetId?: number }[];
379
- bootloaderBinary: ArrayBuffer | null;
983
+ fwBinaryMap?: ProtocolV2TargetBinary[];
984
+ bootloaderBinary?: ArrayBuffer | null;
985
+ installItems?: ProtocolV2InstallItem[];
986
+ resourceBundles?: ProtocolV2ResourceBundleBinary[];
380
987
  }) {
381
- let totalSize = 0;
382
- let processedSize = 0;
988
+ const orderedInstallItems =
989
+ installItems ??
990
+ this.buildProtocolV2InstallItems({
991
+ bootloaderBinary: bootloaderBinary ?? null,
992
+ fwBinaryMap: fwBinaryMap ?? [],
993
+ });
383
994
 
384
- if (resourceBinary) totalSize += resourceBinary.byteLength;
385
- for (const fwbinary of fwBinaryMap) totalSize += fwbinary.binary.byteLength;
386
- if (bootloaderBinary) totalSize += bootloaderBinary.byteLength;
995
+ let firmwareSize = 0;
996
+ for (const item of orderedInstallItems) firmwareSize += item.binary.byteLength;
387
997
 
388
998
  this.postTipMessage(FirmwareUpdateTipMessage.StartTransferData);
389
999
 
390
- const targets: Array<{ target_id: number; path: string }> = [];
391
-
392
- if (resourceBinary) {
393
- // resource 仅支持单文件 .bin:整文件一次上传,target path 指向该文件
394
- const resourceFilePath = `${PROTOCOL_V2_FIRMWARE_STAGING_VOLUME}resource.bin`;
395
- processedSize = await this.protocolV2CommonUpdateProcess({
396
- payload: resourceBinary,
397
- filePath: resourceFilePath,
398
- processedSize,
399
- totalSize,
400
- });
401
- targets.push({
402
- target_id: ProtocolV2FirmwareTargetType.TARGET_RESOURCE,
403
- path: resourceFilePath,
404
- });
1000
+ let processedSize = 0;
1001
+ let totalSize = firmwareSize;
1002
+ if (resourceBundles?.length) {
1003
+ const resourceTransfer = await this.syncProtocolV2ResourceBundles(
1004
+ resourceBundles,
1005
+ firmwareSize
1006
+ );
1007
+ processedSize = resourceTransfer.processedSize;
1008
+ totalSize = resourceTransfer.totalSize;
405
1009
  }
406
1010
 
407
- if (bootloaderBinary) {
408
- const bootloaderPath = `${PROTOCOL_V2_FIRMWARE_STAGING_VOLUME}bootloader.bin`;
409
- processedSize = await this.protocolV2CommonUpdateProcess({
410
- payload: bootloaderBinary,
411
- filePath: bootloaderPath,
412
- processedSize,
413
- totalSize,
414
- });
415
- targets.push({
416
- target_id: ProtocolV2FirmwareTargetType.TARGET_MAIN_BOOT,
417
- path: bootloaderPath,
418
- });
1011
+ // 只有 RESC bundle、没有固件 target 时跳过 staging/install 阶段
1012
+ if (orderedInstallItems.length === 0) {
1013
+ Log.log('[FirmwareUpdateV4] no firmware targets to install (RESC bundles only)');
1014
+ if (totalSize > 0) {
1015
+ this.postProgressMessage(100, 'transferData');
1016
+ }
1017
+ return;
419
1018
  }
1019
+ let transferredSize = processedSize;
1020
+ const transferStartTime = Date.now();
1021
+ const transferTransport = this.getProtocolV2FirmwareTransferTransport();
1022
+ const chunkSize = this.getProtocolV2FirmwareChunkSize();
1023
+ const onTransferredBytes = (bytes: number) => {
1024
+ transferredSize = bytes;
1025
+ };
1026
+ Log.log(
1027
+ `[FirmwareUpdateV4] transfer started transport=${transferTransport} total=${totalSize} bytes chunk=${chunkSize} bytes`
1028
+ );
420
1029
 
421
- for (const fwbinary of fwBinaryMap) {
422
- const firmwarePath = `${PROTOCOL_V2_FIRMWARE_STAGING_VOLUME}${fwbinary.fileName}`;
423
- processedSize = await this.protocolV2CommonUpdateProcess({
424
- payload: fwbinary.binary,
425
- filePath: firmwarePath,
426
- processedSize,
427
- totalSize,
428
- });
429
- targets.push({
430
- target_id: fwbinary.targetId ?? protocolV2FileNameToTargetId(fwbinary.fileName),
431
- path: firmwarePath,
432
- });
433
- }
1030
+ const stagedInstallTargets: ProtocolV2InstallTarget[] = [];
434
1031
 
435
- if (totalSize > 0) {
436
- this.postProgressMessage(100, 'transferData');
1032
+ try {
1033
+ for (const item of orderedInstallItems) {
1034
+ const filePath = this.getProtocolV2InstallItemStagingPath(item);
1035
+ Log.log(
1036
+ `[FirmwareUpdateV4] staging ${item.kind} via FilesystemFileWrite target=${item.targetId} path=${filePath} source=${item.fileName} bytes=${item.binary.byteLength}`
1037
+ );
1038
+ processedSize = await this.protocolV2CommonUpdateProcess({
1039
+ payload: item.binary,
1040
+ filePath,
1041
+ processedSize,
1042
+ totalSize,
1043
+ onTransferredBytes,
1044
+ });
1045
+ transferredSize = processedSize;
1046
+ await this.verifyProtocolV2StagedFile(filePath, item.binary.byteLength);
1047
+
1048
+ stagedInstallTargets.push({
1049
+ ...item,
1050
+ path: filePath,
1051
+ });
1052
+ }
1053
+
1054
+ if (totalSize > 0) {
1055
+ this.postProgressMessage(100, 'transferData');
1056
+ }
1057
+
1058
+ const elapsedMs = Date.now() - transferStartTime;
1059
+ Log.log(
1060
+ `[FirmwareUpdateV4] transfer finished transport=${transferTransport} bytes=${totalSize} elapsed=${(
1061
+ elapsedMs / 1000
1062
+ ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(totalSize, elapsedMs)} KB/s`
1063
+ );
1064
+ } catch (error) {
1065
+ const elapsedMs = Date.now() - transferStartTime;
1066
+ Log.warn(
1067
+ `[FirmwareUpdateV4] transfer failed transport=${transferTransport} bytes=${transferredSize}/${totalSize} elapsed=${(
1068
+ elapsedMs / 1000
1069
+ ).toFixed(2)}s speed=${formatProtocolV2TransferSpeed(transferredSize, elapsedMs)} KB/s`
1070
+ );
1071
+ throw error;
437
1072
  }
438
1073
 
439
1074
  this.postTipMessage(FirmwareUpdateTipMessage.ConfirmOnDevice);
440
- const startResponse = await this.protocolV2StartFirmwareUpdate({ targets });
441
- await this.waitForProtocolV2FirmwareUpdateComplete(targets, startResponse);
1075
+
1076
+ const allTargets = stagedInstallTargets.map(item => ({
1077
+ target_id: item.targetId,
1078
+ path: item.path,
1079
+ }));
1080
+ Log.log(`[FirmwareUpdateV4] DeviceFirmwareUpdateRequest targets=${JSON.stringify(allTargets)}`);
1081
+ const startResponse = await this.protocolV2StartFirmwareUpdate({ targets: allTargets });
1082
+ await this.waitForProtocolV2FirmwareUpdateComplete(allTargets, startResponse);
1083
+ }
1084
+
1085
+ private getProtocolV2InstallItemStagingPath(item: ProtocolV2InstallItem) {
1086
+ return `${PROTOCOL_V2_FIRMWARE_STAGING_VOLUME}${item.fileName}`;
1087
+ }
1088
+
1089
+ private async verifyProtocolV2StagedFile(path: string, expectedSize: number) {
1090
+ const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
1091
+ const response = await typedCall('FilesystemPathInfoQuery', 'FilesystemPathInfo', { path });
1092
+ const actualSize = toProtocolV2FiniteNumber(response.message?.size);
1093
+ if (!response.message?.exist || response.message?.directory || actualSize !== expectedSize) {
1094
+ throw ERRORS.TypedError(
1095
+ HardwareErrorCode.EmmcFileWriteFirmwareError,
1096
+ `staged file verification failed: path=${path} exist=${!!response.message
1097
+ ?.exist} expected=${expectedSize} actual=${actualSize ?? 'unknown'}`
1098
+ );
1099
+ }
442
1100
  }
443
1101
 
444
1102
  private async queryProtocolV2FirmwareUpdateStatus() {
445
1103
  const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
446
1104
  return typedCall(
447
- 'DevGetFirmwareUpdateStatus',
448
- 'DevFirmwareUpdateStatus',
1105
+ 'DeviceFirmwareUpdateStatusGet',
1106
+ 'DeviceFirmwareUpdateStatus',
449
1107
  {},
450
1108
  {
451
1109
  timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
@@ -465,14 +1123,32 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
465
1123
  );
466
1124
  }
467
1125
 
1126
+ private isProtocolV2NormalModeFeatures(features?: Features | null) {
1127
+ return !!features && !features.bootloaderMode && features.mode !== 'bootloader';
1128
+ }
1129
+
1130
+ private async probeProtocolV2NormalMode() {
1131
+ const deviceInfo = await requestProtocolV2DeviceInfo({
1132
+ commands: this.device.getCommands(),
1133
+ timeoutMs: PROTOCOL_V2_SHORT_RESPONSE_TIMEOUT,
1134
+ request: PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST,
1135
+ });
1136
+ const features = this.device.updateProtocolV2Features(deviceInfo);
1137
+ if (this.isProtocolV2NormalModeFeatures(features)) {
1138
+ Log.log('Protocol V2 firmware install finished; device is back in normal mode');
1139
+ return true;
1140
+ }
1141
+ return false;
1142
+ }
1143
+
468
1144
  private assertProtocolV2TargetStatus(
469
1145
  statusTargets: ProtocolV2FirmwareUpdateStatusTarget[],
470
1146
  expectedTargetIds: Set<number>
471
1147
  ) {
472
1148
  const failedTarget = statusTargets.find(
473
1149
  target =>
474
- expectedTargetIds.has(target.target_id) &&
475
- target.status === PROTOCOL_V2_TARGET_STATUS_FAILED
1150
+ expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1) &&
1151
+ isProtocolV2TargetStatusFailed(target.status)
476
1152
  );
477
1153
  if (failedTarget) {
478
1154
  throw ERRORS.TypedError(
@@ -483,8 +1159,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
483
1159
 
484
1160
  const completedTargets = statusTargets.filter(
485
1161
  target =>
486
- expectedTargetIds.has(target.target_id) &&
487
- target.status === PROTOCOL_V2_TARGET_STATUS_FINISHED
1162
+ expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1) &&
1163
+ isProtocolV2TargetStatusFinished(target.status)
488
1164
  );
489
1165
  if (completedTargets.length === expectedTargetIds.size && expectedTargetIds.size > 0) {
490
1166
  return true;
@@ -492,8 +1168,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
492
1168
 
493
1169
  const inProgressTarget = statusTargets.find(
494
1170
  target =>
495
- expectedTargetIds.has(target.target_id) &&
496
- target.status === PROTOCOL_V2_TARGET_STATUS_IN_PROGRESS
1171
+ expectedTargetIds.has(normalizeProtocolV2TargetId(target.target_id) ?? -1) &&
1172
+ isProtocolV2TargetStatusInProgress(target.status)
497
1173
  );
498
1174
  if (inProgressTarget) {
499
1175
  this.postProgressMessage(99, 'installingFirmware');
@@ -507,11 +1183,8 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
507
1183
  startResponse?: ProtocolV2FirmwareUpdateStartResponse
508
1184
  ) {
509
1185
  const expectedTargetIds = new Set(targets.map(target => target.target_id));
510
- if (startResponse?.type === 'Success') {
511
- return;
512
- }
513
- if (startResponse?.type === 'DevFirmwareUpdateStatus') {
514
- const statusTargets = (startResponse.message.targets ??
1186
+ if (startResponse?.type === 'DeviceFirmwareUpdateStatus') {
1187
+ const statusTargets = (startResponse.message.records ??
515
1188
  []) as ProtocolV2FirmwareUpdateStatusTarget[];
516
1189
  if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds)) {
517
1190
  return;
@@ -524,7 +1197,7 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
524
1197
  while (Date.now() - startTime < PROTOCOL_V2_INSTALL_TIMEOUT) {
525
1198
  try {
526
1199
  const statusRes = await this.queryProtocolV2FirmwareUpdateStatus();
527
- const statusTargets = (statusRes.message.targets ??
1200
+ const statusTargets = (statusRes.message.records ??
528
1201
  []) as ProtocolV2FirmwareUpdateStatusTarget[];
529
1202
  if (this.assertProtocolV2TargetStatus(statusTargets, expectedTargetIds)) {
530
1203
  return;
@@ -538,17 +1211,19 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
538
1211
  if (isProtocolV2PollingTransientError(error)) {
539
1212
  try {
540
1213
  await this.reconnectProtocolV2Device();
1214
+ if (await this.probeProtocolV2NormalMode()) {
1215
+ return;
1216
+ }
541
1217
  } catch (reconnectError) {
542
1218
  lastError = reconnectError;
543
1219
  Log.log(
544
- 'Protocol V2 firmware install reconnect/status polling failed: ',
1220
+ 'Protocol V2 firmware install reconnect/normal-mode probe failed: ',
545
1221
  reconnectError
546
1222
  );
547
1223
  }
548
1224
  try {
549
1225
  await this.pingProtocolV2Device();
550
1226
  Log.log('Protocol V2 firmware status unavailable, Ping is ready');
551
- return;
552
1227
  } catch (pingError) {
553
1228
  lastError = pingError;
554
1229
  Log.log('Protocol V2 firmware install Ping polling failed: ', pingError);
@@ -566,7 +1241,16 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
566
1241
 
567
1242
  private async exitProtocolV2BootloaderToNormal() {
568
1243
  this.postTipMessage(FirmwareUpdateTipMessage.SwitchFirmwareReconnectDevice);
569
- await this.protocolV2Reboot(DevRebootType.Normal);
1244
+ try {
1245
+ await this.reconnectProtocolV2Device();
1246
+ if (await this.probeProtocolV2NormalMode()) {
1247
+ Log.log('Protocol V2 device is already in normal mode, skip normal reboot');
1248
+ return;
1249
+ }
1250
+ } catch (error) {
1251
+ Log.log('Protocol V2 normal-mode probe before reboot failed: ', error);
1252
+ }
1253
+ await this.protocolV2Reboot(DeviceRebootType.Normal);
570
1254
  }
571
1255
 
572
1256
  private async waitForProtocolV2FinalFeatures() {
@@ -603,7 +1287,14 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
603
1287
  // 更新完成判定只需要各 target 版本号;scope 与请求内容保持一致
604
1288
  request: PROTOCOL_V2_VERSIONS_DEVICE_INFO_REQUEST,
605
1289
  });
606
- return this.device.updateProtocolV2Features(deviceInfo);
1290
+ const features = this.device.updateProtocolV2Features(deviceInfo);
1291
+ if (features.bootloaderMode || features.mode === 'bootloader') {
1292
+ throw ERRORS.TypedError(
1293
+ HardwareErrorCode.DeviceNotFound,
1294
+ 'Protocol V2 device is still in bootloader mode'
1295
+ );
1296
+ }
1297
+ return features;
607
1298
  } catch (error) {
608
1299
  lastError = error;
609
1300
  Log.log('Protocol V2 normal mode not ready, polling Ping: ', error);
@@ -628,27 +1319,75 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
628
1319
  const deviceDiff = await this.device.deviceConnector?.enumerate();
629
1320
  const devicesDescriptor = deviceDiff?.descriptors ?? [];
630
1321
 
631
- const { deviceList } = await DevicePool.getDevices(devicesDescriptor, this.connectId);
1322
+ if (
1323
+ DataManager.isBrowserWebUsb(DataManager.getSettings('env')) &&
1324
+ devicesDescriptor.length === 1
1325
+ ) {
1326
+ this.device.updateDescriptor(
1327
+ {
1328
+ ...devicesDescriptor[0],
1329
+ protocolType: PROTOCOL_V2_CONNECT_PROTOCOL,
1330
+ },
1331
+ true
1332
+ );
1333
+ await this.device.acquire(PROTOCOL_V2_CONNECT_PROTOCOL, { throwOnRunPromiseError: true });
1334
+ this.device.commands.disposed = false;
1335
+ this.device.getCommands().mainId = this.device.mainId ?? '';
1336
+ await this.device.initialize();
1337
+ return;
1338
+ }
1339
+
1340
+ // App 与 bootloader 序列号暂时可能不一致。V4 升级重连阶段只接受唯一枚举设备,
1341
+ // 避免继续按旧 app connectId 查缓存导致反复输出 path mismatch 日志。
1342
+ const { deviceList } = await DevicePool.getDevices(devicesDescriptor, undefined, {
1343
+ connectProtocol: PROTOCOL_V2_CONNECT_PROTOCOL,
1344
+ });
632
1345
  if (deviceList.length !== 1) {
633
1346
  throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound);
634
1347
  }
635
1348
 
1349
+ Log.debug(
1350
+ 'Protocol V2 firmware reconnect using single enumerated device:',
1351
+ deviceList[0].getConnectId()
1352
+ );
636
1353
  this.device.updateFromCache(deviceList[0]);
637
- await this.device.acquire();
1354
+ await this.device.acquire(PROTOCOL_V2_CONNECT_PROTOCOL, { throwOnRunPromiseError: true });
638
1355
  this.device.commands.disposed = false;
639
1356
  this.device.getCommands().mainId = this.device.mainId ?? '';
1357
+ await this.device.initialize();
1358
+ }
1359
+
1360
+ private async protocolV2CommonUpdateProcess(params: ProtocolV2FileTransferParams) {
1361
+ let lastError: unknown;
1362
+ for (let attempt = 1; attempt <= PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT; attempt += 1) {
1363
+ try {
1364
+ return await this.protocolV2WriteWholeFile(params);
1365
+ } catch (error) {
1366
+ lastError = error;
1367
+ Log.error(
1368
+ `Protocol V2 file transfer failed path=${params.filePath} attempt=${attempt}/${PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT}; restarting from offset 0`,
1369
+ error
1370
+ );
1371
+ if (attempt === PROTOCOL_V2_FILE_TRANSFER_RETRY_COUNT) {
1372
+ break;
1373
+ }
1374
+ await this.recoverProtocolV2FileTransfer();
1375
+ }
1376
+ }
1377
+
1378
+ throw ERRORS.TypedError(
1379
+ HardwareErrorCode.EmmcFileWriteFirmwareError,
1380
+ `transfer data error: ${getProtocolV2UnknownErrorText(lastError)}`
1381
+ );
640
1382
  }
641
1383
 
642
- private async protocolV2CommonUpdateProcess({
1384
+ private async protocolV2WriteWholeFile({
643
1385
  payload,
644
1386
  filePath,
645
1387
  processedSize,
646
1388
  totalSize,
647
- }: PROTO.FirmwareUpload & {
648
- filePath: string;
649
- processedSize?: number;
650
- totalSize?: number;
651
- }) {
1389
+ onTransferredBytes,
1390
+ }: ProtocolV2FileTransferParams) {
652
1391
  const chunkSize = this.getProtocolV2FirmwareChunkSize();
653
1392
  let offset = 0;
654
1393
  const getUploadProgress = (fileOffset: number) => {
@@ -663,9 +1402,13 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
663
1402
  const chunkLength = chunkEnd - offset;
664
1403
  const chunk = payload.slice(offset, chunkEnd);
665
1404
  const overwrite = offset === 0;
666
- const progress = getUploadProgress(chunkEnd);
1405
+ const progress = getProtocolV2DeviceTransferProgress(
1406
+ (processedSize ?? 0) + offset,
1407
+ (processedSize ?? 0) + chunkEnd,
1408
+ totalSize ?? payload.byteLength
1409
+ );
667
1410
 
668
- const writeRes = await this.fileWriteWithRetry(
1411
+ const writeRes = await this.fileWriteChunk(
669
1412
  filePath,
670
1413
  payload.byteLength,
671
1414
  offset,
@@ -685,13 +1428,28 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
685
1428
  );
686
1429
  }
687
1430
  offset = nextOffset;
1431
+ onTransferredBytes?.((processedSize ?? 0) + offset);
688
1432
  this.postProgressMessage(getUploadProgress(offset), 'transferData');
689
1433
  }
690
1434
 
691
1435
  return totalSize !== undefined ? (processedSize ?? 0) + payload.byteLength : 0;
692
1436
  }
693
1437
 
694
- private async fileWriteWithRetry(
1438
+ private getProtocolV2FirmwareTransferTransport() {
1439
+ const env = DataManager.getSettings('env');
1440
+ if (env && DataManager.isBleConnect(env)) {
1441
+ return 'BLE';
1442
+ }
1443
+ if (
1444
+ env &&
1445
+ (DataManager.isBrowserWebUsb(env) || DataManager.isDesktopWebUsb(env) || env === 'web')
1446
+ ) {
1447
+ return 'WebUSB';
1448
+ }
1449
+ return env ?? 'unknown';
1450
+ }
1451
+
1452
+ private async fileWriteChunk(
695
1453
  filePath: string,
696
1454
  totalFileSize: number,
697
1455
  offset: number,
@@ -699,57 +1457,37 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
699
1457
  overwrite: boolean,
700
1458
  progress: number | null
701
1459
  ): Promise<TypedResponseMessage<'FilesystemFile'>> {
702
- const writeFunc = async () => {
703
- const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
704
- const writeRes = await typedCall('FilesystemFileWrite', 'FilesystemFile', {
705
- file: {
706
- path: filePath,
707
- offset,
708
- total_size: totalFileSize,
709
- data: chunk,
710
- },
711
- overwrite,
712
- append: false,
713
- ui_percentage: progress ?? undefined,
714
- });
715
- if (writeRes.type !== 'FilesystemFile') {
716
- if ((writeRes as any).type === 'CallMethodError') {
717
- if (((writeRes as any).message.error ?? '').indexOf(SESSION_ERROR) > -1) {
718
- throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, SESSION_ERROR);
719
- }
1460
+ const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
1461
+ const writeRes = await typedCall('FilesystemFileWrite', 'FilesystemFile', {
1462
+ file: {
1463
+ path: filePath,
1464
+ offset,
1465
+ total_size: totalFileSize,
1466
+ data: chunk,
1467
+ },
1468
+ overwrite,
1469
+ append: false,
1470
+ ui_percentage: progress ?? undefined,
1471
+ });
1472
+ if (writeRes.type !== 'FilesystemFile') {
1473
+ if ((writeRes as any).type === 'CallMethodError') {
1474
+ if (((writeRes as any).message.error ?? '').indexOf(SESSION_ERROR) > -1) {
1475
+ throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, SESSION_ERROR);
720
1476
  }
721
- throw ERRORS.TypedError(
722
- HardwareErrorCode.EmmcFileWriteFirmwareError,
723
- 'transfer data error'
724
- );
725
1477
  }
726
- return writeRes;
727
- };
1478
+ throw ERRORS.TypedError(HardwareErrorCode.EmmcFileWriteFirmwareError, 'transfer data error');
1479
+ }
1480
+ return writeRes;
1481
+ }
728
1482
 
729
- let retryCount = 10;
730
- while (retryCount > 0) {
731
- try {
732
- const result = await writeFunc();
733
- return result;
734
- } catch (error) {
735
- Log.error(`fileWrite error: `, error);
736
- retryCount--;
737
- if (retryCount === 0) {
738
- throw ERRORS.TypedError(
739
- HardwareErrorCode.EmmcFileWriteFirmwareError,
740
- 'transfer data error'
741
- );
742
- }
743
- const env = DataManager.getSettings('env');
744
- if (DataManager.isBleConnect(env)) {
745
- await wait(3000);
746
- await this.acquireProtocolV2BleDevice();
747
- await this.device.initialize();
748
- }
749
- await wait(2000);
750
- }
1483
+ private async recoverProtocolV2FileTransfer() {
1484
+ const env = DataManager.getSettings('env');
1485
+ if (DataManager.isBleConnect(env)) {
1486
+ await wait(3000);
1487
+ await this.acquireProtocolV2BleDevice();
1488
+ await this.device.initialize();
751
1489
  }
752
- throw ERRORS.TypedError(HardwareErrorCode.EmmcFileWriteFirmwareError, 'transfer data error');
1490
+ await wait(2000);
753
1491
  }
754
1492
 
755
1493
  private async acquireProtocolV2BleDevice() {
@@ -770,24 +1508,27 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
770
1508
  let response: ProtocolV2FirmwareUpdateStartResponse;
771
1509
  try {
772
1510
  response = await commands.typedCall(
773
- 'DevFirmwareUpdate',
1511
+ 'DeviceFirmwareUpdateRequest',
774
1512
  PROTOCOL_V2_FIRMWARE_UPDATE_RESPONSE_TYPES,
775
1513
  {
776
1514
  targets,
777
1515
  },
778
1516
  {
779
- intermediateTypes: ['DevFirmwareInstallProgress'],
780
- onIntermediateResponse: (response: { message?: { progress?: number } }) => {
781
- const progress = Number(response.message?.progress);
782
- if (Number.isFinite(progress)) {
783
- this.postProgressMessage(Math.min(progress, 99), 'installingFirmware');
1517
+ intermediateTypes: ['DeviceFirmwareUpdateStatus'],
1518
+ timeoutMs: PROTOCOL_V2_START_UPDATE_TIMEOUT,
1519
+ onIntermediateResponse: (response: { type?: string }) => {
1520
+ if (response.type === 'DeviceFirmwareUpdateStatus') {
1521
+ this.postProgressMessage(99, 'installingFirmware');
784
1522
  }
785
1523
  },
786
1524
  }
787
1525
  );
788
1526
  } catch (error) {
789
- if (isProtocolV2DeviceDisconnectedError(error)) {
790
- Log.log('Rebooting device');
1527
+ if (isProtocolV2StartUpdateTransientError(error)) {
1528
+ Log.log(
1529
+ 'Protocol V2 firmware update request did not return; continue status polling',
1530
+ error
1531
+ );
791
1532
  } else {
792
1533
  throw error;
793
1534
  }
@@ -796,10 +1537,10 @@ export default class FirmwareUpdateV4 extends FirmwareUpdateBaseMethod<FirmwareU
796
1537
  return response;
797
1538
  }
798
1539
 
799
- private async protocolV2Reboot(rebootType: DevRebootType) {
1540
+ private async protocolV2Reboot(rebootType: DeviceRebootType) {
800
1541
  const typedCall = this.device.getCommands().typedCall.bind(this.device.getCommands());
801
1542
  try {
802
- const res = await typedCall('DevReboot', 'Success', {
1543
+ const res = await typedCall('DeviceReboot', 'Success', {
803
1544
  reboot_type: rebootType,
804
1545
  });
805
1546
  return res.message;