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

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