@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.
- package/__tests__/check-all-firmware-release-protocol-v2.test.ts +110 -15
- package/__tests__/device-lifecycle-events.test.ts +105 -3
- package/__tests__/method-protocol-support.test.ts +19 -0
- package/__tests__/protocol-binding.test.ts +89 -0
- package/__tests__/protocol-v2-resources.test.ts +87 -42
- package/__tests__/protocol-v2.test.ts +347 -40
- package/__tests__/search-devices.test.ts +12 -3
- package/dist/api/CheckAllFirmwareRelease.d.ts.map +1 -1
- package/dist/api/DetectDeviceConnectProtocol.d.ts +7 -0
- package/dist/api/DetectDeviceConnectProtocol.d.ts.map +1 -0
- package/dist/api/FirmwareUpdate.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV2.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV3.d.ts.map +1 -1
- package/dist/api/FirmwareUpdateV4.d.ts +1 -0
- package/dist/api/FirmwareUpdateV4.d.ts.map +1 -1
- package/dist/api/SearchDevices.d.ts.map +1 -1
- package/dist/api/firmware/FirmwareUpdateBaseMethod.d.ts.map +1 -1
- package/dist/api/firmware/uploadFirmware.d.ts.map +1 -1
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/device/Device.d.ts +5 -1
- package/dist/device/Device.d.ts.map +1 -1
- package/dist/device/DevicePool.d.ts.map +1 -1
- package/dist/index.d.ts +20 -3
- package/dist/index.js +361 -203
- package/dist/inject.d.ts +6 -1
- package/dist/inject.d.ts.map +1 -1
- package/dist/lowLevelInject.d.ts.map +1 -1
- package/dist/protocols/protocol-v2/resources.d.ts +4 -4
- package/dist/protocols/protocol-v2/resources.d.ts.map +1 -1
- package/dist/topLevelInject.d.ts.map +1 -1
- package/dist/types/api/detectDeviceConnectProtocol.d.ts +4 -0
- package/dist/types/api/detectDeviceConnectProtocol.d.ts.map +1 -0
- package/dist/types/api/index.d.ts +4 -0
- package/dist/types/api/index.d.ts.map +1 -1
- package/dist/types/params.d.ts +1 -0
- package/dist/types/params.d.ts.map +1 -1
- package/dist/utils/patch.d.ts +1 -1
- package/dist/utils/patch.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/api/CheckAllFirmwareRelease.ts +10 -9
- package/src/api/DetectDeviceConnectProtocol.ts +18 -0
- package/src/api/FirmwareUpdate.ts +6 -2
- package/src/api/FirmwareUpdateV2.ts +5 -2
- package/src/api/FirmwareUpdateV3.ts +6 -2
- package/src/api/FirmwareUpdateV4.ts +73 -43
- package/src/api/SearchDevices.ts +3 -1
- package/src/api/firmware/FirmwareUpdateBaseMethod.ts +15 -4
- package/src/api/firmware/uploadFirmware.ts +17 -4
- package/src/api/index.ts +1 -0
- package/src/core/index.ts +11 -2
- package/src/data/messages/messages-protocol-v2.json +0 -43
- package/src/device/Device.ts +53 -15
- package/src/device/DevicePool.ts +7 -2
- package/src/inject.ts +57 -2
- package/src/lowLevelInject.ts +6 -3
- package/src/protocols/protocol-v2/resources.ts +150 -56
- package/src/topLevelInject.ts +6 -3
- package/src/types/api/detectDeviceConnectProtocol.ts +7 -0
- package/src/types/api/index.ts +11 -0
- package/src/types/params.ts +7 -1
package/src/device/Device.ts
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
-
|
|
434
|
-
|
|
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
|
-
|
|
444
|
-
|
|
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
|
-
|
|
1353
|
-
|
|
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);
|
package/src/device/DevicePool.ts
CHANGED
|
@@ -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
|
-
{
|
|
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
|
* 获取设备信息
|
package/src/lowLevelInject.ts
CHANGED
|
@@ -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
|
-
|
|
65
|
-
|
|
66
|
-
value
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
74
|
-
if (
|
|
75
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
|
109
|
-
|
|
110
|
-
'
|
|
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
|
-
|
|
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
|
-
/**
|
|
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 (
|
|
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
|
|
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]));
|
package/src/topLevelInject.ts
CHANGED
|
@@ -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
|
-
|
|
43
|
+
setDeviceConnectProtocol: protocolAwareCall.setDeviceConnectProtocol,
|
|
44
|
+
|
|
45
|
+
...createCoreApi(protocolAwareCall.call),
|
|
43
46
|
|
|
44
47
|
removeAllListeners: type => {
|
|
45
48
|
eventEmitter.removeAllListeners(type);
|
package/src/types/api/index.ts
CHANGED
|
@@ -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;
|
package/src/types/params.ts
CHANGED
|
@@ -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.
|
|
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 };
|