@onekeyfe/hd-core 1.2.0-alpha.3 → 1.2.0-alpha.5

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 (117) hide show
  1. package/__tests__/preInitialize.test.ts +33 -0
  2. package/__tests__/protocol-v2.test.ts +1443 -480
  3. package/dist/api/FileRead.d.ts.map +1 -1
  4. package/dist/api/FileWrite.d.ts.map +1 -1
  5. package/dist/api/FirmwareUpdateV4.d.ts +22 -2
  6. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  7. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts +1 -0
  8. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  9. package/dist/api/firmware/getBinary.d.ts +4 -0
  10. package/dist/api/firmware/getBinary.d.ts.map +1 -1
  11. package/dist/api/firmware/progressThrottle.d.ts +3 -0
  12. package/dist/api/firmware/progressThrottle.d.ts.map +1 -0
  13. package/dist/api/index.d.ts +5 -10
  14. package/dist/api/index.d.ts.map +1 -1
  15. package/dist/api/protocol-v2/DeviceFactoryInfoGet.d.ts +6 -0
  16. package/dist/api/protocol-v2/DeviceFactoryInfoGet.d.ts.map +1 -0
  17. package/dist/api/protocol-v2/DeviceFactoryInfoSet.d.ts +7 -0
  18. package/dist/api/protocol-v2/DeviceFactoryInfoSet.d.ts.map +1 -0
  19. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts +1 -1
  20. package/dist/api/protocol-v2/DeviceFirmwareUpdate.d.ts.map +1 -1
  21. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts +3 -2
  22. package/dist/api/protocol-v2/DeviceGetFirmwareUpdateStatus.d.ts.map +1 -1
  23. package/dist/api/protocol-v2/{DeviceGetDeviceInfo.d.ts → DeviceInfoGet.d.ts} +10 -10
  24. package/dist/api/protocol-v2/DeviceInfoGet.d.ts.map +1 -0
  25. package/dist/api/protocol-v2/{FilesystemFixPermission.d.ts → FilesystemPermissionFix.d.ts} +2 -2
  26. package/dist/api/protocol-v2/FilesystemPermissionFix.d.ts.map +1 -0
  27. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts +6 -0
  28. package/dist/api/protocol-v2/ProtocolInfoRequest.d.ts.map +1 -0
  29. package/dist/api/protocol-v2/helpers.d.ts +27 -18
  30. package/dist/api/protocol-v2/helpers.d.ts.map +1 -1
  31. package/dist/api/ton/TonSignData.d.ts +1 -5
  32. package/dist/api/ton/TonSignData.d.ts.map +1 -1
  33. package/dist/core/index.d.ts.map +1 -1
  34. package/dist/data-manager/DataManager.d.ts +1 -1
  35. package/dist/data-manager/DataManager.d.ts.map +1 -1
  36. package/dist/data-manager/connectSettings.d.ts.map +1 -1
  37. package/dist/device/Device.d.ts +4 -1
  38. package/dist/device/Device.d.ts.map +1 -1
  39. package/dist/deviceProfile/buildDeviceFeatures.d.ts.map +1 -1
  40. package/dist/deviceProfile/buildDeviceProfile.d.ts.map +1 -1
  41. package/dist/index.d.ts +82 -54
  42. package/dist/index.js +1615 -1537
  43. package/dist/inject.d.ts.map +1 -1
  44. package/dist/protocols/protocol-v2/features.d.ts +12 -14
  45. package/dist/protocols/protocol-v2/features.d.ts.map +1 -1
  46. package/dist/protocols/protocol-v2/firmware.d.ts +11 -9
  47. package/dist/protocols/protocol-v2/firmware.d.ts.map +1 -1
  48. package/dist/types/api/firmwareUpdate.d.ts +1 -6
  49. package/dist/types/api/firmwareUpdate.d.ts.map +1 -1
  50. package/dist/types/api/getDeviceInfo.d.ts +2 -0
  51. package/dist/types/api/getDeviceInfo.d.ts.map +1 -1
  52. package/dist/types/api/index.d.ts +6 -11
  53. package/dist/types/api/index.d.ts.map +1 -1
  54. package/dist/types/api/protocolV2.d.ts +12 -17
  55. package/dist/types/api/protocolV2.d.ts.map +1 -1
  56. package/dist/types/device.d.ts +1 -0
  57. package/dist/types/device.d.ts.map +1 -1
  58. package/dist/types/params.d.ts +1 -0
  59. package/dist/types/params.d.ts.map +1 -1
  60. package/dist/types/settings.d.ts +29 -2
  61. package/dist/types/settings.d.ts.map +1 -1
  62. package/dist/utils/deviceFeaturesUtils.d.ts.map +1 -1
  63. package/dist/utils/patch.d.ts +1 -1
  64. package/dist/utils/patch.d.ts.map +1 -1
  65. package/package.json +4 -4
  66. package/src/api/FileRead.ts +14 -1
  67. package/src/api/FileWrite.ts +14 -1
  68. package/src/api/FirmwareUpdateV4.ts +975 -201
  69. package/src/api/GetOnekeyFeatures.ts +1 -1
  70. package/src/api/device/DeviceRebootToBoardloader.ts +4 -4
  71. package/src/api/device/DeviceRebootToBootloader.ts +4 -4
  72. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +18 -4
  73. package/src/api/firmware/progressThrottle.ts +44 -0
  74. package/src/api/index.ts +5 -10
  75. package/src/api/protocol-v2/{FactoryGetDeviceInfo.ts → DeviceFactoryInfoGet.ts} +3 -3
  76. package/src/api/protocol-v2/DeviceFactoryInfoSet.ts +32 -0
  77. package/src/api/protocol-v2/DeviceFirmwareUpdate.ts +3 -5
  78. package/src/api/protocol-v2/DeviceGetFirmwareUpdateStatus.ts +39 -5
  79. package/src/api/protocol-v2/{DeviceGetDeviceInfo.ts → DeviceInfoGet.ts} +52 -22
  80. package/src/api/protocol-v2/DeviceReboot.ts +1 -1
  81. package/src/api/protocol-v2/{FilesystemFixPermission.ts → FilesystemPermissionFix.ts} +2 -2
  82. package/src/api/protocol-v2/{GetProtoVersion.ts → ProtocolInfoRequest.ts} +2 -2
  83. package/src/api/protocol-v2/helpers.ts +64 -43
  84. package/src/api/ton/TonSignData.ts +1 -5
  85. package/src/core/index.ts +19 -5
  86. package/src/data/messages/messages-protocol-v2.json +485 -1069
  87. package/src/data-manager/DataManager.ts +7 -0
  88. package/src/data-manager/connectSettings.ts +0 -6
  89. package/src/device/Device.ts +25 -14
  90. package/src/deviceProfile/buildDeviceFeatures.ts +60 -63
  91. package/src/deviceProfile/buildDeviceProfile.ts +77 -58
  92. package/src/inject.ts +10 -16
  93. package/src/protocols/protocol-v2/features.ts +26 -63
  94. package/src/protocols/protocol-v2/firmware.ts +12 -36
  95. package/src/types/api/firmwareUpdate.ts +10 -20
  96. package/src/types/api/getDeviceInfo.ts +2 -0
  97. package/src/types/api/index.ts +10 -20
  98. package/src/types/api/protocolV2.ts +25 -43
  99. package/src/types/device.ts +1 -0
  100. package/src/types/params.ts +4 -0
  101. package/src/types/settings.ts +44 -35
  102. package/src/utils/deviceFeaturesUtils.ts +45 -10
  103. package/dist/api/protocol-v2/DevReboot.d.ts +0 -7
  104. package/dist/api/protocol-v2/DevReboot.d.ts.map +0 -1
  105. package/dist/api/protocol-v2/DeviceGetDeviceInfo.d.ts.map +0 -1
  106. package/dist/api/protocol-v2/DeviceGetOnboardingStatus.d.ts +0 -6
  107. package/dist/api/protocol-v2/DeviceGetOnboardingStatus.d.ts.map +0 -1
  108. package/dist/api/protocol-v2/FactoryDeviceInfoSettings.d.ts +0 -7
  109. package/dist/api/protocol-v2/FactoryDeviceInfoSettings.d.ts.map +0 -1
  110. package/dist/api/protocol-v2/FactoryGetDeviceInfo.d.ts +0 -6
  111. package/dist/api/protocol-v2/FactoryGetDeviceInfo.d.ts.map +0 -1
  112. package/dist/api/protocol-v2/FilesystemFixPermission.d.ts.map +0 -1
  113. package/dist/api/protocol-v2/GetProtoVersion.d.ts +0 -6
  114. package/dist/api/protocol-v2/GetProtoVersion.d.ts.map +0 -1
  115. package/src/api/protocol-v2/DevReboot.ts +0 -24
  116. package/src/api/protocol-v2/DeviceGetOnboardingStatus.ts +0 -16
  117. package/src/api/protocol-v2/FactoryDeviceInfoSettings.ts +0 -29
@@ -1,17 +1,20 @@
1
1
  import { HardwareErrorCode } from '@onekeyfe/hd-shared';
2
- import { DevRebootType } from '@onekeyfe/hd-transport';
2
+ import { DeviceRebootType } from '@onekeyfe/hd-transport';
3
3
 
4
- import ConfluxSignTransaction from '../src/api/conflux/ConfluxSignTransaction';
4
+ import * as firmwareBinaryApi from '../src/api/firmware/getBinary';
5
5
  import DnxGetAddress from '../src/api/dynex/DnxGetAddress';
6
6
  import DnxSignTransaction from '../src/api/dynex/DnxSignTransaction';
7
7
  import DirList from '../src/api/DirList';
8
8
  import FileRead from '../src/api/FileRead';
9
9
  import FileWrite from '../src/api/FileWrite';
10
- import DevReboot from '../src/api/protocol-v2/DevReboot';
10
+ import DeviceFactoryInfoGet from '../src/api/protocol-v2/DeviceFactoryInfoGet';
11
+ import DeviceFactoryInfoSet from '../src/api/protocol-v2/DeviceFactoryInfoSet';
11
12
  import DeviceFirmwareUpdate from '../src/api/protocol-v2/DeviceFirmwareUpdate';
12
- import DeviceGetDeviceInfo from '../src/api/protocol-v2/DeviceGetDeviceInfo';
13
- import DeviceGetOnboardingStatus from '../src/api/protocol-v2/DeviceGetOnboardingStatus';
13
+ import DeviceGetFirmwareUpdateStatus from '../src/api/protocol-v2/DeviceGetFirmwareUpdateStatus';
14
+ import DeviceInfoGet from '../src/api/protocol-v2/DeviceInfoGet';
14
15
  import DeviceReboot from '../src/api/protocol-v2/DeviceReboot';
16
+ import FilesystemPermissionFix from '../src/api/protocol-v2/FilesystemPermissionFix';
17
+ import ProtocolInfoRequest from '../src/api/protocol-v2/ProtocolInfoRequest';
15
18
  import EVMSignTypedData from '../src/api/evm/EVMSignTypedData';
16
19
  import EVMSignMessageEIP712 from '../src/api/evm/EVMSignMessageEIP712';
17
20
  import FirmwareUpdateV3 from '../src/api/FirmwareUpdateV3';
@@ -35,8 +38,6 @@ import TonSignData from '../src/api/ton/TonSignData';
35
38
  import TonSignMessage from '../src/api/ton/TonSignMessage';
36
39
  import TonSignProof from '../src/api/ton/TonSignProof';
37
40
  import TronGetAddress from '../src/api/tron/TronGetAddress';
38
- import TronSignMessage from '../src/api/tron/TronSignMessage';
39
- import XrpSignTransaction from '../src/api/xrp/XrpSignTransaction';
40
41
  import StellarGetAddress from '../src/api/stellar/StellarGetAddress';
41
42
  import BenfenSignMessage from '../src/api/benfen/BenfenSignMessage';
42
43
  import { getBitcoinForkVersionRange } from '../src/api/btc/helpers/versionLimit';
@@ -45,17 +46,14 @@ import { Device } from '../src/device/Device';
45
46
  import { UI_REQUEST } from '../src/events/ui-request';
46
47
  import {
47
48
  PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
48
- buildMockProtocolV2DeviceInfo,
49
49
  requestProtocolV2DeviceInfo,
50
- setProtocolV2DeviceInfoMock,
51
50
  } from '../src/protocols/protocol-v2/features';
52
- import {
53
- buildProfileFromProtocolV2,
54
- buildProtocolV2FeaturesPayload,
55
- } from '../src/deviceProfile';
51
+ import { buildProfileFromProtocolV2, buildProtocolV2FeaturesPayload } from '../src/deviceProfile';
56
52
  import {
57
53
  getDeviceType,
58
54
  getFirmwareType,
55
+ getFirmwareUpdateField,
56
+ getFirmwareUpdateFieldArray,
59
57
  getMethodVersionRange,
60
58
  isMethodVersionRangeUnsupported,
61
59
  } from '../src/utils';
@@ -74,12 +72,6 @@ jest.mock('../src/data/config', () => ({
74
72
  DEFAULT_DOMAIN: 'https://jssdk.onekey.so/1.0.0/',
75
73
  }));
76
74
 
