@onekeyfe/hd-core 1.2.0-alpha.47 → 1.2.0-alpha.49

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 (62) hide show
  1. package/__tests__/check-all-firmware-release-protocol-v2.test.ts +110 -15
  2. package/__tests__/device-lifecycle-events.test.ts +105 -3
  3. package/__tests__/method-protocol-support.test.ts +19 -0
  4. package/__tests__/protocol-binding.test.ts +89 -0
  5. package/__tests__/protocol-v2-resources.test.ts +87 -42
  6. package/__tests__/protocol-v2.test.ts +347 -40
  7. package/__tests__/search-devices.test.ts +12 -3
  8. package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
  9. package/dist/api/DetectDeviceConnectProtocol.d.ts +7 -0
  10. package/dist/api/DetectDeviceConnectProtocol.d.ts.map +1 -0
  11. package/dist/api/FirmwareUpdate.d.ts.map +1 -1
  12. package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
  13. package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
  14. package/dist/api/FirmwareUpdateV4.d.ts +1 -0
  15. package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
  16. package/dist/api/SearchDevices.d.ts.map +1 -1
  17. package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
  18. package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
  19. package/dist/api/index.d.ts +1 -0
  20. package/dist/api/index.d.ts.map +1 -1
  21. package/dist/core/index.d.ts.map +1 -1
  22. package/dist/device/Device.d.ts +5 -1
  23. package/dist/device/Device.d.ts.map +1 -1
  24. package/dist/device/DevicePool.d.ts.map +1 -1
  25. package/dist/index.d.ts +20 -3
  26. package/dist/index.js +361 -203
  27. package/dist/inject.d.ts +6 -1
  28. package/dist/inject.d.ts.map +1 -1
  29. package/dist/lowLevelInject.d.ts.map +1 -1
  30. package/dist/protocols/protocol-v2/resources.d.ts +4 -4
  31. package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
  32. package/dist/topLevelInject.d.ts.map +1 -1
  33. package/dist/types/api/detectDeviceConnectProtocol.d.ts +4 -0
  34. package/dist/types/api/detectDeviceConnectProtocol.d.ts.map +1 -0
  35. package/dist/types/api/index.d.ts +4 -0
  36. package/dist/types/api/index.d.ts.map +1 -1
  37. package/dist/types/params.d.ts +1 -0
  38. package/dist/types/params.d.ts.map +1 -1
  39. package/dist/utils/patch.d.ts +1 -1
  40. package/dist/utils/patch.d.ts.map +1 -1
  41. package/package.json +4 -4
  42. package/src/api/CheckAllFirmwareRelease.ts +10 -9
  43. package/src/api/DetectDeviceConnectProtocol.ts +18 -0
  44. package/src/api/FirmwareUpdate.ts +6 -2
  45. package/src/api/FirmwareUpdateV2.ts +5 -2
  46. package/src/api/FirmwareUpdateV3.ts +6 -2
  47. package/src/api/FirmwareUpdateV4.ts +73 -43
  48. package/src/api/SearchDevices.ts +3 -1
  49. package/src/api/firmware/FirmwareUpdateBaseMethod.ts +15 -4
  50. package/src/api/firmware/uploadFirmware.ts +17 -4
  51. package/src/api/index.ts +1 -0
  52. package/src/core/index.ts +11 -2
  53. package/src/data/messages/messages-protocol-v2.json +0 -43
  54. package/src/device/Device.ts +53 -15
  55. package/src/device/DevicePool.ts +7 -2
  56. package/src/inject.ts +57 -2
  57. package/src/lowLevelInject.ts +6 -3
  58. package/src/protocols/protocol-v2/resources.ts +150 -56
  59. package/src/topLevelInject.ts +6 -3
  60. package/src/types/api/detectDeviceConnectProtocol.ts +7 -0
  61. package/src/types/api/index.ts +11 -0
  62. package/src/types/params.ts +7 -1