77
- // DeviceGetDeviceInfo mock 开关默认开启(早期工程板固件没有该消息)。
78
- // 本测试文件验证的是真实 wire 调用行为,统一关闭;mock 行为单独有用例覆盖。
79
- beforeAll(() => {
80
- setProtocolV2DeviceInfoMock(false);
81
- });
82
-
83
75
  const descriptor = {
84
76
  id: 'ble-id',
85
77
  path: 'usb-path',
@@ -101,8 +93,7 @@ function stubDevice<T extends Record<string, any>>(device: T): T {
101
93
  d.getCurrentSafetyChecks ??= () => d.features?.safetyChecks;
102
94
  d.getCurrentDeviceId ??= () => d.features?.deviceId || undefined;
103
95
  d.getCurrentSerialNo ??= () => d.features?.serialNo || '';
104
- d.getCurrentPassphraseProtection ??= () =>
105
- d.features?.passphraseProtection;
96
+ d.getCurrentPassphraseProtection ??= () => d.features?.passphraseProtection;
106
97
  d.getCurrentMethodVersionRange ??= (fn: (model: any) => any) => {
107
98
  const deviceType = d.getCurrentDeviceType();
108
99
  const range = fn(deviceType);
@@ -121,7 +112,19 @@ function normalizeProtocolV2Features(_descriptor: unknown, deviceInfo?: Protocol
121
112
  return buildProtocolV2FeaturesPayload(deviceInfo);
122
113
  }
123
114
 
124
- async function requestProtocolV2LegacyFeatures({
115
+ const protocolV2BootloaderDeviceInfo: ProtocolV2DeviceInfo = {
116
+ protocol_version: 1,
117
+ hw: {
118
+ serial_no: 'PR9999999999',
119
+ },
120
+ fw: {
121
+ bootloader: {
122
+ version: '0.0.1',
123
+ },
124
+ },
125
+ };
126
+
127
+ async function requestProtocolV2Features({
125
128
  commands,
126
129
  descriptor: inputDescriptor,
127
130
  }: {
@@ -133,63 +136,38 @@ async function requestProtocolV2LegacyFeatures({
133
136
  }
134
137
 
135
138
  describe('Protocol V2 feature adapter', () => {
136
- test('skips DeviceGetDeviceInfo wire call and returns mock when mock is enabled', async () => {
137
- const typedCall = jest.fn();
138
- setProtocolV2DeviceInfoMock(true);
139
- try {
140
- const deviceInfo = await requestProtocolV2DeviceInfo({
141
- commands: { typedCall } as unknown as DeviceCommands,
142
- });
143
- expect(typedCall).not.toHaveBeenCalled();
144
- expect(deviceInfo).toEqual(buildMockProtocolV2DeviceInfo());
145
- const profile = buildProfileFromProtocolV2({
146
- deviceInfo,
147
- });
148
- expect(profile).toMatchObject({
149
- protocol: 'V2',
150
- deviceId: '',
151
- serialNo: '',
152
- status: { initialized: true },
153
- versions: { firmware: '0.1.0' },
154
- });
155
- } finally {
156
- setProtocolV2DeviceInfoMock(false);
157
- }
158
- });
159
-
160
139
  test('normalizes Protocol V2 DeviceInfo into existing Features fields', () => {
161
140
  const features = normalizeProtocolV2Features(descriptor as any, {
162
141
  protocol_version: 1,
163
142
  hw: {
164
143
  serial_no: 'PR2SERIAL',
165
- device_id: 'PRO2-DEVICE-ID',
166
144
  },
167
145
  fw: {
168
- board: {
146
+ application_data: {
169
147
  version: '0.1.0',
170
148
  hash: [1, 2, 255],
171
149
  },
172
- boot: {
150
+ bootloader: {
173
151
  version: '0.2.0',
174
152
  build_id: 'boot-build',
175
153
  hash: new Uint8Array([10, 11]),
176
154
  },
177
- app: {
155
+ application: {
178
156
  version: '1.2.3',
179
157
  build_id: 'app-build',
180
158
  hash: 'abc123',
181
159
  },
182
160
  },
183
- bt: {
184
- app: {
161
+ coprocessor: {
162
+ application: {
185
163
  version: '4.5.6',
186
164
  build_id: 'bt-build',
187
165
  hash: [12, 13],
188
166
  },
189
- adv_name: 'Pro2 BLE',
167
+ bt_adv_name: 'Pro2 BLE',
190
168
  },
191
169
  se1: {
192
- app: {
170
+ application: {
193
171
  version: '7.8.9',
194
172
  build_id: 'se-build',
195
173
  hash: [14, 15],
@@ -197,18 +175,19 @@ describe('Protocol V2 feature adapter', () => {
197
175
  state: 85,
198
176
  },
199
177
  se2: {
200
- app: {
178
+ application: {
201
179
  version: '8.0.0',
202
180
  },
203
181
  state: 0,
204
182
  },
205
183
  status: {
206
- label: 'My Pro2',
207
- language: 'en-US',
208
- bt_enable: true,
184
+ device_id: 'PRO2-DEVICE-ID',
185
+ unlocked: true,
209
186
  init_states: false,
210
187
  backup_required: true,
211
- passphrase_protection: true,
188
+ passphrase_enabled: true,
189
+ attach_to_pin_enabled: true,
190
+ unlocked_by_attach_to_pin: true,
212
191
  },
213
192
  });
214
193
 
@@ -228,24 +207,47 @@ describe('Protocol V2 feature adapter', () => {
228
207
  expect(features.verify?.bleHash).toBe('0c0d');
229
208
  expect(features.se01Version).toBe('7.8.9');
230
209
  expect(features.verify?.se01Hash).toBe('0e0f');
231
- expect(features.label).toBe('My Pro2');
232
- expect(features.language).toBe('en-US');
210
+ expect(features.label).toBeNull();
211
+ expect(features.language).toBeNull();
233
212
  expect(features.initialized).toBe(false);
213
+ expect(features.unlocked).toBe(true);
234
214
  expect(features.backupRequired).toBe(true);
235
215
  expect(features.passphraseProtection).toBe(true);
236
- expect(features.bleEnabled).toBe(true);
216
+ expect(features.attachToPinEnabled).toBe(true);
217
+ expect(features.unlockedAttachPin).toBe(true);
218
+ expect(features.bleEnabled).toBeNull();
219
+ });
220
+
221
+ test('marks Protocol V2 DeviceInfo without status as bootloader mode', () => {
222
+ const deviceInfo = {
223
+ protocol_version: 1,
224
+ hw: {
225
+ serial_no: 'PR2BOOT',
226
+ },
227
+ fw: {
228
+ bootloader: {
229
+ version: '0.2.0',
230
+ },
231
+ },
232
+ };
233
+ const features = normalizeProtocolV2Features(descriptor as any, deviceInfo);
234
+ const profile = buildProfileFromProtocolV2({ deviceInfo });
235
+
236
+ expect(features.mode).toBe('bootloader');
237
+ expect(features.bootloaderMode).toBe(true);
238
+ expect(features.initialized).toBeNull();
239
+ expect(profile.status.mode).toBe('bootloader');
240
+ expect(profile.status.bootloaderMode).toBe(true);
237
241
  });
238
242
 
239
- test('uses GetPassphraseState payloads compatible with Pro series passphrase flow', async () => {
243
+ test('uses DeviceSessionGet for Protocol V2 passphrase sessions', async () => {
240
244
  const features = normalizeProtocolV2Features(descriptor as any);
241
- // Pro2 版本线独立于 Pro 系列:V2 设备无论固件版本都支持 GetPassphraseState
242
245
  features.firmwareVersion = '1.2.3';
243
246
  const typedCall = jest.fn().mockResolvedValue({
244
- type: 'PassphraseState',
247
+ type: 'DeviceSession',
245
248
  message: {
246
- passphrase_state: 'state-1',
249
+ btc_test_address: 'state-1',
247
250
  session_id: 'session-1',
248
- unlocked_attach_pin: false,
249
251
  },
250
252
  });
251
253
  const commands = { typedCall } as unknown as DeviceCommands;
@@ -256,25 +258,18 @@ describe('Protocol V2 feature adapter', () => {
256
258
  await getPassphraseState(device, {
257
259
  expectPassphraseState: 'state-1',
258
260
  });
259
- expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
260
- passphrase_state: 'state-1',
261
- });
261
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
262
262
 
263
263
  await getPassphraseState(device, {
264
264
  expectPassphraseState: 'state-2',
265
265
  allowCreateAttachPin: true,
266
266
  });
267
- expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
268
- passphrase_state: 'state-2',
269
- allow_create_attach_pin: true,
270
- });
267
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
271
268
 
272
269
  await getPassphraseState(device, {
273
270
  onlyMainPin: true,
274
271
  });
275
- expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
276
- _only_main_pin: true,
277
- });
272
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
278
273
  });
279
274
 
280
275
  test('returns unified GetPassphraseState object payload for existing Pro devices', async () => {
@@ -302,7 +297,7 @@ describe('Protocol V2 feature adapter', () => {
302
297
  },
303
298
  });
304
299
  method.device = stubDevice({
305
- originalDescriptor: { ...descriptor, protocolType: 'V2' },
300
+ originalDescriptor: { ...descriptor, protocolType: 'V1' },
306
301
  features,
307
302
  commands: { typedCall },
308
303
  updateInternalState,
@@ -335,11 +330,10 @@ describe('Protocol V2 feature adapter', () => {
335
330
  unlockedAttachPin: true,
336
331
  };
337
332
  const typedCall = jest.fn().mockResolvedValue({
338
- type: 'PassphraseState',
333
+ type: 'DeviceSession',
339
334
  message: {
340
- passphrase_state: 'state-pro2',
335
+ btc_test_address: 'state-pro2',
341
336
  session_id: 'session-pro2',
342
- unlocked_attach_pin: false,
343
337
  },
344
338
  });
345
339
  const updateInternalState = jest.fn();
@@ -364,7 +358,7 @@ describe('Protocol V2 feature adapter', () => {
364
358
  await expect(method.run()).resolves.toEqual({
365
359
  passphraseState: 'state-pro2',
366
360
  sessionId: 'session-pro2',
367
- unlockedAttachPin: false,
361
+ unlockedAttachPin: true,
368
362
  passphraseProtection: true,
369
363
  });
370
364
  expect(getFeatures).not.toHaveBeenCalled();
@@ -380,10 +374,9 @@ describe('Protocol V2 feature adapter', () => {
380
374
  unlockedAttachPin: false,
381
375
  };
382
376
  const typedCall = jest.fn().mockResolvedValue({
383
- type: 'PassphraseState',
377
+ type: 'DeviceSession',
384
378
  message: {
385
- passphrase_state: 'state-pro2-new',
386
- unlocked_attach_pin: false,
379
+ btc_test_address: 'state-pro2-new',
387
380
  },
388
381
  });
389
382
  const clearInternalState = jest.fn();
@@ -424,19 +417,18 @@ describe('Protocol V2 feature adapter', () => {
424
417
  test('stores Pro2 passphrase session cache without synthetic device id', async () => {
425
418
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
426
419
  const typedCall = jest.fn().mockResolvedValue({
427
- type: 'PassphraseState',
420
+ type: 'DeviceSession',
428
421
  message: {
429
- passphrase_state: 'state-profile',
422
+ btc_test_address: 'state-profile',
430
423
  session_id: 'session-profile',
431
- unlocked_attach_pin: false,
432
424
  },
433
425
  });
434
426
 
435
427
  (device as any).features = {
436
428
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
437
429
  hw: { serial_no: 'PR2SERIAL' },
438
- fw: { app: { version: '4.15.0' } },
439
- status: { passphrase_protection: true },
430
+ fw: { application: { version: '4.15.0' } },
431
+ status: { passphrase_enabled: true },
440
432
  }),
441
433
  unlocked: true,
442
434
  };
@@ -446,17 +438,16 @@ describe('Protocol V2 feature adapter', () => {
446
438
 
447
439
  device.passphraseState = 'state-profile';
448
440
  expect(device.getInternalState()).toBe('session-profile');
449
- expect(device.getInternalState('LEGACY-ID')).toBeUndefined();
441
+ expect(device.getInternalState('CACHED-ID')).toBeUndefined();
450
442
  });
451
443
 
452
444
  test('stores Pro2 passphrase sessions without selecting them implicitly', async () => {
453
445
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
454
446
  const typedCall = jest.fn().mockResolvedValue({
455
- type: 'PassphraseState',
447
+ type: 'DeviceSession',
456
448
  message: {
457
- passphrase_state: 'state-auto',
449
+ btc_test_address: 'state-auto',
458
450
  session_id: 'session-auto',
459
- unlocked_attach_pin: false,
460
451
  },
461
452
  });
462
453
 
@@ -485,18 +476,15 @@ describe('Protocol V2 feature adapter', () => {
485
476
  expect(device.getInternalState()).toBeUndefined();
486
477
  device.passphraseState = 'state-auto';
487
478
  expect(device.getInternalState()).toBe('session-auto');
488
- expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
489
- passphrase_state: undefined,
490
- });
479
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
491
480
  });
492
481
 
493
482
  test('does not mark Pro2 passphrase enabled from a main PIN session alone', async () => {
494
483
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
495
484
  const typedCall = jest.fn().mockResolvedValue({
496
- type: 'PassphraseState',
485
+ type: 'DeviceSession',
497
486
  message: {
498
487
  session_id: 'main-pin-session',
499
- unlocked_attach_pin: false,
500
488
  },
501
489
  });
502
490
 
@@ -507,7 +495,7 @@ describe('Protocol V2 feature adapter', () => {
507
495
  } as any,
508
496
  {
509
497
  status: {
510
- passphrase_protection: false,
498
+ passphrase_enabled: false,
511
499
  },
512
500
  }
513
501
  );
@@ -525,19 +513,16 @@ describe('Protocol V2 feature adapter', () => {
525
513
  expect(device.features?.passphraseProtection).toBe(false);
526
514
  expect(device.features?.sessionId).toBeNull();
527
515
  expect(device.getInternalState()).toBeUndefined();
528
- expect(typedCall).toHaveBeenLastCalledWith('GetPassphraseState', 'PassphraseState', {
529
- _only_main_pin: true,
530
- });
516
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceSessionGet', 'DeviceSession', {});
531
517
  });
532
518
 
533
519
  test('does not let skipPassphraseCheck hide Pro2 passphrase state mismatch', async () => {
534
520
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
535
521
  const typedCall = jest.fn().mockResolvedValue({
536
- type: 'PassphraseState',
522
+ type: 'DeviceSession',
537
523
  message: {
538
- passphrase_state: 'wrong-state',
524
+ btc_test_address: 'wrong-state',
539
525
  session_id: 'wrong-session',
540
- unlocked_attach_pin: false,
541
526
  },
542
527
  });
543
528
 
@@ -554,9 +539,7 @@ describe('Protocol V2 feature adapter', () => {
554
539
  );
555
540
 
556
541
  expect(device.getInternalState()).toBeUndefined();
557
- expect(typedCall).toHaveBeenNthCalledWith(1, 'GetPassphraseState', 'PassphraseState', {
558
- passphrase_state: 'expected-state',
559
- });
542
+ expect(typedCall).toHaveBeenNthCalledWith(1, 'DeviceSessionGet', 'DeviceSession', {});
560
543
  });
561
544
 
562
545
  test('marks fallback features as unavailable when DeviceInfo is missing', () => {
@@ -569,7 +552,7 @@ describe('Protocol V2 feature adapter', () => {
569
552
  expect(features.firmwarePresent).toBeNull();
570
553
  });
571
554
 
572
- test('uses Protocol V2 hw.device_id and does not fall back to serial_no', () => {
555
+ test('uses Protocol V2 status.device_id and does not fall back to serial_no', () => {
573
556
  const features = normalizeProtocolV2Features(
574
557
  {
575
558
  id: 'PR2000000000',
@@ -579,6 +562,8 @@ describe('Protocol V2 feature adapter', () => {
579
562
  {
580
563
  hw: {
581
564
  serial_no: 'PR9999999999',
565
+ },
566
+ status: {
582
567
  device_id: 'DEVICE-ID-9999',
583
568
  },
584
569
  }
@@ -610,17 +595,17 @@ describe('Protocol V2 feature adapter', () => {
610
595
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
611
596
  (device as any).features = {
612
597
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
613
- serialNo: 'LEGACY-SERIAL',
614
- label: 'Legacy Label',
615
- bleName: 'Legacy BLE',
598
+ serialNo: 'CACHED-SERIAL',
599
+ label: 'Cached Label',
600
+ bleName: 'Cached BLE',
616
601
  passphraseProtection: true,
617
602
  };
618
603
 
619
604
  expect(device.toMessageObject()).toMatchObject({
620
- uuid: 'LEGACY-SERIAL',
605
+ uuid: 'CACHED-SERIAL',
621
606
  deviceId: null,
622
- bleName: 'Legacy BLE',
623
- label: 'Legacy Label',
607
+ bleName: 'Cached BLE',
608
+ label: 'Cached Label',
624
609
  deviceType: 'pro2',
625
610
  });
626
611
  expect(device.getCurrentPassphraseProtection()).toBe(true);
@@ -632,52 +617,53 @@ describe('Protocol V2 feature adapter', () => {
632
617
  (cached as any).features = {
633
618
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
634
619
  deviceId: null,
635
- serialNo: 'LEGACY-SERIAL',
620
+ serialNo: 'CACHED-SERIAL',
636
621
  };
637
622
 
638
623
  const current = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
639
624
  current.updateFromCache(cached);
640
625
 
641
626
  expect(current.getCurrentDeviceId()).toBeUndefined();
642
- expect(current.getCurrentSerialNo()).toBe('LEGACY-SERIAL');
627
+ expect(current.getCurrentSerialNo()).toBe('CACHED-SERIAL');
643
628
  expect(current.toMessageObject()).toMatchObject({
644
- uuid: 'LEGACY-SERIAL',
629
+ uuid: 'CACHED-SERIAL',
645
630
  deviceId: null,
646
631
  });
647
632
  });
648
633
 
649
- test('initializes Protocol V2 features from lightweight DeviceGetDeviceInfo', async () => {
634
+ test('initializes Protocol V2 features from lightweight DeviceInfoGet', async () => {
650
635
  const commands = {
651
636
  typedCall: jest.fn().mockResolvedValueOnce({
652
637
  type: 'DeviceInfo',
653
638
  message: {
654
639
  hw: { serial_no: 'PR2SERIAL' },
655
640
  status: {
641
+ device_id: 'PRO2-STATUS-ID',
656
642
  init_states: true,
657
- passphrase_protection: true,
643
+ passphrase_enabled: true,
658
644
  },
659
645
  },
660
646
  }),
661
647
  };
662
648
 
663
- const features = await requestProtocolV2LegacyFeatures({
649
+ const features = await requestProtocolV2Features({
664
650
  commands: commands as unknown as DeviceCommands,
665
651
  descriptor: descriptor as any,
666
652
  });
667
653
 
668
- expect(features.deviceId).toBeNull();
654
+ expect(features.deviceId).toBe('PRO2-STATUS-ID');
669
655
  expect(features.initialized).toBe(true);
670
656
  expect(features.passphraseProtection).toBe(true);
671
657
  expect(commands.typedCall).toHaveBeenCalledTimes(1);
672
658
  expect(commands.typedCall).toHaveBeenNthCalledWith(
673
659
  1,
674
- 'DevGetDeviceInfo',
660
+ 'DeviceInfoGet',
675
661
  'DeviceInfo',
676
662
  {
677
663
  targets: {
678
664
  hw: true,
679
665
  fw: true,
680
- bt: true,
666
+ coprocessor: true,
681
667
  status: true,
682
668
  },
683
669
  types: {
@@ -689,13 +675,13 @@ describe('Protocol V2 feature adapter', () => {
689
675
  );
690
676
  });
691
677
 
692
- test('fails initialization when Protocol V2 DeviceGetDeviceInfo fails', async () => {
678
+ test('fails initialization when Protocol V2 DeviceInfoGet fails', async () => {
693
679
  const commands = {
694
680
  typedCall: jest.fn().mockRejectedValueOnce(new Error('DeviceInfo not supported')),
695
681
  };
696
682
 
697
683
  await expect(
698
- requestProtocolV2LegacyFeatures({
684
+ requestProtocolV2Features({
699
685
  commands: commands as unknown as DeviceCommands,
700
686
  descriptor: descriptor as any,
701
687
  })
@@ -708,20 +694,21 @@ describe('Protocol V2 feature adapter', () => {
708
694
  message: {
709
695
  hw: { serial_no: 'PR2SERIAL' },
710
696
  fw: {
711
- app: {
697
+ application: {
712
698
  version: '5.6.7',
713
699
  },
714
700
  },
715
- bt: {
716
- app: {
701
+ coprocessor: {
702
+ application: {
717
703
  version: '8.9.10',
718
704
  },
719
- adv_name: 'Raw Pro2 BLE',
705
+ bt_adv_name: 'Raw Pro2 BLE',
720
706
  },
721
707
  status: {
708
+ device_id: 'PRO2-BASIC-ID',
709
+ unlocked_by_attach_to_pin: true,
722
710
  init_states: true,
723
- label: 'Raw Pro2',
724
- passphrase_protection: true,
711
+ passphrase_enabled: true,
725
712
  },
726
713
  },
727
714
  });
@@ -738,9 +725,9 @@ describe('Protocol V2 feature adapter', () => {
738
725
  originalDescriptor: { ...descriptor, protocolType: 'V2' },
739
726
  features: {
740
727
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
741
- device_id: 'LEGACY-ID',
742
- serial_no: 'LEGACY-SERIAL',
743
- label: 'Legacy Pro2',
728
+ device_id: 'CACHED-ID',
729
+ serial_no: 'CACHED-SERIAL',
730
+ label: 'Cached Pro2',
744
731
  onekey_firmware_version: '1.2.3',
745
732
  onekey_ble_version: '2.3.4',
746
733
  },
@@ -751,13 +738,13 @@ describe('Protocol V2 feature adapter', () => {
751
738
  const result = await method.run();
752
739
 
753
740
  expect(typedCall).toHaveBeenCalledWith(
754
- 'DevGetDeviceInfo',
741
+ 'DeviceInfoGet',
755
742
  'DeviceInfo',
756
743
  {
757
744
  targets: {
758
745
  hw: true,
759
746
  fw: true,
760
- bt: true,
747
+ coprocessor: true,
761
748
  status: true,
762
749
  },
763
750
  types: {
@@ -770,12 +757,13 @@ describe('Protocol V2 feature adapter', () => {
770
757
  expect(result).toMatchObject({
771
758
  protocol: 'V2',
772
759
  deviceType: 'pro2',
773
- deviceId: '',
760
+ deviceId: 'PRO2-BASIC-ID',
774
761
  serialNo: 'PR2SERIAL',
775
- label: 'Raw Pro2',
762
+ label: null,
776
763
  bleName: 'Raw Pro2 BLE',
777
764
  status: {
778
765
  initialized: true,
766
+ unlockedAttachPin: true,
779
767
  passphraseProtection: true,
780
768
  },
781
769
  versions: {
@@ -785,7 +773,7 @@ describe('Protocol V2 feature adapter', () => {
785
773
  });
786
774
  });
787
775
 
788
- test('does not fill Protocol V2 DeviceProfile from legacy features', async () => {
776
+ test('does not fill Protocol V2 DeviceProfile from cached V1-shaped fields', async () => {
789
777
  const typedCall = jest.fn().mockResolvedValueOnce({
790
778
  type: 'DeviceInfo',
791
779
  message: {
@@ -806,10 +794,10 @@ describe('Protocol V2 feature adapter', () => {
806
794
  originalDescriptor: { ...descriptor, protocolType: 'V2' },
807
795
  features: {
808
796
  ...normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any),
809
- device_id: 'LEGACY-ID',
810
- serial_no: 'LEGACY-SERIAL',
811
- label: 'Legacy Pro2',
812
- ble_name: 'Legacy BLE',
797
+ device_id: 'STALE-ID',
798
+ serial_no: 'STALE-SERIAL',
799
+ label: 'Stale Pro2',
800
+ ble_name: 'Stale BLE',
813
801
  passphrase_protection: true,
814
802
  onekey_firmware_version: '1.2.3',
815
803
  onekey_ble_version: '2.3.4',
@@ -821,7 +809,7 @@ describe('Protocol V2 feature adapter', () => {
821
809
 
822
810
  expect(result).toMatchObject({
823
811
  protocol: 'V2',
824
- // 身份字段不得取自 legacy features(LEGACY-ID / LEGACY-SERIAL),
812
+ // 身份字段不得取自缓存的 V1-shaped features(STALE-ID / STALE-SERIAL),
825
813
  // hw.serial_no 缺失时也不回退 descriptor.path。
826
814
  deviceId: '',
827
815
  serialNo: '',
@@ -864,13 +852,13 @@ describe('Protocol V2 feature adapter', () => {
864
852
  await method.run();
865
853
 
866
854
  expect(typedCall).toHaveBeenCalledWith(
867
- 'DevGetDeviceInfo',
855
+ 'DeviceInfoGet',
868
856
  'DeviceInfo',
869
857
  {
870
858
  targets: {
871
859
  hw: true,
872
860
  fw: true,
873
- bt: true,
861
+ coprocessor: true,
874
862
  se1: true,
875
863
  se2: true,
876
864
  se3: true,
@@ -894,12 +882,12 @@ describe('Protocol V2 feature adapter', () => {
894
882
  message: {
895
883
  hw: { serial_no: 'PR2SERIAL' },
896
884
  se1: {
897
- app: { version: '1.0.1' },
898
- boot: { version: '1.0.0' },
885
+ application: { version: '1.0.1' },
886
+ bootloader: { version: '1.0.0' },
899
887
  },
900
888
  se2: {
901
- app: { version: '2.0.1' },
902
- boot: { version: '2.0.0' },
889
+ application: { version: '2.0.1' },
890
+ bootloader: { version: '2.0.0' },
903
891
  },
904
892
  status: { init_states: true },
905
893
  },
@@ -921,13 +909,13 @@ describe('Protocol V2 feature adapter', () => {
921
909
  const result = await method.run();
922
910
 
923
911
  expect(typedCall).toHaveBeenCalledWith(
924
- 'DevGetDeviceInfo',
912
+ 'DeviceInfoGet',
925
913
  'DeviceInfo',
926
914
  {
927
915
  targets: {
928
916
  hw: true,
929
917
  fw: true,
930
- bt: true,
918
+ coprocessor: true,
931
919
  se1: true,
932
920
  se2: true,
933
921
  se3: true,
@@ -966,10 +954,23 @@ describe('Protocol V2 feature adapter', () => {
966
954
  expect(checkedTypes).toEqual(['pro2']);
967
955
  });
968
956
 
957
+ test('uses firmware-v1 as the Pro2 remote firmware config field', () => {
958
+ const features = normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any);
959
+
960
+ expect(
961
+ getFirmwareUpdateField({
962
+ features,
963
+ updateType: 'firmware',
964
+ firmwareType: 'universal',
965
+ })
966
+ ).toBe('firmware-v1');
967
+ expect(getFirmwareUpdateFieldArray(features, 'firmware')).toEqual(['firmware-v1']);
968
+ });
969
+
969
970
  test('marks known unsupported public-chain methods as unsupported on Protocol V2', () => {
970
971
  const features = normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
971
972
  fw: {
972
- app: {
973
+ application: {
973
974
  version: '9.9.9',
974
975
  },
975
976
  },
@@ -1019,7 +1020,7 @@ describe('Protocol V2 feature adapter', () => {
1019
1020
  expect(isMethodVersionRangeUnsupported(neuraiRange)).toBe(true);
1020
1021
  });
1021
1022
 
1022
- test('does not block legacy batch public key support checks on Protocol V2', async () => {
1023
+ test('does not block batch public key support checks on Protocol V2', async () => {
1023
1024
  const paths = [{ address_n: [0x8000002c, 0x80000000, 0x80000000] }] as any;
1024
1025
  const typedCall = jest.fn().mockResolvedValue({
1025
1026
  type: 'EcdsaPublicKeys',
@@ -1033,7 +1034,7 @@ describe('Protocol V2 feature adapter', () => {
1033
1034
  originalDescriptor: { protocolType: 'V2' },
1034
1035
  features: normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, {
1035
1036
  fw: {
1036
- app: {
1037
+ application: {
1037
1038
  version: '4.14.0',
1038
1039
  },
1039
1040
  },
@@ -1055,7 +1056,7 @@ describe('Protocol V2 feature adapter', () => {
1055
1056
  });
1056
1057
  });
1057
1058
 
1058
- test('does not call legacy OnekeyGetFeatures for Protocol V2 devices', async () => {
1059
+ test('does not call V1 OnekeyGetFeatures for Protocol V2 devices', async () => {
1059
1060
  const method = new GetOnekeyFeatures({
1060
1061
  id: 1,
1061
1062
  payload: {
@@ -1067,9 +1068,9 @@ describe('Protocol V2 feature adapter', () => {
1067
1068
  message: {
1068
1069
  protocol_version: 1,
1069
1070
  hw: { serial_no: 'PR2SERIAL' },
1070
- fw: { app: { version: '1.2.3', build_id: 'app-build' } },
1071
- bt: { adv_name: 'Pro2 BLE' },
1072
- status: { label: 'ignored label' },
1071
+ fw: { application: { version: '1.2.3', build_id: 'app-build' } },
1072
+ coprocessor: { bt_adv_name: 'Pro2 BLE' },
1073
+ status: { init_states: true },
1073
1074
  },
1074
1075
  });
1075
1076
 
@@ -1080,10 +1081,10 @@ describe('Protocol V2 feature adapter', () => {
1080
1081
 
1081
1082
  const message = await method.run();
1082
1083
 
1083
- // V2 走 DeviceGetDeviceInfo 完整请求(含 SE 与 hash/build_id),而不是 legacy OnekeyGetFeatures
1084
+ // V2 走 DeviceInfoGet 完整请求(含 SE 与 hash/build_id),而不是 V1 OnekeyGetFeatures
1084
1085
  expect(typedCall).toHaveBeenCalledTimes(1);
1085
1086
  expect(typedCall).toHaveBeenCalledWith(
1086
- 'DevGetDeviceInfo',
1087
+ 'DeviceInfoGet',
1087
1088
  'DeviceInfo',
1088
1089
  expect.objectContaining({
1089
1090
  targets: expect.objectContaining({ se1: true, se2: true, se3: true, se4: true }),
@@ -1106,9 +1107,10 @@ describe('Protocol V2 feature adapter', () => {
1106
1107
  type: 'DeviceInfo',
1107
1108
  message: {
1108
1109
  hw: { serial_no: 'PR2SERIAL' },
1109
- fw: { app: { version: '1.2.3' } },
1110
+ fw: { application: { version: '1.2.3' } },
1110
1111
  status: {
1111
- passphrase_protection: true,
1112
+ device_id: 'PRO2-REFRESH-ID',
1113
+ passphrase_enabled: true,
1112
1114
  },
1113
1115
  },
1114
1116
  })
@@ -1118,8 +1120,8 @@ describe('Protocol V2 feature adapter', () => {
1118
1120
  message: {
1119
1121
  hw: { serial_no: 'PR2SERIAL' },
1120
1122
  status: {
1121
- passphrase_protection: false,
1122
- label: 'renamed',
1123
+ device_id: 'PRO2-REFRESH-ID',
1124
+ passphrase_enabled: false,
1123
1125
  },
1124
1126
  },
1125
1127
  });
@@ -1130,27 +1132,27 @@ describe('Protocol V2 feature adapter', () => {
1130
1132
  await device.initialize();
1131
1133
 
1132
1134
  expect(device.features).toMatchObject({
1133
- deviceId: null,
1135
+ deviceId: 'PRO2-REFRESH-ID',
1134
1136
  firmwareVersion: '1.2.3',
1135
1137
  passphraseProtection: false,
1136
- label: 'renamed',
1138
+ label: null,
1137
1139
  });
1138
1140
  expect((device as any).profile).toBeUndefined();
1139
1141
  // status 字段被第二次刷新更新
1140
1142
  expect(device.features?.passphraseProtection).toBe(false);
1141
- expect(device.features?.label).toBe('renamed');
1143
+ expect(device.features?.label).toBeNull();
1142
1144
  // 轻量刷新不含 fw target,已有版本信息按字段级合并保留
1143
1145
  expect(device.features?.firmwareVersion).toBe('1.2.3');
1144
1146
  expect(typedCall).toHaveBeenCalledTimes(2);
1145
1147
  expect(typedCall).toHaveBeenNthCalledWith(
1146
1148
  1,
1147
- 'DevGetDeviceInfo',
1149
+ 'DeviceInfoGet',
1148
1150
  'DeviceInfo',
1149
1151
  {
1150
1152
  targets: {
1151
1153
  hw: true,
1152
1154
  fw: true,
1153
- bt: true,
1155
+ coprocessor: true,
1154
1156
  status: true,
1155
1157
  },
1156
1158
  types: {
@@ -1162,15 +1164,15 @@ describe('Protocol V2 feature adapter', () => {
1162
1164
  timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS,
1163
1165
  }
1164
1166
  );
1165
- // 第二次为 status-only 轻量请求(hw/bt 仅用于身份字段,避免顶层覆盖清空)
1167
+ // 第二次为 status-only 轻量请求(hw/coprocessor 仅用于身份字段,避免顶层覆盖清空)
1166
1168
  expect(typedCall).toHaveBeenNthCalledWith(
1167
1169
  2,
1168
- 'DevGetDeviceInfo',
1170
+ 'DeviceInfoGet',
1169
1171
  'DeviceInfo',
1170
1172
  {
1171
1173
  targets: {
1172
1174
  hw: true,
1173
- bt: true,
1175
+ coprocessor: true,
1174
1176
  status: true,
1175
1177
  },
1176
1178
  types: {
@@ -1184,7 +1186,7 @@ describe('Protocol V2 feature adapter', () => {
1184
1186
  );
1185
1187
  });
1186
1188
 
1187
- test('refreshes Protocol V2 features without falling back to legacy GetFeatures', async () => {
1189
+ test('refreshes Protocol V2 features without falling back to V1 GetFeatures', async () => {
1188
1190
  const device = Device.fromDescriptor({
1189
1191
  path: 'usb-path',
1190
1192
  protocolType: 'V2',
@@ -1195,7 +1197,7 @@ describe('Protocol V2 feature adapter', () => {
1195
1197
  type: 'DeviceInfo',
1196
1198
  message: {
1197
1199
  hw: { serial_no: 'PR2SERIAL' },
1198
- fw: { app: { version: '1.2.3' } },
1200
+ fw: { application: { version: '1.2.3' } },
1199
1201
  status: { init_states: true },
1200
1202
  },
1201
1203
  })
@@ -1203,8 +1205,8 @@ describe('Protocol V2 feature adapter', () => {
1203
1205
  type: 'DeviceInfo',
1204
1206
  message: {
1205
1207
  hw: { serial_no: 'PR2SERIAL' },
1206
- fw: { app: { version: '1.2.4' } },
1207
- status: { init_states: true, passphrase_protection: true },
1208
+ fw: { application: { version: '1.2.4' } },
1209
+ status: { init_states: true, passphrase_enabled: true },
1208
1210
  },
1209
1211
  });
1210
1212
 
@@ -1227,13 +1229,13 @@ describe('Protocol V2 feature adapter', () => {
1227
1229
  expect(typedCall).toHaveBeenCalledTimes(2);
1228
1230
  expect(typedCall).toHaveBeenNthCalledWith(
1229
1231
  2,
1230
- 'DevGetDeviceInfo',
1232
+ 'DeviceInfoGet',
1231
1233
  'DeviceInfo',
1232
1234
  {
1233
1235
  targets: {
1234
1236
  hw: true,
1235
1237
  fw: true,
1236
- bt: true,
1238
+ coprocessor: true,
1237
1239
  status: true,
1238
1240
  },
1239
1241
  types: {
@@ -1246,14 +1248,14 @@ describe('Protocol V2 feature adapter', () => {
1246
1248
  expect(typedCall).not.toHaveBeenCalledWith('GetFeatures', 'Features', {});
1247
1249
  });
1248
1250
 
1249
- test('keeps Protocol V2 features available for legacy method internals such as evmSignTypedData', async () => {
1251
+ test('keeps Protocol V2 features available for method internals such as evmSignTypedData', async () => {
1250
1252
  const device = Device.fromDescriptor({ ...descriptor, protocolType: 'V2' } as any);
1251
1253
  const typedCall = jest.fn().mockResolvedValueOnce({
1252
1254
  type: 'DeviceInfo',
1253
1255
  message: {
1254
1256
  hw: { serial_no: 'PR2SERIAL' },
1255
- fw: { app: { version: '1.2.4' } },
1256
- status: { init_states: true, passphrase_protection: true },
1257
+ fw: { application: { version: '1.2.4' } },
1258
+ status: { init_states: true, passphrase_enabled: true },
1257
1259
  },
1258
1260
  });
1259
1261
  (device as any).commands = { typedCall };
@@ -1307,7 +1309,7 @@ describe('Protocol V2 feature adapter', () => {
1307
1309
  { ...descriptor, protocolType: 'V2' } as any,
1308
1310
  {
1309
1311
  hw: { serial_no: 'PR2SERIAL' },
1310
- fw: { app: { version: '1.2.3' } },
1312
+ fw: { application: { version: '1.2.3' } },
1311
1313
  status: { init_states: true },
1312
1314
  }
1313
1315
  );
@@ -1331,14 +1333,15 @@ describe('Protocol V2 feature adapter', () => {
1331
1333
  { ...descriptor, protocolType: 'V2' } as any,
1332
1334
  {
1333
1335
  hw: { serial_no: 'PR2SERIAL' },
1334
- fw: { app: { version: '4.15.0' } },
1335
- status: { passphrase_protection: false },
1336
+ fw: { application: { version: '4.15.0' } },
1337
+ status: { passphrase_enabled: false },
1336
1338
  }
1337
1339
  );
1338
1340
  const typedCall = jest.fn().mockResolvedValue({
1339
1341
  type: 'UnLockDeviceResponse',
1340
1342
  message: {
1341
1343
  unlocked: true,
1344
+ unlocked_attach_pin: true,
1342
1345
  passphrase_protection: true,
1343
1346
  },
1344
1347
  });
@@ -1348,6 +1351,7 @@ describe('Protocol V2 feature adapter', () => {
1348
1351
 
1349
1352
  expect((device as any).profile).toBeUndefined();
1350
1353
  expect(device.features?.passphraseProtection).toBe(true);
1354
+ expect(device.features?.unlockedAttachPin).toBe(true);
1351
1355
  });
1352
1356
  });
1353
1357
 
@@ -1384,25 +1388,7 @@ describe('API compatibility handling', () => {
1384
1388
  );
1385
1389
  });
1386
1390
 
1387
- test('returns a typed unsupported error for Tron sign message V1 before device binding', () => {
1388
- const method = new TronSignMessage({
1389
- id: 1,
1390
- payload: {
1391
- method: 'tronSignMessage',
1392
- path: "m/44'/195'/0'/0/0",
1393
- messageHex: '0x1234',
1394
- messageType: 'V1',
1395
- },
1396
- });
1397
-
1398
- expect(() => method.init()).toThrow(
1399
- expect.objectContaining({
1400
- errorCode: HardwareErrorCode.DeviceNotSupportMethod,
1401
- })
1402
- );
1403
- });
1404
-
1405
- test('does not mark Pro2 Tron, Solana, TON, SUI and Polkadot methods as unsupported', () => {
1391
+ test('does not mark supported Pro2 Tron, Solana, TON, SUI and Polkadot methods as unsupported', () => {
1406
1392
  const features = {
1407
1393
  deviceType: 'pro2',
1408
1394
  } as Features;
@@ -1523,9 +1509,7 @@ describe('API compatibility handling', () => {
1523
1509
  });
1524
1510
  expect(
1525
1511
  getMethodVersionRange(features, type => tonSignDataMethod.getVersionRange()[type])
1526
- ).toEqual({
1527
- min: '0.0.0',
1528
- });
1512
+ ).toBeUndefined();
1529
1513
  expect(
1530
1514
  getMethodVersionRange(features, type => suiGetAddressMethod.getVersionRange()[type])
1531
1515
  ).toEqual({
@@ -1619,15 +1603,6 @@ describe('API compatibility handling', () => {
1619
1603
  });
1620
1604
  });
1621
1605
 
1622
- test('includes TON signData in the Protocol V2 protobuf schema', () => {
1623
- const protocolV2Messages = DataManager.getProtobufMessages('v2Schema') as any;
1624
-
1625
- expect(protocolV2Messages.nested.TonSignData).toBeDefined();
1626
- expect(protocolV2Messages.nested.TonSignedData).toBeDefined();
1627
- expect(protocolV2Messages.nested.MessageType.values.MessageType_TonSignData).toBe(11908);
1628
- expect(protocolV2Messages.nested.MessageType.values.MessageType_TonSignedData).toBe(11909);
1629
- });
1630
-
1631
1606
  test('uses chunk transfer for large Sui transactions on Protocol V2', async () => {
1632
1607
  const rawTx = '0x'.concat('ab'.repeat(5000));
1633
1608
  const typedCall = jest.fn(() => ({
@@ -1672,54 +1647,6 @@ describe('API compatibility handling', () => {
1672
1647
  expect(params.data_initial_chunk).toHaveLength(2048);
1673
1648
  });
1674
1649
 
1675
- test('accepts string XRP payment amount values', () => {
1676
- const method = new XrpSignTransaction({
1677
- id: 1,
1678
- payload: {
1679
- method: 'xrpSignTransaction',
1680
- path: "m/44'/144'/0'/0/0",
1681
- transaction: {
1682
- fee: '100000',
1683
- flags: 2147483648,
1684
- sequence: 25,
1685
- maxLedgerVersion: 8820051,
1686
- payment: {
1687
- amount: '100000000',
1688
- destination: 'rBKz5MC2iXdoS3XgnNSYmF69K1Yo4NS3Ws',
1689
- },
1690
- },
1691
- },
1692
- });
1693
-
1694
- expect(() => method.init()).not.toThrow();
1695
- expect(method.params.payment?.amount).toBe('100000000');
1696
- });
1697
-
1698
- test('accepts Conflux base32 recipient addresses without hex formatting them', () => {
1699
- const to = 'cfx:aak2rra2njvd77ezwjvx04kkds9fzagfe6ku8scz91';
1700
- const method = new ConfluxSignTransaction({
1701
- id: 1,
1702
- payload: {
1703
- method: 'confluxSignTransaction',
1704
- path: "m/44'/503'/0'/0/0",
1705
- transaction: {
1706
- to,
1707
- value: '0x0',
1708
- data: '0x',
1709
- chainId: 1,
1710
- nonce: '0x00',
1711
- epochHeight: '0x00',
1712
- gasLimit: '0x5208',
1713
- storageLimit: '0x5208',
1714
- gasPrice: '0xbebc200',
1715
- },
1716
- },
1717
- });
1718
-
1719
- expect(() => method.init()).not.toThrow();
1720
- expect(method.formattedTx?.to).toBe(to);
1721
- });
1722
-
1723
1650
  test('returns a typed unsupported error for Dynex signing on Protocol V2', async () => {
1724
1651
  const method = new DnxSignTransaction({
1725
1652
  id: 1,
@@ -1775,7 +1702,41 @@ describe('API compatibility handling', () => {
1775
1702
  });
1776
1703
 
1777
1704
  describe('Protocol V2 firmware update targets', () => {
1778
- test('keeps Protocol V2 firmware updates off the legacy firmwareUpdateV3 path', async () => {
1705
+ const OKPP_HEADER_SIZE = 0x52a0;
1706
+
1707
+ const buildOkppHeader = ({
1708
+ type = 'RESC',
1709
+ version = [1, 0, 0],
1710
+ payloadHash = 'ab'.repeat(64),
1711
+ headerHash = 'cd'.repeat(64),
1712
+ }: {
1713
+ type?: string;
1714
+ version?: [number, number, number];
1715
+ payloadHash?: string;
1716
+ headerHash?: string;
1717
+ } = {}) => {
1718
+ const header = new Uint8Array(OKPP_HEADER_SIZE);
1719
+ const view = new DataView(header.buffer);
1720
+ 'OKPP'.split('').forEach((char, index) => {
1721
+ header[index] = char.charCodeAt(0);
1722
+ });
1723
+ view.setUint32(0x04, 1, true);
1724
+ type.split('').forEach((char, index) => {
1725
+ header[0x08 + index] = char.charCodeAt(0);
1726
+ });
1727
+ view.setUint32(0x0c, OKPP_HEADER_SIZE, true);
1728
+ view.setUint32(0x10, version[0] * 0x10000 + version[1] * 0x100 + version[2], true);
1729
+ const writeHex = (offset: number, hex: string) => {
1730
+ for (let i = 0; i < hex.length / 2; i++) {
1731
+ header[offset + i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
1732
+ }
1733
+ };
1734
+ writeHex(0x200, payloadHash);
1735
+ writeHex(0x240, headerHash);
1736
+ return header;
1737
+ };
1738
+
1739
+ test('keeps Protocol V2 firmware updates off the firmwareUpdateV3 path', async () => {
1779
1740
  const method = new FirmwareUpdateV3({
1780
1741
  id: 1,
1781
1742
  payload: {
@@ -1789,25 +1750,26 @@ describe('Protocol V2 firmware update targets', () => {
1789
1750
  await expect(method.run()).rejects.toThrow('firmwareUpdateV4');
1790
1751
  });
1791
1752
 
1792
- test('uses Protocol V2 features after BLE final reconnect without legacy Initialize', async () => {
1753
+ test('uses Protocol V2 features after BLE final reconnect without V1 Initialize', async () => {
1793
1754
  const method = new FirmwareUpdateV4({
1794
1755
  id: 1,
1795
1756
  payload: {
1796
1757
  method: 'firmwareUpdateV4',
1758
+ coprocessorBinary: new Uint8Array([1, 2, 3]).buffer,
1797
1759
  },
1798
1760
  });
1799
1761
  const acquire = jest.fn().mockResolvedValue({ uuid: 'ble-session' });
1800
1762
  const typedCall = jest.fn().mockImplementation((name: string) => {
1801
- if (name === 'DevGetDeviceInfo') {
1763
+ if (name === 'DeviceInfoGet') {
1802
1764
  return Promise.resolve({
1803
1765
  type: 'DeviceInfo',
1804
1766
  message: {
1805
1767
  fw: {
1806
- boot: { version: '0.0.0' },
1807
- app: { version: '0.0.0' },
1768
+ bootloader: { version: '0.0.0' },
1769
+ application: { version: '0.0.0' },
1808
1770
  },
1809
- bt: {
1810
- app: { version: '0.0.0' },
1771
+ coprocessor: {
1772
+ application: { version: '0.0.0' },
1811
1773
  },
1812
1774
  },
1813
1775
  });
@@ -1821,7 +1783,13 @@ describe('Protocol V2 firmware update targets', () => {
1821
1783
  originalDescriptor: { id: 'ble-id', path: 'ble-path', protocolType: 'V2' },
1822
1784
  deviceConnector: { acquire },
1823
1785
  getCommands: () => commands,
1824
- _updateFeatures: jest.fn(),
1786
+ updateProtocolV2Features: jest.fn(() => ({
1787
+ bootloaderMode: false,
1788
+ mode: 'normal',
1789
+ firmwareVersion: '0.0.0',
1790
+ bootloaderVersion: '0.0.0',
1791
+ bleVersion: '0.0.0',
1792
+ })),
1825
1793
  });
1826
1794
 
1827
1795
  const versions = await (method as any).waitForProtocolV2FinalFeatures();
@@ -1830,13 +1798,13 @@ describe('Protocol V2 firmware update targets', () => {
1830
1798
  // 更新完成判定使用 VERSIONS 请求(含 SE targets),scope 与请求内容一致
1831
1799
  expect(typedCall).toHaveBeenNthCalledWith(
1832
1800
  1,
1833
- 'DevGetDeviceInfo',
1801
+ 'DeviceInfoGet',
1834
1802
  'DeviceInfo',
1835
1803
  {
1836
1804
  targets: {
1837
1805
  hw: true,
1838
1806
  fw: true,
1839
- bt: true,
1807
+ coprocessor: true,
1840
1808
  se1: true,
1841
1809
  se2: true,
1842
1810
  se3: true,
@@ -1859,7 +1827,7 @@ describe('Protocol V2 firmware update targets', () => {
1859
1827
  });
1860
1828
  });
1861
1829
 
1862
- test('skips automatic Protocol V2 bootloader entry before upload while boot flow is disabled', async () => {
1830
+ test('enters Protocol V2 bootloader before upload', async () => {
1863
1831
  const method = new FirmwareUpdateV4({
1864
1832
  id: 1,
1865
1833
  payload: {
@@ -1872,14 +1840,15 @@ describe('Protocol V2 firmware update targets', () => {
1872
1840
  originalDescriptor: { id: 'ble-id', path: 'ble-path', protocolType: 'V2' },
1873
1841
  features: { capabilities: [] },
1874
1842
  });
1875
- (method as any).prepareResourceBinary = jest.fn().mockResolvedValue(null);
1876
- (method as any).prepareFirmwareAndBleBinary = jest.fn().mockResolvedValue([
1843
+ (method as any).prepareResourceBinaries = jest.fn().mockResolvedValue([]);
1844
+ (method as any).prepareBootloaderBinary = jest.fn().mockReturnValue(null);
1845
+ (method as any).collectExplicitTargetBinaries = jest.fn().mockReturnValue([
1877
1846
  {
1878
- fileName: 'ble-firmware.bin',
1847
+ fileName: 'coprocessor.bin',
1879
1848
  binary: new Uint8Array([1, 2, 3]).buffer,
1849
+ targetId: 6,
1880
1850
  },
1881
1851
  ]);
1882
- (method as any).prepareBootloaderBinary = jest.fn().mockResolvedValue(null);
1883
1852
  (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
1884
1853
  (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
1885
1854
  (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
@@ -1893,17 +1862,18 @@ describe('Protocol V2 firmware update targets', () => {
1893
1862
 
1894
1863
  await method.run();
1895
1864
 
1865
+ expect((method as any).enterProtocolV2BootloaderMode).toHaveBeenCalledTimes(1);
1896
1866
  expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
1897
- resourceBinary: null,
1867
+ resourceBinaryMap: [],
1898
1868
  fwBinaryMap: [
1899
1869
  {
1900
- fileName: 'ble-firmware.bin',
1870
+ fileName: 'coprocessor.bin',
1901
1871
  binary: expect.any(ArrayBuffer),
1872
+ targetId: 6,
1902
1873
  },
1903
1874
  ],
1904
1875
  bootloaderBinary: null,
1905
1876
  });
1906
- expect((method as any).enterProtocolV2BootloaderMode).not.toHaveBeenCalled();
1907
1877
  });
1908
1878
 
1909
1879
  test('reboots Protocol V2 normal-mode device to bootloader before transfer', async () => {
@@ -1913,31 +1883,70 @@ describe('Protocol V2 firmware update targets', () => {
1913
1883
  method: 'firmwareUpdateV4',
1914
1884
  },
1915
1885
  });
1916
- const acquire = jest.fn().mockResolvedValue(undefined);
1917
1886
  (method as any).device = stubDevice({
1918
1887
  originalDescriptor: { id: 'usb-id', path: 'usb-path', protocolType: 'V2' },
1919
1888
  features: { bootloader_mode: false, capabilities: [] },
1920
1889
  isBootloader: () => false,
1921
- acquire,
1922
1890
  });
1923
1891
  (method as any).protocolV2Reboot = jest.fn().mockResolvedValue({
1924
1892
  message: 'Device rebooted successfully',
1925
1893
  });
1926
- (method as any).checkDeviceToBootloader = jest.fn(() => {
1927
- (method as any).checkPromise = { promise: Promise.resolve(true) };
1894
+ (method as any).checkDeviceToBootloader = jest.fn();
1895
+ const typedCall = jest.fn().mockResolvedValue({
1896
+ type: 'DeviceInfo',
1897
+ message: protocolV2BootloaderDeviceInfo,
1898
+ });
1899
+ const reconnectProtocolV2Device = jest.fn().mockImplementation(() => {
1900
+ (method as any).device.isBootloader = () => true;
1901
+ return Promise.resolve();
1928
1902
  });
1903
+ (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
1904
+ (method as any).device.getCommands = () => ({ typedCall });
1929
1905
  method.postTipMessage = jest.fn();
1930
1906
 
1931
1907
  await (method as any).enterProtocolV2BootloaderMode();
1932
1908
 
1933
1909
  expect(method.postTipMessage).toHaveBeenCalledWith('AutoRebootToBootloader');
1934
- expect((method as any).protocolV2Reboot).toHaveBeenCalledWith(DevRebootType.Bootloader);
1910
+ expect((method as any).protocolV2Reboot).toHaveBeenCalledWith(DeviceRebootType.Bootloader);
1935
1911
  expect(method.postTipMessage).toHaveBeenCalledWith('GoToBootloaderSuccess');
1936
- expect((method as any).checkDeviceToBootloader).toHaveBeenCalledWith(undefined);
1937
- expect(acquire).toHaveBeenCalledTimes(1);
1912
+ expect((method as any).checkDeviceToBootloader).not.toHaveBeenCalled();
1913
+ expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
1914
+ });
1915
+
1916
+ test('polls until Protocol V2 bootloader descriptor is ready after reboot', async () => {
1917
+ const method = new FirmwareUpdateV4({
1918
+ id: 1,
1919
+ payload: {
1920
+ method: 'firmwareUpdateV4',
1921
+ },
1922
+ });
1923
+ (method as any).device = stubDevice({
1924
+ originalDescriptor: { id: 'usb-id', path: 'app-path', protocolType: 'V2' },
1925
+ features: { bootloader_mode: false, capabilities: [] },
1926
+ isBootloader: () => false,
1927
+ });
1928
+ const typedCall = jest.fn().mockResolvedValue({
1929
+ type: 'DeviceInfo',
1930
+ message: protocolV2BootloaderDeviceInfo,
1931
+ });
1932
+ const reconnectProtocolV2Device = jest
1933
+ .fn()
1934
+ .mockRejectedValueOnce(new Error('Device not found'))
1935
+ .mockRejectedValueOnce(new Error('Device not found'))
1936
+ .mockImplementation(() => {
1937
+ (method as any).device.isBootloader = () => true;
1938
+ return Promise.resolve();
1939
+ });
1940
+ (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
1941
+ (method as any).device.getCommands = () => ({ typedCall });
1942
+
1943
+ await (method as any).waitForProtocolV2BootloaderMode(60 * 1000, 0);
1944
+
1945
+ expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(3);
1946
+ expect(typedCall).toHaveBeenCalledTimes(1);
1938
1947
  });
1939
1948
 
1940
- test('reboots Protocol V2 firmware flow back to normal before final feature polling', async () => {
1949
+ test('reboots Protocol V2 firmware flow back to normal after install status is finished', async () => {
1941
1950
  const method = new FirmwareUpdateV4({
1942
1951
  id: 1,
1943
1952
  payload: {
@@ -1952,7 +1961,7 @@ describe('Protocol V2 firmware update targets', () => {
1952
1961
  await (method as any).exitProtocolV2BootloaderToNormal();
1953
1962
 
1954
1963
  expect(method.postTipMessage).toHaveBeenCalledWith('SwitchFirmwareReconnectDevice');
1955
- expect((method as any).protocolV2Reboot).toHaveBeenCalledWith(DevRebootType.Normal);
1964
+ expect((method as any).protocolV2Reboot).toHaveBeenCalledWith(DeviceRebootType.Normal);
1956
1965
  });
1957
1966
 
1958
1967
  test('treats iOS BLE RxError 6 during Protocol V2 reboot as expected disconnect', async () => {
@@ -1972,7 +1981,7 @@ describe('Protocol V2 firmware update targets', () => {
1972
1981
  getCommands: () => ({ typedCall }),
1973
1982
  });
1974
1983
 
1975
- await expect((method as any).protocolV2Reboot(DevRebootType.Normal)).resolves.toEqual({
1984
+ await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).resolves.toEqual({
1976
1985
  message: 'Device rebooted successfully',
1977
1986
  });
1978
1987
  });
@@ -1992,7 +2001,7 @@ describe('Protocol V2 firmware update targets', () => {
1992
2001
  getCommands: () => ({ typedCall }),
1993
2002
  });
1994
2003
 
1995
- await expect((method as any).protocolV2Reboot(DevRebootType.Normal)).resolves.toEqual({
2004
+ await expect((method as any).protocolV2Reboot(DeviceRebootType.Normal)).resolves.toEqual({
1996
2005
  message: 'Device rebooted successfully',
1997
2006
  });
1998
2007
  });
@@ -2020,7 +2029,30 @@ describe('Protocol V2 firmware update targets', () => {
2020
2029
 
2021
2030
  await expect(
2022
2031
  (method as any).protocolV2StartFirmwareUpdate({
2023
- targets: [{ target_id: 0, path: 'vol1:firmware.bin' }],
2032
+ targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
2033
+ })
2034
+ ).resolves.toBeUndefined();
2035
+
2036
+ expect(method.postTipMessage).toHaveBeenCalledWith('FirmwareUpdating');
2037
+ });
2038
+
2039
+ test('continues Protocol V2 install polling when update request transfer times out', async () => {
2040
+ const method = new FirmwareUpdateV4({
2041
+ id: 1,
2042
+ payload: {
2043
+ method: 'firmwareUpdateV4',
2044
+ },
2045
+ });
2046
+ const typedCall = jest.fn().mockRejectedValue(new Error('LIBUSB_TRANSFER_TIMED_OUT'));
2047
+
2048
+ (method as any).device = stubDevice({
2049
+ getCommands: () => ({ typedCall }),
2050
+ });
2051
+ method.postTipMessage = jest.fn();
2052
+
2053
+ await expect(
2054
+ (method as any).protocolV2StartFirmwareUpdate({
2055
+ targets: [{ target_id: 1, path: 'vol1:resource.bin' }],
2024
2056
  })
2025
2057
  ).resolves.toBeUndefined();
2026
2058
 
@@ -2034,19 +2066,31 @@ describe('Protocol V2 firmware update targets', () => {
2034
2066
  method: 'firmwareUpdateV4',
2035
2067
  },
2036
2068
  });
2037
- const typedCall = jest
2038
- .fn()
2039
- .mockRejectedValueOnce(
2040
- new Error(
2041
- 'Device protocol mismatch: expected V2, but device did not respond to expected protocol'
2042
- )
2043
- )
2044
- .mockResolvedValueOnce({
2045
- type: 'DevFirmwareUpdateStatus',
2046
- message: {
2047
- targets: [{ target_id: 2, status: 0 }],
2048
- },
2049
- });
2069
+ const typedCall = jest.fn().mockImplementation((name: string) => {
2070
+ if (name === 'DeviceFirmwareUpdateStatusGet') {
2071
+ const callCount = typedCall.mock.calls.filter(call => call[0] === name).length;
2072
+ if (callCount === 1) {
2073
+ return Promise.reject(
2074
+ new Error(
2075
+ 'Device protocol mismatch: expected V2, but device did not respond to expected protocol'
2076
+ )
2077
+ );
2078
+ }
2079
+ return Promise.resolve({
2080
+ type: 'DeviceFirmwareUpdateStatus',
2081
+ message: {
2082
+ records: [{ target_id: 6, status: 2 }],
2083
+ },
2084
+ });
2085
+ }
2086
+ if (name === 'DeviceInfoGet') {
2087
+ return Promise.reject(new Error('DeviceInfo not ready'));
2088
+ }
2089
+ if (name === 'Ping') {
2090
+ return Promise.resolve({ type: 'Success', message: { message: 'ready' } });
2091
+ }
2092
+ return Promise.reject(new Error(`unexpected call ${name}`));
2093
+ });
2050
2094
  const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
2051
2095
 
2052
2096
  (method as any).device = stubDevice({
@@ -2056,14 +2100,75 @@ describe('Protocol V2 firmware update targets', () => {
2056
2100
  method.postProgressMessage = jest.fn();
2057
2101
 
2058
2102
  await (method as any).waitForProtocolV2FirmwareUpdateComplete([
2059
- { target_id: 2, path: 'vol1:ble-firmware.bin' },
2103
+ { target_id: 6, path: 'vol1:ble-firmware.bin' },
2060
2104
  ]);
2061
2105
 
2062
2106
  expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
2063
- expect(typedCall).toHaveBeenCalledTimes(2);
2107
+ expect(typedCall.mock.calls.map(call => call[0])).toEqual([
2108
+ 'DeviceFirmwareUpdateStatusGet',
2109
+ 'DeviceInfoGet',
2110
+ 'Ping',
2111
+ 'DeviceFirmwareUpdateStatusGet',
2112
+ ]);
2113
+ });
2114
+
2115
+ test('treats Protocol V2 normal mode after reconnect as firmware update complete', async () => {
2116
+ const method = new FirmwareUpdateV4({
2117
+ id: 1,
2118
+ payload: {
2119
+ method: 'firmwareUpdateV4',
2120
+ },
2121
+ });
2122
+ const typedCall = jest.fn().mockImplementation((name: string) => {
2123
+ if (name === 'DeviceFirmwareUpdateStatusGet') {
2124
+ return Promise.reject(new Error('Device disconnected'));
2125
+ }
2126
+ if (name === 'DeviceInfoGet') {
2127
+ return Promise.resolve({
2128
+ type: 'DeviceInfo',
2129
+ message: {
2130
+ hw: { serial_no: 'PR9999999999' },
2131
+ fw: {
2132
+ bootloader: { version: '9.9.9' },
2133
+ application: { version: '9.9.9' },
2134
+ },
2135
+ status: {
2136
+ device_id: 'PRO2-DEVICE-ID',
2137
+ init_states: true,
2138
+ },
2139
+ },
2140
+ });
2141
+ }
2142
+ return Promise.reject(new Error(`unexpected call ${name}`));
2143
+ });
2144
+ const reconnectProtocolV2Device = jest.fn().mockResolvedValue(undefined);
2145
+ const updateProtocolV2Features = jest.fn((deviceInfo: ProtocolV2DeviceInfo) =>
2146
+ normalizeProtocolV2Features({ ...descriptor, protocolType: 'V2' } as any, deviceInfo)
2147
+ );
2148
+
2149
+ (method as any).device = stubDevice({
2150
+ getCommands: () => ({ typedCall }),
2151
+ updateProtocolV2Features,
2152
+ });
2153
+ (method as any).reconnectProtocolV2Device = reconnectProtocolV2Device;
2154
+ method.postProgressMessage = jest.fn();
2155
+
2156
+ await expect(
2157
+ (method as any).waitForProtocolV2FirmwareUpdateComplete([
2158
+ { target_id: 3, path: 'vol1:bootloader.bin' },
2159
+ { target_id: 4, path: 'vol1:application_p1.bin' },
2160
+ ])
2161
+ ).resolves.toBeUndefined();
2162
+
2163
+ expect(reconnectProtocolV2Device).toHaveBeenCalledTimes(1);
2164
+ expect(updateProtocolV2Features).toHaveBeenCalledTimes(1);
2165
+ expect(typedCall.mock.calls.map(call => call[0])).toEqual([
2166
+ 'DeviceFirmwareUpdateStatusGet',
2167
+ 'DeviceInfoGet',
2168
+ ]);
2064
2169
  });
2065
2170
 
2066
- test('uses dev firmware status semantics for Protocol V2 install polling', () => {
2171
+ test('uses SDK decoded enum names for Protocol V2 install polling', () => {
2067
2172
  const method = new FirmwareUpdateV4({
2068
2173
  id: 1,
2069
2174
  payload: {
@@ -2072,18 +2177,36 @@ describe('Protocol V2 firmware update targets', () => {
2072
2177
  });
2073
2178
  method.postProgressMessage = jest.fn();
2074
2179
 
2075
- const expectedTargetIds = new Set([3]);
2180
+ const expectedTargetIds = new Set([4]);
2076
2181
 
2182
+ expect(
2183
+ (method as any).assertProtocolV2TargetStatus([{ target_id: 4, status: 2 }], expectedTargetIds)
2184
+ ).toBe(true);
2077
2185
  expect(
2078
2186
  (method as any).assertProtocolV2TargetStatus(
2079
- [{ target_id: 3, status: 0 }],
2187
+ [{ target_id: 4, status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED' }],
2080
2188
  expectedTargetIds
2081
2189
  )
2082
2190
  ).toBe(true);
2191
+ expect(
2192
+ (method as any).assertProtocolV2TargetStatus(
2193
+ [
2194
+ {
2195
+ target_id: 'FW_MGMT_TARGET_CRATE',
2196
+ status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED',
2197
+ },
2198
+ { target_id: 'FW_MGMT_TARGET_SE04', status: 'FW_MGMT_UPDATER_TASK_STATUS_FINISHED' },
2199
+ ],
2200
+ new Set([1, 10])
2201
+ )
2202
+ ).toBe(true);
2083
2203
 
2204
+ expect(
2205
+ (method as any).assertProtocolV2TargetStatus([{ target_id: 4, status: 1 }], expectedTargetIds)
2206
+ ).toBe(false);
2084
2207
  expect(
2085
2208
  (method as any).assertProtocolV2TargetStatus(
2086
- [{ target_id: 3, status: 1 }],
2209
+ [{ target_id: 4, status: 'FW_MGMT_UPDATER_TASK_STATUS_IN_PROGRESS' }],
2087
2210
  expectedTargetIds
2088
2211
  )
2089
2212
  ).toBe(false);
@@ -2091,7 +2214,17 @@ describe('Protocol V2 firmware update targets', () => {
2091
2214
 
2092
2215
  try {
2093
2216
  (method as any).assertProtocolV2TargetStatus(
2094
- [{ target_id: 3, status: 2 }],
2217
+ [{ target_id: 4, status: 3 }],
2218
+ expectedTargetIds
2219
+ );
2220
+ throw new Error('Expected Protocol V2 failed firmware status to throw');
2221
+ } catch (error: any) {
2222
+ expect(error.errorCode).toBe(HardwareErrorCode.FirmwareError);
2223
+ }
2224
+
2225
+ try {
2226
+ (method as any).assertProtocolV2TargetStatus(
2227
+ [{ target_id: 4, status: 'FW_MGMT_UPDATER_TASK_STATUS_FAILED_VERIFY' }],
2095
2228
  expectedTargetIds
2096
2229
  );
2097
2230
  throw new Error('Expected Protocol V2 failed firmware status to throw');
@@ -2100,7 +2233,7 @@ describe('Protocol V2 firmware update targets', () => {
2100
2233
  }
2101
2234
  });
2102
2235
 
2103
- test('passes resource, bootloader, BLE, SE and app files to DeviceFirmwareUpdate targets', async () => {
2236
+ test('passes resource, bootloader, coprocessor, SE and app files to DeviceFirmwareUpdate targets', async () => {
2104
2237
  const method = new FirmwareUpdateV4({
2105
2238
  id: 1,
2106
2239
  payload: {
@@ -2121,21 +2254,29 @@ describe('Protocol V2 firmware update targets', () => {
2121
2254
  .mockResolvedValue(undefined);
2122
2255
 
2123
2256
  await (method as any).executeProtocolV2Update({
2124
- // resource 只支持单文件 .bin,整文件一次上传
2125
- resourceBinary: new Uint8Array([1, 2, 3]).buffer,
2257
+ resourceBinaryMap: [
2258
+ {
2259
+ fileName: 'resource.bin',
2260
+ binary: new Uint8Array([1, 2, 3]).buffer,
2261
+ targetId: 1,
2262
+ },
2263
+ ],
2126
2264
  bootloaderBinary: new Uint8Array([4, 5]).buffer,
2127
2265
  fwBinaryMap: [
2128
2266
  {
2129
- fileName: 'ble-firmware.bin',
2267
+ fileName: 'coprocessor.bin',
2130
2268
  binary: new Uint8Array([6]).buffer,
2269
+ targetId: 6,
2131
2270
  },
2132
2271
  {
2133
- fileName: 'se1-firmware.bin',
2272
+ fileName: 'se01.bin',
2134
2273
  binary: new Uint8Array([7]).buffer,
2274
+ targetId: 7,
2135
2275
  },
2136
2276
  {
2137
- fileName: 'firmware.bin',
2277
+ fileName: 'application_p1.bin',
2138
2278
  binary: new Uint8Array([8]).buffer,
2279
+ targetId: 4,
2139
2280
  },
2140
2281
  ],
2141
2282
  });
@@ -2143,18 +2284,19 @@ describe('Protocol V2 firmware update targets', () => {
2143
2284
  expect(writtenPaths).toEqual([
2144
2285
  'vol1:resource.bin',
2145
2286
  'vol1:bootloader.bin',
2146
- 'vol1:ble-firmware.bin',
2147
- 'vol1:se1-firmware.bin',
2148
- 'vol1:firmware.bin',
2287
+ 'vol1:coprocessor.bin',
2288
+ 'vol1:se01.bin',
2289
+ 'vol1:application_p1.bin',
2149
2290
  ]);
2150
- // DevFirmwareTargetType:RESOURCE=10, MAIN_BOOT=1, BT=2, SE1=3, MAIN_APP=0
2151
- expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
2291
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(1, {
2292
+ targets: [{ target_id: 1, path: 'vol1:resource.bin' }],
2293
+ });
2294
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(2, {
2152
2295
  targets: [
2153
- { target_id: 10, path: 'vol1:resource.bin' },
2154
- { target_id: 1, path: 'vol1:bootloader.bin' },
2155
- { target_id: 2, path: 'vol1:ble-firmware.bin' },
2156
- { target_id: 3, path: 'vol1:se1-firmware.bin' },
2157
- { target_id: 0, path: 'vol1:firmware.bin' },
2296
+ { target_id: 3, path: 'vol1:bootloader.bin' },
2297
+ { target_id: 6, path: 'vol1:coprocessor.bin' },
2298
+ { target_id: 7, path: 'vol1:se01.bin' },
2299
+ { target_id: 4, path: 'vol1:application_p1.bin' },
2158
2300
  ],
2159
2301
  });
2160
2302
  expect(method.postProgressMessage).toHaveBeenCalledWith(100, 'transferData');
@@ -2167,7 +2309,6 @@ describe('Protocol V2 firmware update targets', () => {
2167
2309
  payload: {
2168
2310
  method: 'firmwareUpdateV4',
2169
2311
  platform: 'web',
2170
- romloaderBinary: new Uint8Array([1]).buffer,
2171
2312
  applicationP2Binary: new Uint8Array([2]).buffer,
2172
2313
  se04Binary: new Uint8Array([3]).buffer,
2173
2314
  },
@@ -2176,9 +2317,8 @@ describe('Protocol V2 firmware update targets', () => {
2176
2317
 
2177
2318
  const explicit = (method as any).collectExplicitTargetBinaries();
2178
2319
  expect(explicit).toEqual([
2179
- { fileName: 'romloader.bin', binary: expect.anything(), targetId: 1 },
2180
- { fileName: 'application_p2.bin', binary: expect.anything(), targetId: 0 },
2181
- { fileName: 'se04.bin', binary: expect.anything(), targetId: 6 },
2320
+ { fileName: 'application_p2.bin', binary: expect.anything(), targetId: 5 },
2321
+ { fileName: 'se04.bin', binary: expect.anything(), targetId: 10 },
2182
2322
  ]);
2183
2323
 
2184
2324
  method.postTipMessage = jest.fn();
@@ -2194,43 +2334,759 @@ describe('Protocol V2 firmware update targets', () => {
2194
2334
  .mockResolvedValue(undefined);
2195
2335
 
2196
2336
  await (method as any).executeProtocolV2Update({
2197
- resourceBinary: null,
2337
+ resourceBinaryMap: [],
2198
2338
  bootloaderBinary: null,
2199
2339
  fwBinaryMap: explicit,
2200
2340
  });
2201
2341
 
2202
2342
  expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenCalledWith({
2203
2343
  targets: [
2204
- { target_id: 1, path: 'vol1:romloader.bin' },
2205
- { target_id: 0, path: 'vol1:application_p2.bin' },
2206
- { target_id: 6, path: 'vol1:se04.bin' },
2344
+ { target_id: 5, path: 'vol1:application_p2.bin' },
2345
+ { target_id: 10, path: 'vol1:se04.bin' },
2207
2346
  ],
2208
2347
  });
2209
2348
  });
2210
2349
 
2211
- test('uses absolute processed_byte offsets and disables append for firmware file writes', async () => {
2350
+ test('maps explicit Pro2 resource crate binaries to separate CRATE targets', async () => {
2351
+ const firstResource = new Uint8Array([1]).buffer;
2352
+ const secondResource = new Uint8Array([2]).buffer;
2212
2353
  const method = new FirmwareUpdateV4({
2213
2354
  id: 1,
2214
2355
  payload: {
2215
2356
  method: 'firmwareUpdateV4',
2357
+ platform: 'web',
2358
+ resourceBinaries: [firstResource, secondResource],
2216
2359
  },
2217
2360
  });
2218
- const typedCall = jest.fn(
2219
- (
2220
- _name: string,
2221
- _resType: string,
2222
- params: { file: { offset: number; data: { byteLength: number } } }
2223
- ) =>
2224
- Promise.resolve({
2225
- type: 'FilesystemFile',
2226
- message: {
2227
- processed_byte: params.file.offset + params.file.data.byteLength,
2228
- },
2229
- })
2230
- );
2361
+ method.init();
2231
2362
 
2232
- (method as any).device = stubDevice({
2233
- getCommands: () => ({ typedCall }),
2363
+ const resourceTargets = await (method as any).prepareResourceBinaries('universal', {
2364
+ deviceType: 'pro2',
2365
+ firmwareVersion: '1.0.0',
2366
+ capabilities: [],
2367
+ });
2368
+
2369
+ expect(resourceTargets).toEqual([
2370
+ {
2371
+ fileName: 'resource-1.bin',
2372
+ binary: firstResource,
2373
+ targetId: 1,
2374
+ },
2375
+ {
2376
+ fileName: 'resource-2.bin',
2377
+ binary: secondResource,
2378
+ targetId: 1,
2379
+ },
2380
+ ]);
2381
+ expect((method as any).hasExplicitProtocolV2Payload([])).toBe(true);
2382
+ });
2383
+
2384
+ test('rejects romloaderBinary before sending a Protocol V2 update request', () => {
2385
+ const method = new FirmwareUpdateV4({
2386
+ id: 1,
2387
+ payload: {
2388
+ method: 'firmwareUpdateV4',
2389
+ platform: 'web',
2390
+ romloaderBinary: new Uint8Array([1]).buffer,
2391
+ },
2392
+ });
2393
+ method.init();
2394
+
2395
+ expect(() => (method as any).collectExplicitTargetBinaries()).toThrow(
2396
+ 'FW_MGMT_TARGET_ROMLOADER is not accepted'
2397
+ );
2398
+ });
2399
+
2400
+ test('maps Pro2 remote firmware-v1 components to Protocol V2 targets in installOrder', async () => {
2401
+ const method = new FirmwareUpdateV4({
2402
+ id: 1,
2403
+ payload: {
2404
+ method: 'firmwareUpdateV4',
2405
+ platform: 'web',
2406
+ },
2407
+ });
2408
+ method.init();
2409
+
2410
+ const binaries = new Map(
2411
+ [
2412
+ 'bootloader',
2413
+ 'applicationP1',
2414
+ 'applicationP2',
2415
+ 'coprocessor',
2416
+ 'se01',
2417
+ 'se02',
2418
+ 'se03',
2419
+ 'se04',
2420
+ ].map((key, index) => [
2421
+ `https://example.com/${key}.pp.bin`,
2422
+ new Uint8Array([index + 1]).buffer,
2423
+ ])
2424
+ );
2425
+ const getSysResourceBinarySpy = jest
2426
+ .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
2427
+ .mockImplementation(url =>
2428
+ Promise.resolve({
2429
+ binary: binaries.get(url) ?? new Uint8Array([0]).buffer,
2430
+ })
2431
+ );
2432
+ const getFirmwareLatestReleaseSpy = jest
2433
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
2434
+ .mockReturnValue({
2435
+ required: false,
2436
+ version: [1, 0, 0],
2437
+ url: 'https://example.com/applicationP1.pp.bin',
2438
+ bootloaderResource: 'https://example.com/bootloader.pp.bin',
2439
+ bootloaderVersion: [1, 0, 0],
2440
+ displayBootloaderVersion: [1, 0, 0],
2441
+ upgradeType: 'payload-package-set',
2442
+ installOrder: [
2443
+ 'bootloader',
2444
+ 'applicationP1',
2445
+ 'applicationP2',
2446
+ 'coprocessor',
2447
+ 'se01',
2448
+ 'se02',
2449
+ 'se03',
2450
+ 'se04',
2451
+ ],
2452
+ components: {
2453
+ bootloader: {
2454
+ target: 'BOOTLOADER',
2455
+ url: 'https://example.com/bootloader.pp.bin',
2456
+ },
2457
+ applicationP1: {
2458
+ target: 'APPLICATION_P1',
2459
+ url: 'https://example.com/applicationP1.pp.bin',
2460
+ },
2461
+ applicationP2: {
2462
+ target: 'APPLICATION_P2',
2463
+ url: 'https://example.com/applicationP2.pp.bin',
2464
+ },
2465
+ coprocessor: {
2466
+ target: 'COPROCESSOR',
2467
+ url: 'https://example.com/coprocessor.pp.bin',
2468
+ },
2469
+ se01: { target: 'SE01', url: 'https://example.com/se01.pp.bin' },
2470
+ se02: { target: 'SE02', url: 'https://example.com/se02.pp.bin' },
2471
+ se03: { target: 'SE03', url: 'https://example.com/se03.pp.bin' },
2472
+ se04: { target: 'SE04', url: 'https://example.com/se04.pp.bin' },
2473
+ },
2474
+ fingerprint: '',
2475
+ changelog: {
2476
+ 'zh-CN': '',
2477
+ 'en-US': '',
2478
+ },
2479
+ });
2480
+
2481
+ const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
2482
+ deviceType: 'pro2',
2483
+ firmwareVersion: '0.0.0',
2484
+ });
2485
+
2486
+ expect(remoteBinaries.bootloaderBinary).toBe(
2487
+ binaries.get('https://example.com/bootloader.pp.bin')
2488
+ );
2489
+ expect(remoteBinaries.fwBinaryMap).toEqual([
2490
+ { fileName: 'application_p1.bin', binary: expect.anything(), targetId: 4 },
2491
+ { fileName: 'application_p2.bin', binary: expect.anything(), targetId: 5 },
2492
+ { fileName: 'coprocessor.bin', binary: expect.anything(), targetId: 6 },
2493
+ { fileName: 'se01.bin', binary: expect.anything(), targetId: 7 },
2494
+ { fileName: 'se02.bin', binary: expect.anything(), targetId: 8 },
2495
+ { fileName: 'se03.bin', binary: expect.anything(), targetId: 9 },
2496
+ { fileName: 'se04.bin', binary: expect.anything(), targetId: 10 },
2497
+ ]);
2498
+ expect(getSysResourceBinarySpy.mock.calls.map(call => call[0])).toEqual([
2499
+ 'https://example.com/bootloader.pp.bin',
2500
+ 'https://example.com/applicationP1.pp.bin',
2501
+ 'https://example.com/applicationP2.pp.bin',
2502
+ 'https://example.com/coprocessor.pp.bin',
2503
+ 'https://example.com/se01.pp.bin',
2504
+ 'https://example.com/se02.pp.bin',
2505
+ 'https://example.com/se03.pp.bin',
2506
+ 'https://example.com/se04.pp.bin',
2507
+ ]);
2508
+
2509
+ getSysResourceBinarySpy.mockRestore();
2510
+ getFirmwareLatestReleaseSpy.mockRestore();
2511
+ });
2512
+
2513
+ test('skips Pro2 resource crate download when resourceManifest matches device OKPP headers', async () => {
2514
+ const method = new FirmwareUpdateV4({
2515
+ id: 1,
2516
+ payload: {
2517
+ method: 'firmwareUpdateV4',
2518
+ platform: 'web',
2519
+ },
2520
+ });
2521
+ method.init();
2522
+
2523
+ const payloadHash = 'ab'.repeat(64);
2524
+ const header = buildOkppHeader({ payloadHash });
2525
+ const typedCall = jest.fn(
2526
+ (name: string, _resType: string, params: { file: { offset: number }; chunk_len: number }) => {
2527
+ if (name === 'FilesystemPathInfoQuery') {
2528
+ return Promise.resolve({
2529
+ type: 'FilesystemPathInfo',
2530
+ message: { exist: true, directory: false, size: header.byteLength },
2531
+ });
2532
+ }
2533
+ if (name === 'FilesystemFileRead') {
2534
+ const { offset } = params.file;
2535
+ const readLen = params.chunk_len;
2536
+ return Promise.resolve({
2537
+ type: 'FilesystemFile',
2538
+ message: { data: header.slice(offset, offset + readLen) },
2539
+ });
2540
+ }
2541
+ return Promise.reject(new Error(`unexpected call ${name}`));
2542
+ }
2543
+ );
2544
+ (method as any).device = stubDevice({
2545
+ getCommands: () => ({ typedCall }),
2546
+ });
2547
+
2548
+ const getSysResourceBinarySpy = jest
2549
+ .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
2550
+ .mockResolvedValue({ binary: new Uint8Array([9]).buffer });
2551
+ const getFirmwareLatestReleaseSpy = jest
2552
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
2553
+ .mockReturnValue({
2554
+ required: false,
2555
+ version: [1, 0, 0],
2556
+ url: '',
2557
+ upgradeType: 'payload-package-set',
2558
+ installOrder: ['resource'],
2559
+ components: {
2560
+ resource: {
2561
+ target: 'CRATE',
2562
+ url: 'https://example.com/pro2-resource.crate.okpkg',
2563
+ },
2564
+ },
2565
+ resourceManifest: {
2566
+ format: 'okpkg-crate',
2567
+ target: 'CRATE',
2568
+ version: [1, 0, 0],
2569
+ packages: [
2570
+ {
2571
+ path: 'resource/images/images.okpkg',
2572
+ type: 'RESC',
2573
+ payloadHash,
2574
+ },
2575
+ ],
2576
+ },
2577
+ fingerprint: '',
2578
+ changelog: {
2579
+ 'zh-CN': '',
2580
+ 'en-US': '',
2581
+ },
2582
+ });
2583
+
2584
+ const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
2585
+ deviceType: 'pro2',
2586
+ firmwareVersion: '1.0.0',
2587
+ capabilities: [],
2588
+ });
2589
+
2590
+ expect(remoteBinaries).toEqual({
2591
+ resourceBinaryMap: [],
2592
+ bootloaderBinary: null,
2593
+ fwBinaryMap: [],
2594
+ installItems: [],
2595
+ });
2596
+ expect(getSysResourceBinarySpy).not.toHaveBeenCalled();
2597
+ expect(typedCall.mock.calls[0][2]).toEqual({
2598
+ path: 'vol0:/resource/images/images.okpkg',
2599
+ });
2600
+
2601
+ getSysResourceBinarySpy.mockRestore();
2602
+ getFirmwareLatestReleaseSpy.mockRestore();
2603
+ });
2604
+
2605
+ test('downloads Pro2 resource crate when resourceManifest package is missing', async () => {
2606
+ const method = new FirmwareUpdateV4({
2607
+ id: 1,
2608
+ payload: {
2609
+ method: 'firmwareUpdateV4',
2610
+ platform: 'web',
2611
+ },
2612
+ });
2613
+ method.init();
2614
+
2615
+ const resourceBinary = new Uint8Array([9]).buffer;
2616
+ const typedCall = jest.fn().mockResolvedValue({
2617
+ type: 'FilesystemPathInfo',
2618
+ message: { exist: false, directory: false, size: 0 },
2619
+ });
2620
+ (method as any).device = stubDevice({
2621
+ getCommands: () => ({ typedCall }),
2622
+ });
2623
+
2624
+ const getSysResourceBinarySpy = jest
2625
+ .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
2626
+ .mockResolvedValue({ binary: resourceBinary });
2627
+ const getFirmwareLatestReleaseSpy = jest
2628
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
2629
+ .mockReturnValue({
2630
+ required: false,
2631
+ version: [1, 0, 0],
2632
+ url: '',
2633
+ upgradeType: 'payload-package-set',
2634
+ installOrder: ['resource'],
2635
+ components: {
2636
+ resource: {
2637
+ target: 'CRATE',
2638
+ url: 'https://example.com/pro2-resource.crate.okpkg',
2639
+ resourceManifest: {
2640
+ format: 'okpkg-crate',
2641
+ target: 'CRATE',
2642
+ version: [1, 0, 0],
2643
+ packages: [{ path: 'resource/images/images.okpkg', type: 'RESC' }],
2644
+ },
2645
+ },
2646
+ },
2647
+ fingerprint: '',
2648
+ changelog: {
2649
+ 'zh-CN': '',
2650
+ 'en-US': '',
2651
+ },
2652
+ });
2653
+
2654
+ const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
2655
+ deviceType: 'pro2',
2656
+ firmwareVersion: '1.0.0',
2657
+ capabilities: [],
2658
+ });
2659
+
2660
+ expect(remoteBinaries.resourceBinaryMap).toEqual([
2661
+ {
2662
+ fileName: 'resource-resource.bin',
2663
+ binary: resourceBinary,
2664
+ targetId: 1,
2665
+ },
2666
+ ]);
2667
+ expect(getSysResourceBinarySpy).toHaveBeenCalledWith(
2668
+ 'https://example.com/pro2-resource.crate.okpkg'
2669
+ );
2670
+
2671
+ getSysResourceBinarySpy.mockRestore();
2672
+ getFirmwareLatestReleaseSpy.mockRestore();
2673
+ });
2674
+
2675
+ test('maps Pro2 CRATE remote component to resource binary target', async () => {
2676
+ const method = new FirmwareUpdateV4({
2677
+ id: 1,
2678
+ payload: {
2679
+ method: 'firmwareUpdateV4',
2680
+ platform: 'web',
2681
+ },
2682
+ });
2683
+ method.init();
2684
+
2685
+ const resourceBinary = new Uint8Array([1, 2, 3]).buffer;
2686
+ const getSysResourceBinarySpy = jest
2687
+ .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
2688
+ .mockResolvedValue({ binary: resourceBinary });
2689
+ const getFirmwareLatestReleaseSpy = jest
2690
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
2691
+ .mockReturnValue({
2692
+ required: false,
2693
+ version: [1, 0, 0],
2694
+ url: '',
2695
+ upgradeType: 'payload-package-set',
2696
+ installOrder: ['resource'],
2697
+ components: {
2698
+ resource: {
2699
+ target: 'CRATE',
2700
+ url: 'https://example.com/pro2-resource.crate.okpkg',
2701
+ },
2702
+ },
2703
+ fingerprint: '',
2704
+ changelog: {
2705
+ 'zh-CN': '',
2706
+ 'en-US': '',
2707
+ },
2708
+ });
2709
+
2710
+ const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
2711
+ deviceType: 'pro2',
2712
+ firmwareVersion: '0.0.0',
2713
+ capabilities: [],
2714
+ });
2715
+
2716
+ expect(remoteBinaries).toEqual({
2717
+ resourceBinaryMap: [
2718
+ {
2719
+ fileName: 'resource-resource.bin',
2720
+ binary: resourceBinary,
2721
+ targetId: 1,
2722
+ },
2723
+ ],
2724
+ bootloaderBinary: null,
2725
+ fwBinaryMap: [],
2726
+ installItems: [
2727
+ {
2728
+ fileName: 'resource-resource.bin',
2729
+ binary: resourceBinary,
2730
+ targetId: 1,
2731
+ kind: 'resource',
2732
+ },
2733
+ ],
2734
+ });
2735
+
2736
+ getSysResourceBinarySpy.mockRestore();
2737
+ getFirmwareLatestReleaseSpy.mockRestore();
2738
+ });
2739
+
2740
+ test('downloads only unmatched Pro2 resource crate components by component manifests', async () => {
2741
+ const method = new FirmwareUpdateV4({
2742
+ id: 1,
2743
+ payload: {
2744
+ method: 'firmwareUpdateV4',
2745
+ platform: 'web',
2746
+ },
2747
+ });
2748
+ method.init();
2749
+
2750
+ const imagesPayloadHash = 'ab'.repeat(64);
2751
+ const imagesHeader = buildOkppHeader({ payloadHash: imagesPayloadHash });
2752
+ const fontsBinary = new Uint8Array([7, 8, 9]).buffer;
2753
+ const typedCall = jest.fn(
2754
+ (
2755
+ name: string,
2756
+ _resType: string,
2757
+ params: { path?: string; file?: { offset: number }; chunk_len?: number }
2758
+ ) => {
2759
+ if (name === 'FilesystemPathInfoQuery') {
2760
+ const exist = params.path === 'vol0:/resource/images/images.okpkg';
2761
+ return Promise.resolve({
2762
+ type: 'FilesystemPathInfo',
2763
+ message: { exist, directory: false, size: exist ? imagesHeader.byteLength : 0 },
2764
+ });
2765
+ }
2766
+ if (name === 'FilesystemFileRead') {
2767
+ const offset = params.file?.offset ?? 0;
2768
+ const readLen = params.chunk_len ?? 0;
2769
+ return Promise.resolve({
2770
+ type: 'FilesystemFile',
2771
+ message: { data: imagesHeader.slice(offset, offset + readLen) },
2772
+ });
2773
+ }
2774
+ return Promise.reject(new Error(`unexpected call ${name}`));
2775
+ }
2776
+ );
2777
+ (method as any).device = stubDevice({
2778
+ getCommands: () => ({ typedCall }),
2779
+ });
2780
+
2781
+ const getSysResourceBinarySpy = jest
2782
+ .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
2783
+ .mockResolvedValue({ binary: fontsBinary });
2784
+ const getFirmwareLatestReleaseSpy = jest
2785
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
2786
+ .mockReturnValue({
2787
+ required: false,
2788
+ version: [1, 0, 0],
2789
+ url: '',
2790
+ upgradeType: 'payload-package-set',
2791
+ installOrder: ['resourceImages', 'resourceFonts'],
2792
+ components: {
2793
+ resourceImages: {
2794
+ target: 'CRATE',
2795
+ url: 'https://example.com/pro2-resource-images.crate.okpkg',
2796
+ resourceManifest: {
2797
+ format: 'okpkg-crate',
2798
+ target: 'CRATE',
2799
+ version: [1, 0, 0],
2800
+ packages: [
2801
+ {
2802
+ path: 'resource/images/images.okpkg',
2803
+ type: 'RESC',
2804
+ payloadHash: imagesPayloadHash,
2805
+ },
2806
+ ],
2807
+ },
2808
+ },
2809
+ resourceFonts: {
2810
+ target: 'CRATE',
2811
+ url: 'https://example.com/pro2-resource-fonts.crate.okpkg',
2812
+ resourceManifest: {
2813
+ format: 'okpkg-crate',
2814
+ target: 'CRATE',
2815
+ version: [1, 0, 0],
2816
+ packages: [{ path: 'resource/font/noto.okpkg', type: 'RESC' }],
2817
+ },
2818
+ },
2819
+ },
2820
+ fingerprint: '',
2821
+ changelog: {
2822
+ 'zh-CN': '',
2823
+ 'en-US': '',
2824
+ },
2825
+ });
2826
+
2827
+ const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
2828
+ deviceType: 'pro2',
2829
+ firmwareVersion: '1.0.0',
2830
+ capabilities: [],
2831
+ });
2832
+
2833
+ expect(remoteBinaries.resourceBinaryMap).toEqual([
2834
+ {
2835
+ fileName: 'resource-resourceFonts.bin',
2836
+ binary: fontsBinary,
2837
+ targetId: 1,
2838
+ },
2839
+ ]);
2840
+ expect(getSysResourceBinarySpy).toHaveBeenCalledTimes(1);
2841
+ expect(getSysResourceBinarySpy).toHaveBeenCalledWith(
2842
+ 'https://example.com/pro2-resource-fonts.crate.okpkg'
2843
+ );
2844
+
2845
+ getSysResourceBinarySpy.mockRestore();
2846
+ getFirmwareLatestReleaseSpy.mockRestore();
2847
+ });
2848
+
2849
+ test('installs multiple Pro2 resource crates as separate update requests', async () => {
2850
+ const method = new FirmwareUpdateV4({
2851
+ id: 1,
2852
+ payload: {
2853
+ method: 'firmwareUpdateV4',
2854
+ },
2855
+ });
2856
+
2857
+ const operations: string[] = [];
2858
+ method.postTipMessage = jest.fn();
2859
+ method.postProgressMessage = jest.fn();
2860
+ (method as any).protocolV2CommonUpdateProcess = jest
2861
+ .fn()
2862
+ .mockImplementation(params => {
2863
+ operations.push(`write:${params.filePath}`);
2864
+ return Promise.resolve(
2865
+ Number(params.processedSize ?? 0) + Number(params.payload.byteLength)
2866
+ );
2867
+ });
2868
+ (method as any).protocolV2StartFirmwareUpdate = jest.fn().mockImplementation(({ targets }) => {
2869
+ operations.push(
2870
+ `install:${targets.map((target: { path: string }) => target.path).join(',')}`
2871
+ );
2872
+ return Promise.resolve(undefined);
2873
+ });
2874
+ (method as any).waitForProtocolV2FirmwareUpdateComplete = jest
2875
+ .fn()
2876
+ .mockResolvedValue(undefined);
2877
+
2878
+ await (method as any).executeProtocolV2Update({
2879
+ resourceBinaryMap: [
2880
+ {
2881
+ fileName: 'resource-images.bin',
2882
+ binary: new Uint8Array([1]).buffer,
2883
+ targetId: 1,
2884
+ },
2885
+ {
2886
+ fileName: 'resource-fonts.bin',
2887
+ binary: new Uint8Array([2]).buffer,
2888
+ targetId: 1,
2889
+ },
2890
+ ],
2891
+ bootloaderBinary: null,
2892
+ fwBinaryMap: [
2893
+ {
2894
+ fileName: 'application_p1.bin',
2895
+ binary: new Uint8Array([3]).buffer,
2896
+ targetId: 4,
2897
+ },
2898
+ ],
2899
+ });
2900
+
2901
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(1, {
2902
+ targets: [{ target_id: 1, path: 'vol1:resource-images.bin' }],
2903
+ });
2904
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(2, {
2905
+ targets: [{ target_id: 1, path: 'vol1:resource-fonts.bin' }],
2906
+ });
2907
+ expect((method as any).protocolV2StartFirmwareUpdate).toHaveBeenNthCalledWith(3, {
2908
+ targets: [{ target_id: 4, path: 'vol1:application_p1.bin' }],
2909
+ });
2910
+ expect(operations).toEqual([
2911
+ 'write:vol1:resource-images.bin',
2912
+ 'write:vol1:resource-fonts.bin',
2913
+ 'write:vol1:application_p1.bin',
2914
+ 'install:vol1:resource-images.bin',
2915
+ 'install:vol1:resource-fonts.bin',
2916
+ 'install:vol1:application_p1.bin',
2917
+ ]);
2918
+ });
2919
+
2920
+ test('skips Pro2 firmware components when configured versions are already installed', async () => {
2921
+ const method = new FirmwareUpdateV4({
2922
+ id: 1,
2923
+ payload: {
2924
+ method: 'firmwareUpdateV4',
2925
+ platform: 'web',
2926
+ },
2927
+ });
2928
+ method.init();
2929
+
2930
+ const getSysResourceBinarySpy = jest
2931
+ .spyOn(firmwareBinaryApi, 'getSysResourceBinary')
2932
+ .mockResolvedValue({ binary: new Uint8Array([1]).buffer });
2933
+ const getFirmwareLatestReleaseSpy = jest
2934
+ .spyOn(DataManager, 'getFirmwareLatestRelease')
2935
+ .mockReturnValue({
2936
+ required: false,
2937
+ version: [1, 0, 0],
2938
+ url: 'https://example.com/applicationP1.pp.bin',
2939
+ bootloaderVersion: [1, 0, 0],
2940
+ upgradeType: 'payload-package-set',
2941
+ installOrder: ['bootloader', 'applicationP1', 'applicationP2'],
2942
+ components: {
2943
+ bootloader: {
2944
+ target: 'BOOTLOADER',
2945
+ url: 'https://example.com/bootloader.pp.bin',
2946
+ },
2947
+ applicationP1: {
2948
+ target: 'APPLICATION_P1',
2949
+ url: 'https://example.com/applicationP1.pp.bin',
2950
+ },
2951
+ applicationP2: {
2952
+ target: 'APPLICATION_P2',
2953
+ url: 'https://example.com/applicationP2.pp.bin',
2954
+ },
2955
+ },
2956
+ fingerprint: '',
2957
+ changelog: {
2958
+ 'zh-CN': '',
2959
+ 'en-US': '',
2960
+ },
2961
+ });
2962
+
2963
+ const remoteBinaries = await (method as any).prepareRemoteProtocolV2Binaries('universal', {
2964
+ deviceType: 'pro2',
2965
+ firmwareVersion: '1.0.0',
2966
+ bootloaderVersion: '1.0.0',
2967
+ capabilities: [],
2968
+ });
2969
+
2970
+ expect(remoteBinaries).toEqual({
2971
+ resourceBinaryMap: [],
2972
+ bootloaderBinary: null,
2973
+ fwBinaryMap: [],
2974
+ installItems: [],
2975
+ });
2976
+ expect(getSysResourceBinarySpy).not.toHaveBeenCalled();
2977
+
2978
+ getSysResourceBinarySpy.mockRestore();
2979
+ getFirmwareLatestReleaseSpy.mockRestore();
2980
+ });
2981
+
2982
+ test('uses Pro2 remote components when firmwareUpdateV4 has no explicit binaries', async () => {
2983
+ const method = new FirmwareUpdateV4({
2984
+ id: 1,
2985
+ payload: {
2986
+ method: 'firmwareUpdateV4',
2987
+ platform: 'web',
2988
+ },
2989
+ });
2990
+ method.init();
2991
+
2992
+ const remoteBinaries = {
2993
+ resourceBinaryMap: [],
2994
+ bootloaderBinary: new Uint8Array([1]).buffer,
2995
+ fwBinaryMap: [
2996
+ {
2997
+ fileName: 'application_p1.bin',
2998
+ binary: new Uint8Array([2]).buffer,
2999
+ targetId: 4,
3000
+ },
3001
+ ],
3002
+ };
3003
+
3004
+ (method as any).device = stubDevice({
3005
+ originalDescriptor: { protocolType: 'V2' },
3006
+ features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
3007
+ });
3008
+ (method as any).prepareResourceBinaries = jest.fn().mockResolvedValue([]);
3009
+ (method as any).prepareRemoteProtocolV2Binaries = jest.fn().mockResolvedValue(remoteBinaries);
3010
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
3011
+ (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
3012
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
3013
+ (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
3014
+ bootloaderVersion: '1.0.0',
3015
+ bleVersion: '0.0.0',
3016
+ firmwareVersion: '1.0.0',
3017
+ });
3018
+ method.postTipMessage = jest.fn();
3019
+
3020
+ await method.run();
3021
+
3022
+ expect((method as any).prepareRemoteProtocolV2Binaries).toHaveBeenCalledTimes(1);
3023
+ expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
3024
+ resourceBinaryMap: remoteBinaries.resourceBinaryMap,
3025
+ bootloaderBinary: remoteBinaries.bootloaderBinary,
3026
+ fwBinaryMap: remoteBinaries.fwBinaryMap,
3027
+ });
3028
+ });
3029
+
3030
+ test('keeps explicit Protocol V2 binaries isolated from remote component auto-fill', async () => {
3031
+ const method = new FirmwareUpdateV4({
3032
+ id: 1,
3033
+ payload: {
3034
+ method: 'firmwareUpdateV4',
3035
+ platform: 'web',
3036
+ coprocessorBinary: new Uint8Array([1]).buffer,
3037
+ },
3038
+ });
3039
+ method.init();
3040
+
3041
+ (method as any).device = stubDevice({
3042
+ originalDescriptor: { protocolType: 'V2' },
3043
+ features: { deviceType: 'pro2', firmwareVersion: '0.0.0', capabilities: [] },
3044
+ });
3045
+ (method as any).prepareResourceBinaries = jest.fn().mockResolvedValue([]);
3046
+ (method as any).prepareRemoteProtocolV2Binaries = jest.fn();
3047
+ (method as any).enterProtocolV2BootloaderMode = jest.fn().mockResolvedValue(true);
3048
+ (method as any).executeProtocolV2Update = jest.fn().mockResolvedValue(undefined);
3049
+ (method as any).exitProtocolV2BootloaderToNormal = jest.fn().mockResolvedValue(undefined);
3050
+ (method as any).waitForProtocolV2FinalFeatures = jest.fn().mockResolvedValue({
3051
+ bootloaderVersion: '1.0.0',
3052
+ bleVersion: '0.0.0',
3053
+ firmwareVersion: '1.0.0',
3054
+ });
3055
+ method.postTipMessage = jest.fn();
3056
+
3057
+ await method.run();
3058
+
3059
+ expect((method as any).prepareRemoteProtocolV2Binaries).not.toHaveBeenCalled();
3060
+ expect((method as any).executeProtocolV2Update).toHaveBeenCalledWith({
3061
+ resourceBinaryMap: [],
3062
+ bootloaderBinary: null,
3063
+ fwBinaryMap: [{ fileName: 'coprocessor.bin', binary: expect.anything(), targetId: 6 }],
3064
+ });
3065
+ });
3066
+
3067
+ test('uses absolute processed_byte offsets and disables append for firmware file writes', async () => {
3068
+ const method = new FirmwareUpdateV4({
3069
+ id: 1,
3070
+ payload: {
3071
+ method: 'firmwareUpdateV4',
3072
+ },
3073
+ });
3074
+ const typedCall = jest.fn(
3075
+ (
3076
+ _name: string,
3077
+ _resType: string,
3078
+ params: { file: { offset: number; data: { byteLength: number } } }
3079
+ ) =>
3080
+ Promise.resolve({
3081
+ type: 'FilesystemFile',
3082
+ message: {
3083
+ processed_byte: params.file.offset + params.file.data.byteLength,
3084
+ },
3085
+ })
3086
+ );
3087
+
3088
+ (method as any).device = stubDevice({
3089
+ getCommands: () => ({ typedCall }),
2234
3090
  });
2235
3091
  method.postProgressMessage = jest.fn();
2236
3092
 
@@ -2242,10 +3098,11 @@ describe('Protocol V2 firmware update targets', () => {
2242
3098
  });
2243
3099
 
2244
3100
  const writePayloads = typedCall.mock.calls.map(call => call[2]);
2245
- expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 4096]);
2246
- expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([4096, 1]);
3101
+ expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 4000]);
3102
+ expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([4000, 97]);
2247
3103
  expect(writePayloads.map(payload => payload.overwrite)).toEqual([true, false]);
2248
3104
  expect(writePayloads.every(payload => payload.append === false)).toBe(true);
3105
+ expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 100]);
2249
3106
  });
2250
3107
 
2251
3108
  test('continues to DeviceFirmwareUpdate when FilesystemFileWrite returns processed chunk length', async () => {
@@ -2264,9 +3121,17 @@ describe('Protocol V2 firmware update targets', () => {
2264
3121
  },
2265
3122
  });
2266
3123
  }
2267
- if (name === 'DevFirmwareUpdate') {
3124
+ if (name === 'DeviceFirmwareUpdateRequest') {
2268
3125
  return Promise.resolve({ type: 'Success', message: { message: 'ok' } });
2269
3126
  }
3127
+ if (name === 'DeviceFirmwareUpdateStatusGet') {
3128
+ return Promise.resolve({
3129
+ type: 'DeviceFirmwareUpdateStatus',
3130
+ message: {
3131
+ records: [{ target_id: 4, status: 2 }],
3132
+ },
3133
+ });
3134
+ }
2270
3135
  return Promise.reject(new Error(`unexpected call ${name}`));
2271
3136
  });
2272
3137
 
@@ -2277,21 +3142,22 @@ describe('Protocol V2 firmware update targets', () => {
2277
3142
  method.postTipMessage = jest.fn();
2278
3143
 
2279
3144
  await (method as any).executeProtocolV2Update({
2280
- resourceBinary: null,
3145
+ resourceBinaryMap: [],
2281
3146
  bootloaderBinary: null,
2282
3147
  fwBinaryMap: [
2283
3148
  {
2284
3149
  fileName: 'firmware.bin',
2285
3150
  binary: new Uint8Array(4097).buffer,
3151
+ targetId: 4,
2286
3152
  },
2287
3153
  ],
2288
3154
  });
2289
3155
 
2290
3156
  expect(typedCall).toHaveBeenCalledWith(
2291
- 'DevFirmwareUpdate',
2292
- ['Success', 'DevFirmwareUpdateStatus'],
3157
+ 'DeviceFirmwareUpdateRequest',
3158
+ ['Success', 'DeviceFirmwareUpdateStatus'],
2293
3159
  {
2294
- targets: [{ target_id: 0, path: 'vol1:firmware.bin' }],
3160
+ targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
2295
3161
  },
2296
3162
  expect.any(Object)
2297
3163
  );
@@ -2337,6 +3203,7 @@ describe('Protocol V2 firmware update targets', () => {
2337
3203
  const writePayloads = typedCall.mock.calls.map(call => call[2]);
2338
3204
  expect(writePayloads.map(payload => payload.file.offset)).toEqual([0, 1800]);
2339
3205
  expect(writePayloads.map(payload => payload.file.data.byteLength)).toEqual([1800, 1]);
3206
+ expect(writePayloads.map(payload => payload.ui_percentage)).toEqual([0, 100]);
2340
3207
  });
2341
3208
 
2342
3209
  test('consumes Protocol V2 install progress before final update success', async () => {
@@ -2355,18 +3222,18 @@ describe('Protocol V2 firmware update targets', () => {
2355
3222
  method.postTipMessage = jest.fn();
2356
3223
 
2357
3224
  await (method as any).protocolV2StartFirmwareUpdate({
2358
- targets: [{ target_id: 0, path: 'vol1:firmware.bin' }],
3225
+ targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
2359
3226
  });
2360
3227
 
2361
3228
  const callOptions = typedCall.mock.calls[0][3];
2362
- expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DevFirmwareUpdateStatus']);
2363
- expect(callOptions.intermediateTypes).toEqual(['DevFirmwareInstallProgress']);
3229
+ expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DeviceFirmwareUpdateStatus']);
3230
+ expect(callOptions.intermediateTypes).toEqual(['DeviceFirmwareUpdateStatus']);
2364
3231
  callOptions.onIntermediateResponse({
2365
- type: 'DevFirmwareInstallProgress',
2366
- message: { target_id: 0, progress: 42 },
3232
+ type: 'DeviceFirmwareUpdateStatus',
3233
+ message: { records: [{ target_id: 4, status: 1 }] },
2367
3234
  });
2368
3235
 
2369
- expect(method.postProgressMessage).toHaveBeenCalledWith(42, 'installingFirmware');
3236
+ expect(method.postProgressMessage).toHaveBeenCalledWith(99, 'installingFirmware');
2370
3237
  });
2371
3238
 
2372
3239
  test('accepts Protocol V2 firmware update status as start response', async () => {
@@ -2377,8 +3244,8 @@ describe('Protocol V2 firmware update targets', () => {
2377
3244
  },
2378
3245
  });
2379
3246
  const typedCall = jest.fn().mockResolvedValue({
2380
- type: 'DevFirmwareUpdateStatus',
2381
- message: { targets: [{ target_id: 0, status: 1 }] },
3247
+ type: 'DeviceFirmwareUpdateStatus',
3248
+ message: { records: [{ target_id: 4, status: 1 }] },
2382
3249
  });
2383
3250
 
2384
3251
  (method as any).device = stubDevice({
@@ -2388,29 +3255,29 @@ describe('Protocol V2 firmware update targets', () => {
2388
3255
  method.postTipMessage = jest.fn();
2389
3256
 
2390
3257
  await (method as any).protocolV2StartFirmwareUpdate({
2391
- targets: [{ target_id: 0, path: 'vol1:firmware.bin' }],
3258
+ targets: [{ target_id: 4, path: 'vol1:firmware.bin' }],
2392
3259
  });
2393
3260
 
2394
- expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DevFirmwareUpdateStatus']);
3261
+ expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DeviceFirmwareUpdateStatus']);
2395
3262
  expect(method.postTipMessage).toHaveBeenCalledWith('FirmwareUpdating');
2396
3263
  });
2397
3264
  });
2398
3265
 
2399
3266
  describe('Protocol V2 firmware update method', () => {
2400
- test('returns DevFirmwareUpdateStatus from low-level update trigger', async () => {
3267
+ test('returns DeviceFirmwareUpdateStatus from low-level update trigger', async () => {
2401
3268
  const method = new DeviceFirmwareUpdate({
2402
3269
  id: 1,
2403
3270
  payload: {
2404
3271
  method: 'deviceFirmwareUpdate',
2405
- targetId: 0,
3272
+ targetId: 4,
2406
3273
  path: 'vol0:firmware.bin',
2407
3274
  },
2408
3275
  });
2409
3276
  method.init();
2410
3277
 
2411
3278
  const typedCall = jest.fn().mockResolvedValue({
2412
- type: 'DevFirmwareUpdateStatus',
2413
- message: { targets: [{ target_id: 0, status: 1 }] },
3279
+ type: 'DeviceFirmwareUpdateStatus',
3280
+ message: { records: [{ target_id: 4, status: 1 }] },
2414
3281
  });
2415
3282
 
2416
3283
  (method as any).device = stubDevice({
@@ -2418,11 +3285,11 @@ describe('Protocol V2 firmware update method', () => {
2418
3285
  });
2419
3286
 
2420
3287
  await expect(method.run()).resolves.toEqual({
2421
- targets: [{ target_id: 0, status: 1 }],
3288
+ records: [{ target_id: 4, status: 1 }],
2422
3289
  });
2423
- expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DevFirmwareUpdateStatus']);
3290
+ expect(typedCall.mock.calls[0][1]).toEqual(['Success', 'DeviceFirmwareUpdateStatus']);
2424
3291
  expect(typedCall.mock.calls[0][2]).toEqual({
2425
- targets: [{ target_id: 0, path: 'vol0:firmware.bin' }],
3292
+ targets: [{ target_id: 4, path: 'vol0:firmware.bin' }],
2426
3293
  });
2427
3294
  });
2428
3295
 
@@ -2431,7 +3298,7 @@ describe('Protocol V2 firmware update method', () => {
2431
3298
  id: 1,
2432
3299
  payload: {
2433
3300
  method: 'deviceFirmwareUpdate',
2434
- targetId: 0,
3301
+ targetId: 4,
2435
3302
  path: 'vol0:firmware.bin',
2436
3303
  },
2437
3304
  });
@@ -2485,7 +3352,7 @@ describe('Protocol V2 firmware update method', () => {
2485
3352
  targets: [
2486
3353
  {
2487
3354
  target_id: undefined,
2488
- targetId: 3,
3355
+ targetId: 4,
2489
3356
  path: 'vol0:firmware.bin',
2490
3357
  },
2491
3358
  ],
@@ -2498,32 +3365,72 @@ describe('Protocol V2 firmware update method', () => {
2498
3365
 
2499
3366
  await method.run();
2500
3367
  expect(typedCall.mock.calls[0][2]).toEqual({
2501
- targets: [{ target_id: 3, path: 'vol0:firmware.bin' }],
3368
+ targets: [{ target_id: 4, path: 'vol0:firmware.bin' }],
2502
3369
  });
2503
3370
  });
2504
- });
2505
3371
 
2506
- describe('Protocol V2 reboot methods', () => {
2507
- test('sends DevReboot from devReboot', async () => {
2508
- const typedCall = jest.fn().mockResolvedValue({ message: { message: 'ok' } });
2509
- const method = new DevReboot({
3372
+ test('accepts firmware target enum names from low-level update params', async () => {
3373
+ const typedCall = jest.fn().mockResolvedValue({ message: {} });
3374
+ const method = new DeviceFirmwareUpdate({
2510
3375
  id: 1,
2511
3376
  payload: {
2512
- method: 'devReboot',
2513
- rebootType: 2,
3377
+ method: 'deviceFirmwareUpdate',
3378
+ targetId: 'FW_MGMT_TARGET_CRATE',
3379
+ path: 'vol0:resource.bin',
2514
3380
  },
2515
3381
  });
2516
3382
  method.init();
2517
- (method as any).device = stubDevice({ commands: { typedCall } });
3383
+ (method as any).device = stubDevice({
3384
+ commands: { typedCall },
3385
+ });
2518
3386
 
2519
3387
  await method.run();
3388
+ expect(typedCall.mock.calls[0][2]).toEqual({
3389
+ targets: [{ target_id: 1, path: 'vol0:resource.bin' }],
3390
+ });
3391
+ });
2520
3392
 
2521
- expect(typedCall).toHaveBeenCalledWith('DevReboot', 'Success', {
2522
- reboot_type: 2,
3393
+ test('passes firmware update status fields through to Protocol V2', async () => {
3394
+ const typedCall = jest.fn().mockResolvedValue({
3395
+ message: {
3396
+ records: [{ target_id: 4, status: 2, payload_version: 1, path: 'vol1:application_p1.bin' }],
3397
+ },
2523
3398
  });
3399
+ const method = new DeviceGetFirmwareUpdateStatus({
3400
+ id: 1,
3401
+ payload: {
3402
+ method: 'deviceGetFirmwareUpdateStatus',
3403
+ fields: {
3404
+ status: true,
3405
+ payload_version: true,
3406
+ path: true,
3407
+ },
3408
+ },
3409
+ });
3410
+ method.init();
3411
+ (method as any).device = stubDevice({
3412
+ commands: { typedCall },
3413
+ });
3414
+
3415
+ await expect(method.run()).resolves.toEqual({
3416
+ records: [{ target_id: 4, status: 2, payload_version: 1, path: 'vol1:application_p1.bin' }],
3417
+ });
3418
+ expect(typedCall).toHaveBeenCalledWith(
3419
+ 'DeviceFirmwareUpdateStatusGet',
3420
+ 'DeviceFirmwareUpdateStatus',
3421
+ {
3422
+ fields: {
3423
+ status: true,
3424
+ payload_version: true,
3425
+ path: true,
3426
+ },
3427
+ }
3428
+ );
2524
3429
  });
3430
+ });
2525
3431
 
2526
- test('sends DevReboot from deviceReboot', async () => {
3432
+ describe('Protocol V2 reboot methods', () => {
3433
+ test('sends DeviceReboot from deviceReboot', async () => {
2527
3434
  const typedCall = jest.fn().mockResolvedValue({ message: { message: 'ok' } });
2528
3435
  const method = new DeviceReboot({
2529
3436
  id: 1,
@@ -2537,18 +3444,86 @@ describe('Protocol V2 reboot methods', () => {
2537
3444
 
2538
3445
  await method.run();
2539
3446
 
2540
- expect(typedCall).toHaveBeenCalledWith('DevReboot', 'Success', {
3447
+ expect(typedCall).toHaveBeenCalledWith('DeviceReboot', 'Success', {
2541
3448
  reboot_type: 2,
2542
3449
  });
2543
3450
  });
2544
3451
  });
2545
3452
 
3453
+ describe('Protocol V2 current low-level methods', () => {
3454
+ test('sends ProtocolInfoRequest from protocolInfoRequest', async () => {
3455
+ const typedCall = jest.fn().mockResolvedValue({ message: { version: 1 } });
3456
+ const method = new ProtocolInfoRequest({
3457
+ id: 1,
3458
+ payload: { method: 'protocolInfoRequest' },
3459
+ });
3460
+ method.init();
3461
+ (method as any).device = stubDevice({ commands: { typedCall } });
3462
+
3463
+ await method.run();
3464
+
3465
+ expect(typedCall).toHaveBeenCalledWith('ProtocolInfoRequest', 'ProtocolInfo', {});
3466
+ });
3467
+
3468
+ test('sends DeviceFactoryInfoSet and DeviceFactoryInfoGet', async () => {
3469
+ const typedCall = jest.fn().mockResolvedValue({ message: {} });
3470
+ const setMethod = new DeviceFactoryInfoSet({
3471
+ id: 1,
3472
+ payload: {
3473
+ method: 'deviceFactoryInfoSet',
3474
+ serial_number: 'PR2SERIAL',
3475
+ burn_in_completed: true,
3476
+ },
3477
+ });
3478
+ setMethod.init();
3479
+ (setMethod as any).device = stubDevice({ commands: { typedCall } });
3480
+
3481
+ await setMethod.run();
3482
+
3483
+ expect(typedCall).toHaveBeenCalledWith('DeviceFactoryInfoSet', 'Success', {
3484
+ info: {
3485
+ version: undefined,
3486
+ serial_number: 'PR2SERIAL',
3487
+ burn_in_completed: true,
3488
+ factory_test_completed: undefined,
3489
+ manufacture_time: undefined,
3490
+ },
3491
+ });
3492
+
3493
+ typedCall.mockResolvedValueOnce({ message: { serial_number: 'PR2SERIAL' } });
3494
+ const getMethod = new DeviceFactoryInfoGet({
3495
+ id: 2,
3496
+ payload: { method: 'deviceFactoryInfoGet' },
3497
+ });
3498
+ getMethod.init();
3499
+ (getMethod as any).device = stubDevice({ commands: { typedCall } });
3500
+
3501
+ await getMethod.run();
3502
+
3503
+ expect(typedCall).toHaveBeenLastCalledWith('DeviceFactoryInfoGet', 'DeviceFactoryInfo', {});
3504
+ });
3505
+
3506
+ test('sends FilesystemPermissionFix from filesystemPermissionFix', async () => {
3507
+ const typedCall = jest.fn().mockResolvedValue({ message: {} });
3508
+ const method = new FilesystemPermissionFix({
3509
+ id: 1,
3510
+ payload: { method: 'filesystemPermissionFix' },
3511
+ });
3512
+ method.init();
3513
+ (method as any).device = stubDevice({ commands: { typedCall } });
3514
+
3515
+ await method.run();
3516
+
3517
+ expect(typedCall).toHaveBeenCalledWith('FilesystemPermissionFix', 'Success', {});
3518
+ });
3519
+ });
3520
+
2546
3521
  describe('Protocol V2 raw device info method', () => {
2547
3522
  const buildMethod = (payload: Record<string, unknown> = {}) => {
2548
- const method = new DeviceGetDeviceInfo({
3523
+ const method = new DeviceInfoGet({
2549
3524
  id: 1,
2550
3525
  payload: {
2551
- method: 'deviceGetDeviceInfo',
3526
+ method: 'deviceInfoGet',
2552
3527
  ...payload,
2553
3528
  },
2554
3529
  });
@@ -2558,7 +3533,7 @@ describe('Protocol V2 raw device info method', () => {
2558
3533
 
2559
3534
  test('passes requested targets/types through and returns the raw DeviceInfo message', async () => {
2560
3535
  const method = buildMethod({
2561
- targets: { hw: true, se1: true, junk: true },
3536
+ targets: { hw: true, se1: true },
2562
3537
  types: { version: true, hash: true },
2563
3538
  });
2564
3539
  const typedCall = jest.fn().mockResolvedValue({
@@ -2574,9 +3549,8 @@ describe('Protocol V2 raw device info method', () => {
2574
3549
  protocol_version: 1,
2575
3550
  hw: { serial_no: 'PR2SERIAL' },
2576
3551
  });
2577
- // 未知 key(junk)被过滤,已选 key 原样透传,不构建 DeviceProfile
2578
3552
  expect(typedCall).toHaveBeenCalledWith(
2579
- 'DevGetDeviceInfo',
3553
+ 'DeviceInfoGet',
2580
3554
  'DeviceInfo',
2581
3555
  {
2582
3556
  targets: { hw: true, se1: true },
@@ -2586,6 +3560,20 @@ describe('Protocol V2 raw device info method', () => {
2586
3560
  );
2587
3561
  });
2588
3562
 
3563
+ test('rejects removed target/type names before transport call', () => {
3564
+ expect(() =>
3565
+ buildMethod({
3566
+ targets: { hw: true, bt: true },
3567
+ })
3568
+ ).toThrow('targets');
3569
+
3570
+ expect(() =>
3571
+ buildMethod({
3572
+ types: { version: true, digest: true },
3573
+ })
3574
+ ).toThrow('types');
3575
+ });
3576
+
2589
3577
  test('defaults to the basic targets/types when none are given', async () => {
2590
3578
  const method = buildMethod();
2591
3579
  const typedCall = jest.fn().mockResolvedValue({ type: 'DeviceInfo', message: {} });
@@ -2597,10 +3585,10 @@ describe('Protocol V2 raw device info method', () => {
2597
3585
  await method.run();
2598
3586
 
2599
3587
  expect(typedCall).toHaveBeenCalledWith(
2600
- 'DevGetDeviceInfo',
3588
+ 'DeviceInfoGet',
2601
3589
  'DeviceInfo',
2602
3590
  {
2603
- targets: { hw: true, fw: true, bt: true, status: true },
3591
+ targets: { hw: true, fw: true, coprocessor: true, status: true },
2604
3592
  types: { version: true, specific: true },
2605
3593
  },
2606
3594
  { timeoutMs: PROTOCOL_V2_DEVICE_INFO_TIMEOUT_MS }
@@ -2620,48 +3608,6 @@ describe('Protocol V2 raw device info method', () => {
2620
3608
  });
2621
3609
  });
2622
3610
 
2623
- describe('Protocol V2 onboarding status method', () => {
2624
- test('returns OnboardingStatus from low-level status query', async () => {
2625
- const method = new DeviceGetOnboardingStatus({
2626
- id: 1,
2627
- payload: {
2628
- method: 'deviceGetOnboardingStatus',
2629
- },
2630
- });
2631
- method.init();
2632
-
2633
- const typedCall = jest.fn().mockResolvedValue({
2634
- type: 'OnboardingStatus',
2635
- message: {
2636
- step: 3,
2637
- setup: {
2638
- restore: {
2639
- mnemonic: true,
2640
- },
2641
- },
2642
- detail_code: 7,
2643
- detail_str: 'Recovery Phrase',
2644
- },
2645
- });
2646
-
2647
- (method as any).device = stubDevice({
2648
- commands: { typedCall },
2649
- });
2650
-
2651
- await expect(method.run()).resolves.toEqual({
2652
- step: 3,
2653
- setup: {
2654
- restore: {
2655
- mnemonic: true,
2656
- },
2657
- },
2658
- detail_code: 7,
2659
- detail_str: 'Recovery Phrase',
2660
- });
2661
- expect(typedCall).toHaveBeenCalledWith('GetOnboardingStatus', 'OnboardingStatus', {});
2662
- });
2663
- });
2664
-
2665
3611
  describe('Protocol V2 file write method', () => {
2666
3612
  test('rejects invalid write parameters before transport call', () => {
2667
3613
  expect(() => {
@@ -2711,17 +3657,22 @@ describe('Protocol V2 file write method', () => {
2711
3657
  method.init();
2712
3658
  await method.run();
2713
3659
 
2714
- expect(typedCall).toHaveBeenCalledWith('FilesystemFileWrite', 'FilesystemFile', {
2715
- file: {
2716
- path: 'vol1:test.bin',
2717
- offset: 1,
2718
- total_size: 2,
2719
- data: new Uint8Array([1]),
3660
+ expect(typedCall).toHaveBeenCalledWith(
3661
+ 'FilesystemFileWrite',
3662
+ 'FilesystemFile',
3663
+ {
3664
+ file: {
3665
+ path: 'vol1:test.bin',
3666
+ offset: 1,
3667
+ total_size: 2,
3668
+ data: new Uint8Array([1]),
3669
+ },
3670
+ overwrite: false,
3671
+ append: false,
3672
+ ui_percentage: 100,
2720
3673
  },
2721
- overwrite: false,
2722
- append: false,
2723
- ui_percentage: 99,
2724
- }, { timeoutMs: undefined });
3674
+ { timeoutMs: undefined }
3675
+ );
2725
3676
  expect(method.postMessage).toHaveBeenCalledWith({
2726
3677
  event: 'UI_EVENT',
2727
3678
  type: UI_REQUEST.DEVICE_PROGRESS,
@@ -2754,28 +3705,40 @@ describe('Protocol V2 file write method', () => {
2754
3705
  const result = await method.run();
2755
3706
 
2756
3707
  expect(typedCall).toHaveBeenCalledTimes(2);
2757
- expect(typedCall).toHaveBeenNthCalledWith(1, 'FilesystemFileWrite', 'FilesystemFile', {
2758
- file: {
2759
- path: 'vol1:test.bin',
2760
- offset: 0,
2761
- total_size: 4097,
2762
- data: data.slice(0, 4096),
2763
- },
2764
- overwrite: true,
2765
- append: false,
2766
- ui_percentage: 99,
2767
- }, { timeoutMs: undefined });
2768
- expect(typedCall).toHaveBeenNthCalledWith(2, 'FilesystemFileWrite', 'FilesystemFile', {
2769
- file: {
2770
- path: 'vol1:test.bin',
2771
- offset: 4096,
2772
- total_size: 4097,
2773
- data: data.slice(4096),
2774
- },
2775
- overwrite: false,
2776
- append: false,
2777
- ui_percentage: 99,
2778
- }, { timeoutMs: undefined });
3708
+ expect(typedCall).toHaveBeenNthCalledWith(
3709
+ 1,
3710
+ 'FilesystemFileWrite',
3711
+ 'FilesystemFile',
3712
+ {
3713
+ file: {
3714
+ path: 'vol1:test.bin',
3715
+ offset: 0,
3716
+ total_size: 4097,
3717
+ data: data.slice(0, 4000),
3718
+ },
3719
+ overwrite: true,
3720
+ append: false,
3721
+ ui_percentage: 0,
3722
+ },
3723
+ { timeoutMs: undefined }
3724
+ );
3725
+ expect(typedCall).toHaveBeenNthCalledWith(
3726
+ 2,
3727
+ 'FilesystemFileWrite',
3728
+ 'FilesystemFile',
3729
+ {
3730
+ file: {
3731
+ path: 'vol1:test.bin',
3732
+ offset: 4000,
3733
+ total_size: 4097,
3734
+ data: data.slice(4000),
3735
+ },
3736
+ overwrite: false,
3737
+ append: false,
3738
+ ui_percentage: 100,
3739
+ },
3740
+ { timeoutMs: undefined }
3741
+ );
2779
3742
  expect(result).toMatchObject({
2780
3743
  path: 'vol1:test.bin',
2781
3744
  processed_byte: 4097,
@@ -2785,8 +3748,8 @@ describe('Protocol V2 file write method', () => {
2785
3748
  event: 'UI_EVENT',
2786
3749
  type: UI_REQUEST.DEVICE_PROGRESS,
2787
3750
  payload: expect.objectContaining({
2788
- progress: 99,
2789
- transferredBytes: 4096,
3751
+ progress: 97,
3752
+ transferredBytes: 4000,
2790
3753
  totalBytes: 4097,
2791
3754
  elapsedMs: expect.any(Number),
2792
3755
  }),
@@ -2911,7 +3874,7 @@ describe('Protocol V2 file read method', () => {
2911
3874
  total_size: 0,
2912
3875
  },
2913
3876
  chunk_len: 64,
2914
- ui_percentage: 99,
3877
+ ui_percentage: 0,
2915
3878
  });
2916
3879
  expect(typedCall).toHaveBeenNthCalledWith(3, 'FilesystemFileRead', 'FilesystemFile', {
2917
3880
  file: {
@@ -2920,7 +3883,7 @@ describe('Protocol V2 file read method', () => {
2920
3883
  total_size: 0,
2921
3884
  },
2922
3885
  chunk_len: 1,
2923
- ui_percentage: 99,
3886
+ ui_percentage: 100,
2924
3887
  });
2925
3888
  expect(result.data.byteLength).toBe(65);
2926
3889
  expect(result.data[0]).toBe(1);