@@ -95,6 +95,7 @@ export type InitOptions = {
95
95
  passphraseState?: string;
96
96
  deriveCardano?: boolean;
97
97
  connectProtocol?: HardwareConnectProtocol;
98
+ forceProtocolDetection?: boolean;
98
99
  protocolV2DeviceInfoTimeoutMs?: number;
99
100
  /** Refresh Protocol V2 runtime state before returning discovery results. */
100
101
  refreshRuntimeState?: boolean;
@@ -377,7 +378,10 @@ export class Device extends EventEmitter {
377
378
  * Device connect
378
379
  * @returns {Promise<boolean>}
379
380
  */
380
- connect(connectProtocol?: HardwareConnectProtocol) {
381
+ connect(
382
+ connectProtocol?: HardwareConnectProtocol,
383
+ options?: { forceProtocolDetection?: boolean }
384
+ ) {
381
385
  const env = DataManager.getSettings('env');
382
386
  // eslint-disable-next-line no-async-promise-executor
383
387
  return new Promise<boolean>(async (resolve, reject) => {
@@ -387,7 +391,7 @@ export class Device extends EventEmitter {
387
391
  return;
388
392
  }
389
393
  try {
390
- await this.acquire(connectProtocol);
394
+ await this.acquire(connectProtocol, options);
391
395
  resolve(true);
392
396
  } catch (error) {
393
397
  reject(error);
@@ -397,7 +401,7 @@ export class Device extends EventEmitter {
397
401
  // 不存在 Session ID 或存在 Session ID 但设备在别处使用,都需要 acquire 获取最新 sessionID
398
402
  if (!this.mainId || (!this.isUsedHere() && this.originalDescriptor)) {
399
403
  try {
400
- await this.acquire(connectProtocol);
404
+ await this.acquire(connectProtocol, options);
401
405
  resolve(true);
402
406
  } catch (error) {
403
407
  reject(error);
@@ -414,11 +418,16 @@ export class Device extends EventEmitter {
414
418
 
415
419
  async acquire(
416
420
  expectedProtocol?: HardwareConnectProtocol,
417
- options?: { throwOnRunPromiseError?: boolean }
421
+ options?: { throwOnRunPromiseError?: boolean; forceProtocolDetection?: boolean }
418
422
  ) {
419
423
  const env = DataManager.getSettings('env');
420
424
  const mainIdKey = DataManager.isBleConnect(env) ? 'id' : 'session';
421
- const protocolHint = expectedProtocol ? undefined : this.originalDescriptor.protocolType;
425
+ const previousProtocol = this.originalDescriptor.protocolType;
426
+ // A protocol stored after a successful probe is authoritative. Only the explicit
427
+ // first-connection/recovery path may bypass it and probe both protocols again.
428
+ const strictProtocol = options?.forceProtocolDetection
429
+ ? undefined
430
+ : expectedProtocol ?? this.originalDescriptor.protocolType;
422
431
  try {
423
432
  let acquireResult: unknown;
424
433
  if (DataManager.isBleConnect(env)) {
@@ -430,8 +439,8 @@ export class Device extends EventEmitter {
430
439
  this.originalDescriptor.id,
431
440
  undefined,
432
441
  true,
433
- expectedProtocol,
434
- protocolHint
442
+ strictProtocol,
443
+ undefined
435
444
  );
436
445
  this.mainId = (acquireResult as any)?.uuid ?? '';
437
446
  Log.debug('Expected uuid:', this.mainId);
@@ -440,24 +449,31 @@ export class Device extends EventEmitter {
440
449
  this.originalDescriptor.path,
441
450
  this.originalDescriptor.session,
442
451
  undefined,
443
- expectedProtocol,
444
- protocolHint
452
+ strictProtocol,
453
+ undefined
445
454
  );
446
455
  this.mainId = acquireResult as string | undefined;
447
456
  Log.debug('Expected session id:', this.mainId);
448
457
  }
449
- this.deviceAcquired = true;
450
- this.updateDescriptor({ [mainIdKey]: this.mainId } as unknown as DeviceDescriptor);
451
-
452
458
  // Propagate protocol version detected during acquire.
453
459
  const detectedProtocol =
454
460
  (acquireResult as { protocolType?: HardwareConnectProtocol } | undefined)?.protocolType ??
455
461
  TransportManager.transport?.getProtocolType?.(
456
462
  DataManager.isBleConnect(env) ? this.originalDescriptor.id : this.originalDescriptor.path
457
463
  );
464
+ if (options?.forceProtocolDetection && !detectedProtocol) {
465
+ throw ERRORS.TypedError(
466
+ HardwareErrorCode.RuntimeError,
467
+ `Active protocol detection returned no protocol for ${
468
+ this.originalDescriptor.path || this.originalDescriptor.id
469
+ }`
470
+ );
471
+ }
458
472
  if (detectedProtocol) {
459
473
  this.originalDescriptor.protocolType = detectedProtocol;
460
474
  }
475
+ this.deviceAcquired = true;
476
+ this.updateDescriptor({ [mainIdKey]: this.mainId } as unknown as DeviceDescriptor);
461
477
 
462
478
  if (this.commands) {
463
479
  await this.commands.dispose(false);
@@ -465,6 +481,22 @@ export class Device extends EventEmitter {
465
481
 
466
482
  this.commands = new DeviceCommands(this, this.mainId ?? '');
467
483
  } catch (error) {
484
+ if (options?.forceProtocolDetection) {
485
+ this.originalDescriptor.protocolType = previousProtocol;
486
+ const failedSession = this.mainId;
487
+ this.deviceAcquired = false;
488
+ if (failedSession) {
489
+ try {
490
+ await this.deviceConnector?.release?.(failedSession, false);
491
+ } catch (releaseError) {
492
+ Log.debug('Failed to release an unsuccessful protocol probe', releaseError);
493
+ }
494
+ }
495
+ if (!DataManager.isBleConnect(env)) {
496
+ this.mainId = null;
497
+ this.updateDescriptor({ session: null } as DeviceDescriptor);
498
+ }
499
+ }
468
500
  if (options?.throwOnRunPromiseError) {
469
501
  throw error;
470
502
  }
@@ -1349,11 +1381,17 @@ export class Device extends EventEmitter {
1349
1381
  }
1350
1382
  };
1351
1383
 
1352
- if (!this.isUsedHere() || this.commands.disposed) {
1353
- const env = DataManager.getSettings('env');
1384
+ const env = DataManager.getSettings('env');
1385
+ if (options.forceProtocolDetection && env !== 'react-native' && this.isUsedHere()) {
1386
+ await this.release();
1387
+ }
1388
+
1389
+ if (options.forceProtocolDetection || !this.isUsedHere() || this.commands.disposed) {
1354
1390
  if (env !== 'react-native') {
1355
1391
  try {
1356
- await this.acquire(options.connectProtocol);
1392
+ await this.acquire(options.connectProtocol, {
1393
+ forceProtocolDetection: options.forceProtocolDetection,
1394
+ });
1357
1395
  } catch (error) {
1358
1396
  clearRunPromise();
1359
1397
  runPromise.reject(error);
@@ -156,7 +156,9 @@ export class DevicePool extends EventEmitter {
156
156
  if (!device) {
157
157
  device = Device.fromDescriptor(descriptor);
158
158
  device.deviceConnector = this.connector;
159
- await device.connect(initOptions?.connectProtocol);
159
+ await device.connect(initOptions?.connectProtocol, {
160
+ forceProtocolDetection: initOptions?.forceProtocolDetection,
161
+ });
160
162
  try {
161
163
  await device.initialize(initOptions);
162
164
  if (initOptions?.refreshRuntimeState && device.isProtocolV2()) {
@@ -181,7 +183,10 @@ export class DevicePool extends EventEmitter {
181
183
  refreshError = error;
182
184
  }
183
185
  },
184
- { connectProtocol: initOptions.connectProtocol }
186
+ {
187
+ connectProtocol: initOptions.connectProtocol,
188
+ forceProtocolDetection: initOptions.forceProtocolDetection,
189
+ }
185
190
  );
186
191
  if (refreshError instanceof Error) throw refreshError;
187
192
  if (refreshError) throw new Error(String(refreshError));
package/src/inject.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { HardwareConnectProtocol } from '@onekeyfe/hd-shared';
1
2
  import type { Unsuccessful } from './types';
2
3
  import type { EventEmitter } from 'events';
3
4
  import type { CallMethod } from './events';
@@ -39,6 +40,50 @@ export interface InjectApi {
39
40
  switchTransport: CoreApi['switchTransport'];
40
41
  }
41
42
 
43
+ const normalizeConnectId = (connectId: string) => connectId.trim().toLowerCase();
44
+
45
+ /**
46
+ * Keeps verified protocols isolated per transport endpoint and injects them at
47
+ * the single public call boundary, including APIs that do not expose params.
48
+ */
49
+ export const createProtocolAwareCall = (rawCall: CoreApi['call']) => {
50
+ const protocolByConnectId = new Map<string, HardwareConnectProtocol>();
51
+
52
+ const setDeviceConnectProtocol: CoreApi['setDeviceConnectProtocol'] = (
53
+ connectId,
54
+ connectProtocol
55
+ ) => {
56
+ const normalizedConnectId = normalizeConnectId(connectId);
57
+ if (!normalizedConnectId) return;
58
+ if (connectProtocol) {
59
+ protocolByConnectId.set(normalizedConnectId, connectProtocol);
60
+ } else {
61
+ protocolByConnectId.delete(normalizedConnectId);
62
+ }
63
+ };
64
+
65
+ const call: CoreApi['call'] = params => {
66
+ if (!params || typeof params !== 'object') {
67
+ return rawCall(params);
68
+ }
69
+
70
+ const connectId = typeof params.connectId === 'string' ? params.connectId : undefined;
71
+ const boundProtocol = connectId
72
+ ? protocolByConnectId.get(normalizeConnectId(connectId))
73
+ : undefined;
74
+ if (
75
+ boundProtocol &&
76
+ params.connectProtocol === undefined &&
77
+ params.forceProtocolDetection !== true
78
+ ) {
79
+ return rawCall({ ...params, connectProtocol: boundProtocol });
80
+ }
81
+ return rawCall(params);
82
+ };
83
+
84
+ return { call, setDeviceConnectProtocol };
85
+ };
86
+
42
87
  export const inject = ({
43
88
  call,
44
89
  cancel,
@@ -49,6 +94,7 @@ export const inject = ({
49
94
  switchTransport,
50
95
  uiResponse,
51
96
  }: InjectApi): CoreApi => {
97
+ const protocolAwareCall = createProtocolAwareCall(call);
52
98
  const api: CoreApi = {
53
99
  on: <T extends string, P extends (...args: any[]) => any>(type: T, fn: P) => {
54
100
  eventEmitter.on(type, fn);
@@ -66,7 +112,9 @@ export const inject = ({
66
112
 
67
113
  init,
68
114
 
69
- call,
115
+ call: protocolAwareCall.call,
116
+
117
+ setDeviceConnectProtocol: protocolAwareCall.setDeviceConnectProtocol,
70
118
 
71
119
  dispose,
72
120
 
@@ -78,7 +126,7 @@ export const inject = ({
78
126
 
79
127
  switchTransport,
80
128
 
81
- ...createCoreApi(call),
129
+ ...createCoreApi(protocolAwareCall.call),
82
130
  };
83
131
  return api;
84
132
  };
@@ -93,6 +141,7 @@ export const createCoreApi = (
93
141
  | 'removeAllListeners'
94
142
  | 'init'
95
143
  | 'call'
144
+ | 'setDeviceConnectProtocol'
96
145
  | 'dispose'
97
146
  | 'uiResponse'
98
147
  | 'cancel'
@@ -105,6 +154,12 @@ export const createCoreApi = (
105
154
  * 搜索设备
106
155
  */
107
156
  searchDevices: params => call({ ...params, method: 'searchDevices' }),
157
+ detectDeviceConnectProtocol: connectId =>
158
+ call({
159
+ connectId,
160
+ method: 'detectDeviceConnectProtocol',
161
+ forceProtocolDetection: true,
162
+ }),
108
163
 
109
164
  /**
110
165
  * 获取设备信息
@@ -1,4 +1,4 @@
1
- import { createCoreApi } from './inject';
1
+ import { createCoreApi, createProtocolAwareCall } from './inject';
2
2
 
3
3
  import type { EventEmitter } from 'events';
4
4
  import type { CallMethod, CoreMessage } from './events';
@@ -33,6 +33,7 @@ export const lowLevelInject = ({
33
33
  switchTransport,
34
34
  addHardwareGlobalEventListener,
35
35
  }: LowLevelInjectApi): LowLevelCoreApi => {
36
+ const protocolAwareCall = createProtocolAwareCall(call);
36
37
  const api: LowLevelCoreApi = {
37
38
  addHardwareGlobalEventListener,
38
39
  removeAllListeners: type => {
@@ -41,7 +42,9 @@ export const lowLevelInject = ({
41
42
 
42
43
  init,
43
44
 
44
- call,
45
+ call: protocolAwareCall.call,
46
+
47
+ setDeviceConnectProtocol: protocolAwareCall.setDeviceConnectProtocol,
45
48
 
46
49
  dispose,
47
50
 
@@ -55,7 +58,7 @@ export const lowLevelInject = ({
55
58
 
56
59
  emit: () => {},
57
60
 
58
- ...createCoreApi(call),
61
+ ...createCoreApi(protocolAwareCall.call),
59
62
  };
60
63
  return api;
61
64
  };
@@ -1,4 +1,5 @@
1
1
  import { sha256 } from '@noble/hashes/sha256';
2
+ import { PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE } from '@onekeyfe/hd-transport';
2
3
 
3
4
  import type {
4
5
  IProtocolV2BootResources,
@@ -7,7 +8,6 @@ import type {
7
8
  IProtocolV2Resources,
8
9
  } from '../../types';
9
10
  import type { DeviceCommands } from '../../device/DeviceCommands';
10
- import type { ResourceInventory } from '@onekeyfe/hd-transport';
11
11
 
12
12
  export const PROTOCOL_V2_RESOURCE_TYPES = [
13
13
  'images',
@@ -31,23 +31,16 @@ export const PROTOCOL_V2_RESOURCE_DEVICE_PATHS: Readonly<Record<IProtocolV2Resou
31
31
  const RESOURCE_TYPE_SET = new Set<string>(PROTOCOL_V2_RESOURCE_TYPES);
32
32
  const SHA256_HEX_LENGTH = 64;
33
33
  const SHA3_512_HEX_LENGTH = 128;
34
+ const PROTOCOL_V2_OKPP_HEADER_SIZE = 0x52a0;
35
+ const PROTOCOL_V2_OKPP_TYPE_OFFSET = 0x08;
36
+ const PROTOCOL_V2_OKPP_HEADER_LENGTH_OFFSET = 0x0c;
37
+ const PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET = 0x240;
38
+ const PROTOCOL_V2_OKPP_HASH_SIZE = 64;
39
+ const PROTOCOL_V2_RESOURCE_IDENTITY_READ_SIZE =
40
+ PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET + PROTOCOL_V2_OKPP_HASH_SIZE;
41
+ const PROTOCOL_V2_MIN_FILE_READ_CHUNK_SIZE = 64;
34
42
  export const PROTOCOL_V2_RESOURCE_INVENTORY_TIMEOUT_MS = 5 * 1000;
35
43
 
36
- const RESOURCE_TYPE_BY_DEVICE_VALUE: Readonly<Record<string, IProtocolV2ResourceType>> = {
37
- '0': 'images',
38
- IMAGES: 'images',
39
- '1': 'animation',
40
- ANIMATION: 'animation',
41
- '2': 'wallpaper',
42
- WALLPAPER: 'wallpaper',
43
- '3': 'translations',
44
- TRANSLATIONS: 'translations',
45
- '4': 'roobert',
46
- ROOBERT: 'roobert',
47
- '5': 'noto',
48
- NOTO: 'noto',
49
- };
50
-
51
44
  export type ProtocolV2ResourceInventoryItem = {
52
45
  type: IProtocolV2ResourceType;
53
46
  size: number;
@@ -61,57 +54,153 @@ export type ProtocolV2ResourceUpdatePlan = {
61
54
  resources: IProtocolV2Resource[];
62
55
  };
63
56
 
64
- /** Normalize the success-only device response into the SDK resource identity shape. */
65
- export function parseProtocolV2ResourceInventory(
66
- value: ResourceInventory | unknown
67
- ): ProtocolV2ResourceInventoryItem[] {
68
- const items = (value as { items?: unknown })?.items;
69
- if (!Array.isArray(items)) {
70
- throw new Error('Invalid Pro2 resource inventory: items must be an array');
57
+ function toFiniteNumber(value: unknown): number | undefined {
58
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
59
+ if (typeof value === 'string') {
60
+ const numeric = Number(value);
61
+ return Number.isFinite(numeric) ? numeric : undefined;
71
62
  }
72
-
73
- const inventory = items.map((item, index) => {
74
- if (!item || typeof item !== 'object') {
75
- throw new Error(`Invalid Pro2 resource inventory item at ${index}`);
63
+ if (value && typeof value === 'object') {
64
+ const longLike = value as { toNumber?: () => number };
65
+ if (typeof longLike.toNumber === 'function') {
66
+ const numeric = longLike.toNumber();
67
+ return Number.isFinite(numeric) ? numeric : undefined;
76
68
  }
77
- const raw = item as { type?: unknown; size?: unknown; header_hash?: unknown };
78
- const type = RESOURCE_TYPE_BY_DEVICE_VALUE[String(raw.type).toUpperCase()];
79
- if (!type) {
80
- throw new Error(`Invalid Pro2 resource inventory type at ${index}`);
69
+ }
70
+ return undefined;
71
+ }
72
+
73
+ function toUint8Array(value: unknown): Uint8Array {
74
+ if (value instanceof Uint8Array) return value;
75
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
76
+ if (ArrayBuffer.isView(value)) {
77
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
78
+ }
79
+ if (typeof value === 'string') {
80
+ const hex = value.replace(/^0x/i, '');
81
+ if (!hex || hex.length % 2 !== 0 || /[^0-9a-f]/i.test(hex)) {
82
+ return new Uint8Array(0);
81
83
  }
82
- if (!Number.isSafeInteger(raw.size) || Number(raw.size) <= 0) {
83
- throw new Error(`Invalid Pro2 resource inventory size at ${index}`);
84
+ const bytes = new Uint8Array(hex.length / 2);
85
+ for (let index = 0; index < bytes.length; index += 1) {
86
+ bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
84
87
  }
85
- return {
86
- type,
87
- size: Number(raw.size),
88
- headerHash: normalizeHex(raw.header_hash, SHA3_512_HEX_LENGTH, 'inventory headerHash'),
89
- };
90
- });
88
+ return bytes;
89
+ }
90
+ return new Uint8Array(0);
91
+ }
92
+
93
+ function readAscii(bytes: Uint8Array, offset: number, length: number): string {
94
+ return Array.from(bytes.slice(offset, offset + length), byte => String.fromCharCode(byte)).join(
95
+ ''
96
+ );
97
+ }
91
98
 
92
- if (new Set(inventory.map(item => item.type)).size !== inventory.length) {
93
- throw new Error('Invalid Pro2 resource inventory: duplicate resource type');
99
+ function parseProtocolV2ResourceHeaderHash(bytes: Uint8Array): string | undefined {
100
+ if (bytes.byteLength < PROTOCOL_V2_RESOURCE_IDENTITY_READ_SIZE) return undefined;
101
+ if (readAscii(bytes, 0, 4) !== 'OKPP') return undefined;
102
+ if (readAscii(bytes, PROTOCOL_V2_OKPP_TYPE_OFFSET, 4) !== 'RESC') return undefined;
103
+
104
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
105
+ if (
106
+ view.getUint32(PROTOCOL_V2_OKPP_HEADER_LENGTH_OFFSET, true) !== PROTOCOL_V2_OKPP_HEADER_SIZE
107
+ ) {
108
+ return undefined;
94
109
  }
95
- return PROTOCOL_V2_RESOURCE_TYPES.flatMap(type => {
96
- const item = inventory.find(candidate => candidate.type === type);
97
- return item ? [item] : [];
98
- });
110
+ return bytesToHex(
111
+ bytes.slice(
112
+ PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET,
113
+ PROTOCOL_V2_OKPP_HEADER_HASH_OFFSET + PROTOCOL_V2_OKPP_HASH_SIZE
114
+ )
115
+ );
99
116
  }
100
117
 
101
- export async function requestProtocolV2ResourceInventory({
118
+ async function readProtocolV2ResourceIdentity({
102
119
  commands,
120
+ resource,
121
+ chunkSize,
103
122
  timeoutMs = PROTOCOL_V2_RESOURCE_INVENTORY_TIMEOUT_MS,
104
123
  }: {
105
- commands: DeviceCommands;
124
+ commands: Pick<DeviceCommands, 'typedCall'>;
125
+ resource: IProtocolV2Resource;
126
+ chunkSize: number;
106
127
  timeoutMs?: number;
107
- }): Promise<ProtocolV2ResourceInventoryItem[]> {
108
- const { message } = await commands.typedCall(
109
- 'ResourceInventoryGet',
110
- 'ResourceInventory',
111
- {},
128
+ }): Promise<ProtocolV2ResourceInventoryItem | undefined> {
129
+ const path = PROTOCOL_V2_RESOURCE_DEVICE_PATHS[resource.type];
130
+ const pathInfo = await commands.typedCall(
131
+ 'FilesystemPathInfoQuery',
132
+ 'FilesystemPathInfo',
133
+ { path },
112
134
  { timeoutMs }
113
135
  );
114
- return parseProtocolV2ResourceInventory(message);
136
+ const size = toFiniteNumber(pathInfo.message?.size);
137
+ if (
138
+ !pathInfo.message?.exist ||
139
+ pathInfo.message?.directory ||
140
+ !Number.isSafeInteger(size) ||
141
+ size !== resource.size ||
142
+ size < PROTOCOL_V2_OKPP_HEADER_SIZE
143
+ ) {
144
+ return undefined;
145
+ }
146
+
147
+ const header = new Uint8Array(PROTOCOL_V2_RESOURCE_IDENTITY_READ_SIZE);
148
+ let offset = 0;
149
+ while (offset < header.byteLength) {
150
+ const readLength = Math.min(chunkSize, header.byteLength - offset);
151
+ const response = await commands.typedCall(
152
+ 'FilesystemFileRead',
153
+ 'FilesystemFile',
154
+ {
155
+ file: { path, offset, total_size: 0 },
156
+ chunk_len: readLength,
157
+ },
158
+ { timeoutMs }
159
+ );
160
+ const data = toUint8Array(response.message?.data);
161
+ if (data.byteLength === 0) return undefined;
162
+ const copied = Math.min(data.byteLength, header.byteLength - offset);
163
+ header.set(data.subarray(0, copied), offset);
164
+ offset += copied;
165
+ }
166
+
167
+ const headerHash = parseProtocolV2ResourceHeaderHash(header);
168
+ return headerHash ? { type: resource.type, size, headerHash } : undefined;
169
+ }
170
+
171
+ /**
172
+ * 使用已发布 Pro2 固件支持的文件系统消息构建资源清单。
173
+ * 缺失、无法读取或格式错误的文件不会进入清单,因此会被选中重写。
174
+ */
175
+ export async function readProtocolV2ResourceInventory({
176
+ commands,
177
+ resources,
178
+ chunkSize = PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE,
179
+ timeoutMs = PROTOCOL_V2_RESOURCE_INVENTORY_TIMEOUT_MS,
180
+ }: {
181
+ commands: Pick<DeviceCommands, 'typedCall'>;
182
+ resources: readonly IProtocolV2Resource[];
183
+ chunkSize?: number;
184
+ timeoutMs?: number;
185
+ }): Promise<ProtocolV2ResourceInventoryItem[]> {
186
+ const normalizedChunkSize = Number.isFinite(chunkSize)
187
+ ? Math.max(Math.floor(chunkSize), PROTOCOL_V2_MIN_FILE_READ_CHUNK_SIZE)
188
+ : PROTOCOL_V2_BLE_FILE_READ_CHUNK_SIZE;
189
+ const inventory: ProtocolV2ResourceInventoryItem[] = [];
190
+ for (const resource of resources) {
191
+ try {
192
+ const item = await readProtocolV2ResourceIdentity({
193
+ commands,
194
+ resource,
195
+ chunkSize: normalizedChunkSize,
196
+ timeoutMs,
197
+ });
198
+ if (item) inventory.push(item);
199
+ } catch {
200
+ // 单个资源无法读取时按缺失处理,不阻断其余资源的增量检查。
201
+ }
202
+ }
203
+ return inventory;
115
204
  }
116
205
 
117
206
  function normalizeHex(value: unknown, expectedLength: number, field: string): string {
@@ -213,7 +302,7 @@ export function parseProtocolV2Resources(value: unknown): IProtocolV2Resources |
213
302
  };
214
303
  }
215
304
 
216
- /** Compare the application inventory or select the full set for bootloader recovery. */
305
+ /** 比较文件系统资源清单;恢复模式无法取得清单时回退到全量更新。 */
217
306
  export function buildProtocolV2ResourceUpdatePlan({
218
307
  resources,
219
308
  inventory,
@@ -225,14 +314,19 @@ export function buildProtocolV2ResourceUpdatePlan({
225
314
  mode: ProtocolV2ResourceUpdateMode;
226
315
  forced?: boolean;
227
316
  }): ProtocolV2ResourceUpdatePlan {
228
- if (mode === 'bootloader-recovery' || forced) {
317
+ if (forced) {
229
318
  return {
230
319
  status: resources.length > 0 ? 'outdated' : 'valid',
231
320
  resources: [...resources],
232
321
  };
233
322
  }
234
323
  if (!inventory) {
235
- return { status: 'unknown', resources: [] };
324
+ return mode === 'bootloader-recovery'
325
+ ? {
326
+ status: resources.length > 0 ? 'outdated' : 'valid',
327
+ resources: [...resources],
328
+ }
329
+ : { status: 'unknown', resources: [] };
236
330
  }
237
331
 
238
332
  const inventoryByType = new Map(inventory.map(item => [item.type, item]));
@@ -1,6 +1,6 @@
1
1
  import EventEmitter from 'events';
2
2
 
3
- import { createCoreApi } from './inject';
3
+ import { createCoreApi, createProtocolAwareCall } from './inject';
4
4
 
5
5
  import type { ConnectSettings } from './types/settings';
6
6
  import type { CoreApi } from './types/api';
@@ -19,6 +19,7 @@ export const topLevelInject = () => {
19
19
  if (!lowLevelApi) return Promise.resolve(undefined);
20
20
  return lowLevelApi.call(params);
21
21
  };
22
+ const protocolAwareCall = createProtocolAwareCall(call);
22
23
  const api: CoreApi = {
23
24
  on: <T extends string, P extends (...args: any[]) => any>(type: T, fn: P) => {
24
25
  eventEmitter.on(type, fn);
@@ -37,9 +38,11 @@ export const topLevelInject = () => {
37
38
  return lowLevelApi?.init(settings) ?? Promise.resolve(false);
38
39
  },
39
40
 
40
- call,
41
+ call: protocolAwareCall.call,
41
42
 
42
- ...createCoreApi(call),
43
+ setDeviceConnectProtocol: protocolAwareCall.setDeviceConnectProtocol,
44
+
45
+ ...createCoreApi(protocolAwareCall.call),
43
46
 
44
47
  removeAllListeners: type => {
45
48
  eventEmitter.removeAllListeners(type);
@@ -0,0 +1,7 @@
1
+ import type { HardwareConnectProtocol } from '@onekeyfe/hd-shared';
2
+
3
+ import type { Response } from '../params';
4
+
5
+ export declare function detectDeviceConnectProtocol(
6
+ connectId: string
7
+ ): Response<HardwareConnectProtocol>;
@@ -19,6 +19,7 @@ import type { checkBootloaderRelease } from './checkBootloaderRelease';
19
19
  import type { checkAllFirmwareRelease } from './checkAllFirmwareRelease';
20
20
  import type { checkFirmwareTypeAvailable } from './checkFirmwareTypeAvailable';
21
21
  import type { searchDevices } from './searchDevices';
22
+ import type { detectDeviceConnectProtocol } from './detectDeviceConnectProtocol';
22
23
  import type { getFeatures } from './getFeatures';
23
24
  import type { getDeviceState } from './getDeviceState';
24
25
  import type { getOnekeyFeatures } from './getOnekeyFeatures';
@@ -145,6 +146,7 @@ import type { benfenSignMessage } from './benfenSignMessage';
145
146
  import type { neoGetAddress } from './neoGetAddress';
146
147
  import type { neoSignTransaction } from './neoSignTransaction';
147
148
  import type { ConnectSettings } from '../settings';
149
+ import type { HardwareConnectProtocol } from '@onekeyfe/hd-shared';
148
150
 
149
151
  export * from './export';
150
152
  export type { DeviceStateScope, GetDeviceStateParams } from './getDeviceState';
@@ -169,6 +171,14 @@ export type CoreApi = {
169
171
  removeAllListeners: typeof removeAllListeners;
170
172
  dispose: () => void | Promise<void>;
171
173
  call: (params: any) => Promise<any>;
174
+ /**
175
+ * Bind a protocol that has already been verified for one transport endpoint.
176
+ * Every later SDK call for the same connectId uses it as a strict expectation.
177
+ */
178
+ setDeviceConnectProtocol: (
179
+ connectId: string,
180
+ connectProtocol: HardwareConnectProtocol | undefined
181
+ ) => void;
172
182
  uiResponse: typeof uiResponse;
173
183
  cancel: (connectId?: string) => void;
174
184
  updateSettings: typeof updateSettings;
@@ -196,6 +206,7 @@ export type CoreApi = {
196
206
  * Device function
197
207
  */
198
208
  searchDevices: typeof searchDevices;
209
+ detectDeviceConnectProtocol: typeof detectDeviceConnectProtocol;
199
210
  promptWebDeviceAccess: typeof promptWebDeviceAccess;
200
211
  getFeatures: typeof getFeatures;
201
212
  getDeviceState: typeof getDeviceState;
@@ -61,9 +61,15 @@ export interface CommonParams {
61
61
 
62
62
  /**
63
63
  * Strictly expected transport protocol. The SDK actively verifies this value and
64
- * rejects a mismatch. If omitted, a cached protocol only influences probe order.
64
+ * rejects a mismatch. After a successful probe, the cached protocol is also strict.
65
65
  */
66
66
  connectProtocol?: HardwareConnectProtocol;
67
+
68
+ /**
69
+ * Ignore a previously bound protocol for this call and actively detect the
70
+ * protocol again. Intended for the first verified connection or explicit recovery.
71
+ */
72
+ forceProtocolDetection?: boolean;
67
73
  }
68
74
 
69
75
  export type Params<T> = CommonParams & T & { bundle?: undefined };