@onekeyfe/hardware-cli 1.2.0-alpha.4 → 1.2.0-alpha.40
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/dist/cli.d.ts +86 -1
- package/dist/cli.js +364 -273
- package/dist/deviceSelection.d.ts +3 -0
- package/dist/deviceSelection.js +11 -0
- package/dist/deviceStateCommands.d.ts +19 -0
- package/dist/deviceStateCommands.js +62 -0
- package/dist/pinentry.d.ts +1 -0
- package/dist/sdk.d.ts +10 -2
- package/dist/sdk.js +17 -8
- package/dist/session.d.ts +2 -9
- package/dist/session.js +3 -22
- package/dist/transports/nobleBlePlugin.js +113 -70
- package/package.json +7 -6
- package/src/__tests__/cli-version.test.ts +8 -0
- package/src/__tests__/device-selection.test.ts +29 -0
- package/src/__tests__/device-state-commands.test.ts +118 -0
- package/src/__tests__/firmware-update-legacy-command.test.ts +18 -0
- package/src/__tests__/firmware-update-v4-command.test.ts +100 -0
- package/src/__tests__/noble-ble-plugin.test.ts +370 -0
- package/src/__tests__/wallet-session.test.ts +64 -0
- package/src/__tests__/wallpaper-upload-command.test.ts +46 -0
- package/src/cli.ts +459 -321
- package/src/deviceSelection.ts +13 -0
- package/src/deviceStateCommands.ts +71 -0
- package/src/pinentry.ts +1 -0
- package/src/sdk.ts +18 -10
- package/src/session.ts +2 -24
- package/src/transports/nobleBlePlugin.ts +148 -76
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.selectSearchDevice = void 0;
|
|
4
|
+
function selectSearchDevice(devices, preferredConnectId) {
|
|
5
|
+
if (preferredConnectId) {
|
|
6
|
+
return (devices.find(device => device.connectId === preferredConnectId) ??
|
|
7
|
+
{ connectId: preferredConnectId });
|
|
8
|
+
}
|
|
9
|
+
return devices[0];
|
|
10
|
+
}
|
|
11
|
+
exports.selectSearchDevice = selectSearchDevice;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CoreApi, DeviceStateScope } from '@onekeyfe/hd-core';
|
|
2
|
+
type DeviceStateSdk = Pick<CoreApi, 'searchDevices' | 'getDeviceState' | 'getFeatures'>;
|
|
3
|
+
/**
|
|
4
|
+
* Unified state entry for the new CLI. Resolve the user-facing connectId through
|
|
5
|
+
* discovery so V1 serial IDs map to the process-local USB path.
|
|
6
|
+
*/
|
|
7
|
+
export declare const getCanonicalDeviceState: (sdk: DeviceStateSdk, connectId: string | undefined, scope: DeviceStateScope) => Promise<import("@onekeyfe/hd-core").Unsuccessful | {
|
|
8
|
+
success: true;
|
|
9
|
+
payload: import("@onekeyfe/hd-core").DeviceState;
|
|
10
|
+
}>;
|
|
11
|
+
/**
|
|
12
|
+
* Legacy CLI only: retain getFeatures for V1 and reuse the discovery projection for V2.
|
|
13
|
+
* Public SDK.getFeatures remains V1-only.
|
|
14
|
+
*/
|
|
15
|
+
export declare const getCompatibleFeatures: (sdk: DeviceStateSdk, connectId?: string) => Promise<import("@onekeyfe/hd-core").Unsuccessful | {
|
|
16
|
+
success: true;
|
|
17
|
+
payload: import("@onekeyfe/hd-core").Features;
|
|
18
|
+
}>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getCompatibleFeatures = exports.getCanonicalDeviceState = void 0;
|
|
4
|
+
const createDeviceNotFoundResult = (connectId) => ({
|
|
5
|
+
success: false,
|
|
6
|
+
payload: {
|
|
7
|
+
code: 'DEVICE_NOT_FOUND',
|
|
8
|
+
error: connectId ? `Device not found: ${connectId}` : 'No device found',
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
const resolveSearchDevice = async (sdk, connectId) => {
|
|
12
|
+
const searchResult = await sdk.searchDevices();
|
|
13
|
+
if (!searchResult.success)
|
|
14
|
+
return searchResult;
|
|
15
|
+
const devices = searchResult.payload;
|
|
16
|
+
const device = connectId ? devices.find(item => item.connectId === connectId) : devices[0];
|
|
17
|
+
if (!device?.connectId)
|
|
18
|
+
return createDeviceNotFoundResult(connectId);
|
|
19
|
+
return { success: true, payload: device };
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Unified state entry for the new CLI. Resolve the user-facing connectId through
|
|
23
|
+
* discovery so V1 serial IDs map to the process-local USB path.
|
|
24
|
+
*/
|
|
25
|
+
const getCanonicalDeviceState = async (sdk, connectId, scope) => {
|
|
26
|
+
const deviceResult = await resolveSearchDevice(sdk, connectId);
|
|
27
|
+
if (!deviceResult.success)
|
|
28
|
+
return deviceResult;
|
|
29
|
+
if (scope === 'runtime' && deviceResult.payload.state) {
|
|
30
|
+
return { success: true, payload: deviceResult.payload.state };
|
|
31
|
+
}
|
|
32
|
+
const resolvedConnectId = deviceResult.payload.connectId ?? undefined;
|
|
33
|
+
if (!resolvedConnectId)
|
|
34
|
+
return createDeviceNotFoundResult();
|
|
35
|
+
return sdk.getDeviceState(resolvedConnectId, { scope });
|
|
36
|
+
};
|
|
37
|
+
exports.getCanonicalDeviceState = getCanonicalDeviceState;
|
|
38
|
+
/**
|
|
39
|
+
* Legacy CLI only: retain getFeatures for V1 and reuse the discovery projection for V2.
|
|
40
|
+
* Public SDK.getFeatures remains V1-only.
|
|
41
|
+
*/
|
|
42
|
+
const getCompatibleFeatures = async (sdk, connectId) => {
|
|
43
|
+
const deviceResult = await resolveSearchDevice(sdk, connectId);
|
|
44
|
+
if (!deviceResult.success)
|
|
45
|
+
return deviceResult;
|
|
46
|
+
const device = deviceResult.payload;
|
|
47
|
+
const protocol = device.state?.protocol ?? device.features?.protocol;
|
|
48
|
+
if (protocol === 'V2') {
|
|
49
|
+
if (!device.features) {
|
|
50
|
+
return {
|
|
51
|
+
success: false,
|
|
52
|
+
payload: {
|
|
53
|
+
code: 'DEVICE_FEATURES_UNAVAILABLE',
|
|
54
|
+
error: 'Protocol V2 compatibility features are unavailable',
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return { success: true, payload: device.features };
|
|
59
|
+
}
|
|
60
|
+
return sdk.getFeatures(device.connectId ?? '');
|
|
61
|
+
};
|
|
62
|
+
exports.getCompatibleFeatures = getCompatibleFeatures;
|
package/dist/pinentry.d.ts
CHANGED
package/dist/sdk.d.ts
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* Passphrase flow aligns with app-monorepo CLI:
|
|
6
6
|
* - Standard wallet: --use-empty-passphrase, auto-respond
|
|
7
7
|
* - Hidden wallet: interactive 1/2/3 selection (standard / pinentry / on-device)
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
8
|
+
* - Legacy session caching: existing OS keychain entries may still be preloaded,
|
|
9
|
+
* but public SDK responses never expose new device session ids
|
|
10
10
|
*/
|
|
11
11
|
import HardwareSDK from '@onekeyfe/hd-common-connect-sdk';
|
|
12
|
+
import type { PinentryResult } from './pinentry';
|
|
12
13
|
export interface SDKOptions {
|
|
13
14
|
connectId?: string;
|
|
14
15
|
passphraseState?: string;
|
|
@@ -16,6 +17,13 @@ export interface SDKOptions {
|
|
|
16
17
|
debug?: boolean;
|
|
17
18
|
transport?: 'usb' | 'ble';
|
|
18
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Prompt user to select wallet type (aligns with app-monorepo flow):
|
|
22
|
+
* 1. Standard wallet (no passphrase)
|
|
23
|
+
* 2. Hidden wallet — enter passphrase via pinentry (secure OS dialog)
|
|
24
|
+
* 3. Hidden wallet — enter passphrase on device screen
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolvePassphraseByChoice(choice: '1' | '2' | '3' | '4'): Promise<PinentryResult>;
|
|
19
27
|
export declare function createSDK(opts: SDKOptions): Promise<typeof HardwareSDK>;
|
|
20
28
|
/**
|
|
21
29
|
* Release the SDK and clear the singleton. Must be called before the CLI
|
package/dist/sdk.js
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* Passphrase flow aligns with app-monorepo CLI:
|
|
7
7
|
* - Standard wallet: --use-empty-passphrase, auto-respond
|
|
8
8
|
* - Hidden wallet: interactive 1/2/3 selection (standard / pinentry / on-device)
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
9
|
+
* - Legacy session caching: existing OS keychain entries may still be preloaded,
|
|
10
|
+
* but public SDK responses never expose new device session ids
|
|
11
11
|
*/
|
|
12
12
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
13
|
if (k2 === undefined) k2 = k;
|
|
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.disposeSDK = exports.createSDK = void 0;
|
|
39
|
+
exports.disposeSDK = exports.createSDK = exports.resolvePassphraseByChoice = void 0;
|
|
40
40
|
const readline = __importStar(require("node:readline"));
|
|
41
41
|
const hd_common_connect_sdk_1 = __importDefault(require("@onekeyfe/hd-common-connect-sdk"));
|
|
42
42
|
const hd_core_1 = require("@onekeyfe/hd-core");
|
|
@@ -76,8 +76,15 @@ function resolvePassphraseByChoice(choice) {
|
|
|
76
76
|
return Promise.resolve({ value: '', passphraseOnDevice: false });
|
|
77
77
|
if (choice === '2')
|
|
78
78
|
return (0, pinentry_1.promptPassphraseViaPinentry)();
|
|
79
|
+
if (choice === '4')
|
|
80
|
+
return Promise.resolve({
|
|
81
|
+
value: '',
|
|
82
|
+
passphraseOnDevice: false,
|
|
83
|
+
attachPinOnDevice: true,
|
|
84
|
+
});
|
|
79
85
|
return Promise.resolve({ value: '', passphraseOnDevice: true });
|
|
80
86
|
}
|
|
87
|
+
exports.resolvePassphraseByChoice = resolvePassphraseByChoice;
|
|
81
88
|
function promptPassphraseMode() {
|
|
82
89
|
if (!process.stdin.isTTY) {
|
|
83
90
|
return Promise.resolve({ value: '', passphraseOnDevice: true });
|
|
@@ -94,16 +101,17 @@ function promptPassphraseMode() {
|
|
|
94
101
|
' 1. Standard wallet (no passphrase)',
|
|
95
102
|
' 2. Hidden wallet — enter passphrase on this computer (pinentry)',
|
|
96
103
|
' 3. Hidden wallet — enter passphrase on device screen',
|
|
104
|
+
' 4. Attach PIN wallet — enter Attach PIN on device screen',
|
|
97
105
|
'',
|
|
98
106
|
].join('\n'));
|
|
99
107
|
rl.question('Enter selection [1/2/3]: ', answer => {
|
|
100
108
|
const n = answer.trim();
|
|
101
|
-
if (n === '1' || n === '2' || n === '3') {
|
|
109
|
+
if (n === '1' || n === '2' || n === '3' || n === '4') {
|
|
102
110
|
rl.close();
|
|
103
111
|
resolvePassphraseByChoice(n).then(resolve);
|
|
104
112
|
return;
|
|
105
113
|
}
|
|
106
|
-
process.stderr.write('Invalid selection. Enter 1, 2, or
|
|
114
|
+
process.stderr.write('Invalid selection. Enter 1, 2, 3, or 4.\n');
|
|
107
115
|
prompt();
|
|
108
116
|
});
|
|
109
117
|
};
|
|
@@ -147,6 +155,7 @@ function registerEventHandlers(sdk) {
|
|
|
147
155
|
payload: {
|
|
148
156
|
value: result.value,
|
|
149
157
|
passphraseOnDevice: result.passphraseOnDevice,
|
|
158
|
+
attachPinOnDevice: result.attachPinOnDevice,
|
|
150
159
|
save: false,
|
|
151
160
|
},
|
|
152
161
|
});
|
|
@@ -168,12 +177,12 @@ function registerEventHandlers(sdk) {
|
|
|
168
177
|
process.stderr.write('[onekey-hw] Please confirm the action on your device...\n');
|
|
169
178
|
}
|
|
170
179
|
});
|
|
171
|
-
sdk.on(hd_core_1.DEVICE.CONNECT, (device) => {
|
|
180
|
+
sdk.on(hd_core_1.DEVICE.CONNECT, ({ device }) => {
|
|
172
181
|
const name = device?.label || device?.name;
|
|
173
182
|
if (name)
|
|
174
183
|
process.stderr.write(`[onekey-hw] Device connected: ${name}\n`);
|
|
175
184
|
});
|
|
176
|
-
sdk.on(hd_core_1.DEVICE.DISCONNECT, (device) => {
|
|
185
|
+
sdk.on(hd_core_1.DEVICE.DISCONNECT, ({ device }) => {
|
|
177
186
|
const name = device?.label || device?.name;
|
|
178
187
|
if (name)
|
|
179
188
|
process.stderr.write(`[onekey-hw] Device disconnected: ${name}\n`);
|
|
@@ -222,7 +231,7 @@ async function disposeSDK() {
|
|
|
222
231
|
return;
|
|
223
232
|
try {
|
|
224
233
|
const sdk = await sdkReadyPromise;
|
|
225
|
-
sdk.dispose();
|
|
234
|
+
await Promise.resolve(sdk.dispose());
|
|
226
235
|
}
|
|
227
236
|
catch {
|
|
228
237
|
// ignore errors during cleanup
|
package/dist/session.d.ts
CHANGED
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Passphrase session management for hd-cli.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Command: keychain → preloadSessionCache → SDK call (no passphrase prompt)
|
|
7
|
-
* Stale: error 112 → clear keychain → re-prompt → retry
|
|
8
|
-
* Logout: keychain delete
|
|
4
|
+
* Existing keychain entries remain readable for compatibility, but the public
|
|
5
|
+
* SDK no longer exposes new device session ids for persistence.
|
|
9
6
|
*/
|
|
10
7
|
/**
|
|
11
8
|
* Load passphraseState + sessionId from keychain and call preloadSessionCache.
|
|
12
9
|
* Non-fatal: returns the loaded passphraseState or undefined.
|
|
13
10
|
*/
|
|
14
11
|
export declare function preloadSessionFromKeychain(deviceId: string): Promise<string | undefined>;
|
|
15
|
-
/**
|
|
16
|
-
* Save passphraseState + sessionId to keychain for next CLI invocation.
|
|
17
|
-
*/
|
|
18
|
-
export declare function saveSessionToKeychain(deviceId: string, passphraseState: string, sessionId: string): Promise<void>;
|
|
19
12
|
/**
|
|
20
13
|
* Clear cached session from keychain.
|
|
21
14
|
*/
|
package/dist/session.js
CHANGED
|
@@ -2,14 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Passphrase session management for hd-cli.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Command: keychain → preloadSessionCache → SDK call (no passphrase prompt)
|
|
8
|
-
* Stale: error 112 → clear keychain → re-prompt → retry
|
|
9
|
-
* Logout: keychain delete
|
|
5
|
+
* Existing keychain entries remain readable for compatibility, but the public
|
|
6
|
+
* SDK no longer exposes new device session ids for persistence.
|
|
10
7
|
*/
|
|
11
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.clearSessionFromKeychain = exports.
|
|
9
|
+
exports.clearSessionFromKeychain = exports.preloadSessionFromKeychain = void 0;
|
|
13
10
|
const hd_core_1 = require("@onekeyfe/hd-core");
|
|
14
11
|
const storage_1 = require("./storage");
|
|
15
12
|
// Keychain key format: scoped by deviceId for multi-device support
|
|
@@ -50,22 +47,6 @@ async function preloadSessionFromKeychain(deviceId) {
|
|
|
50
47
|
return undefined;
|
|
51
48
|
}
|
|
52
49
|
exports.preloadSessionFromKeychain = preloadSessionFromKeychain;
|
|
53
|
-
/**
|
|
54
|
-
* Save passphraseState + sessionId to keychain for next CLI invocation.
|
|
55
|
-
*/
|
|
56
|
-
async function saveSessionToKeychain(deviceId, passphraseState, sessionId) {
|
|
57
|
-
try {
|
|
58
|
-
const storage = getStorage();
|
|
59
|
-
await Promise.all([
|
|
60
|
-
storage.set(psKey(deviceId), Buffer.from(passphraseState, 'utf-8')),
|
|
61
|
-
storage.set(sidKey(deviceId), Buffer.from(sessionId, 'utf-8')),
|
|
62
|
-
]);
|
|
63
|
-
}
|
|
64
|
-
catch {
|
|
65
|
-
// Non-fatal — session still works in-memory for this invocation
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
exports.saveSessionToKeychain = saveSessionToKeychain;
|
|
69
50
|
/**
|
|
70
51
|
* Clear cached session from keychain.
|
|
71
52
|
*/
|
|
@@ -3,54 +3,89 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.createNobleBlePlugin = void 0;
|
|
4
4
|
const hd_shared_1 = require("@onekeyfe/hd-shared");
|
|
5
5
|
const ONEKEY_SERVICE_UUIDS = [hd_shared_1.ONEKEY_SERVICE_UUID];
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const NORMALIZED_ONEKEY_SERVICE_UUIDS = new Set([
|
|
10
|
-
...ONEKEY_SERVICE_UUIDS.map(uuid => getBleUuidKey(uuid)),
|
|
11
|
-
'0001',
|
|
12
|
-
]);
|
|
6
|
+
const ONEKEY_SERVICE_UUID_ALIASES = (0, hd_shared_1.createKnownBleUuidAliases)(hd_shared_1.ONEKEY_SERVICE_UUID);
|
|
7
|
+
const ONEKEY_WRITE_UUID_ALIASES = (0, hd_shared_1.createKnownBleUuidAliases)(hd_shared_1.ONEKEY_WRITE_CHARACTERISTIC_UUID);
|
|
8
|
+
const ONEKEY_NOTIFY_UUID_ALIASES = (0, hd_shared_1.createKnownBleUuidAliases)(hd_shared_1.ONEKEY_NOTIFY_CHARACTERISTIC_UUID);
|
|
13
9
|
const BLUETOOTH_INIT_TIMEOUT = 10000;
|
|
14
10
|
const DEVICE_SCAN_TIMEOUT = 8000;
|
|
15
11
|
const CONNECTION_TIMEOUT = 8000;
|
|
16
12
|
const SERVICE_DISCOVERY_TIMEOUT = 10000;
|
|
13
|
+
const BLE_CLEANUP_TIMEOUT = 100;
|
|
17
14
|
const BLE_PACKET_SIZE = 192;
|
|
18
|
-
const BLE_WRITE_DELAY = 5;
|
|
19
15
|
const BLE_ENCRYPTION_ERROR_PATTERNS = [/encryption is insufficient/i, /insufficient encryption/i];
|
|
20
16
|
let noble = null;
|
|
21
17
|
let nobleReadyPromise = null;
|
|
22
18
|
const discoveredDevices = new Map();
|
|
23
19
|
const connectedDevices = new Map();
|
|
24
20
|
const deviceCharacteristics = new Map();
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
function getBleUuidKey(uuid) {
|
|
28
|
-
const normalized = (uuid ?? '').replace(/-/g, '').toLowerCase();
|
|
29
|
-
return normalized.length >= 8 ? normalized.substring(4, 8) : normalized;
|
|
30
|
-
}
|
|
31
|
-
function isGenericBleService(uuid) {
|
|
32
|
-
return ['1800', '1801', '180a', '180f'].includes(getBleUuidKey(uuid));
|
|
33
|
-
}
|
|
34
|
-
function hasOneKeyAdvertisementService(peripheral) {
|
|
35
|
-
const serviceUuids = peripheral.advertisement?.serviceUuids ?? [];
|
|
36
|
-
return serviceUuids.some(uuid => {
|
|
37
|
-
const uuidKey = getBleUuidKey(uuid);
|
|
38
|
-
return (NORMALIZED_ONEKEY_SERVICE_UUIDS.has(uuidKey) ||
|
|
39
|
-
PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(uuidKey));
|
|
40
|
-
});
|
|
41
|
-
}
|
|
21
|
+
const notificationStates = new Map();
|
|
22
|
+
const notificationGenerations = new Map();
|
|
42
23
|
function isOneKeyPeripheral(peripheral) {
|
|
43
|
-
const
|
|
44
|
-
return (0, hd_shared_1.
|
|
24
|
+
const serviceUuids = peripheral.advertisement?.serviceUuids;
|
|
25
|
+
return ((0, hd_shared_1.hasOnekeyCommunicationService)(serviceUuids) &&
|
|
26
|
+
(0, hd_shared_1.isOnekeyBluetoothDevice)({
|
|
27
|
+
id: peripheral.id,
|
|
28
|
+
localName: peripheral.advertisement?.localName,
|
|
29
|
+
serviceUuids,
|
|
30
|
+
}));
|
|
45
31
|
}
|
|
46
|
-
function enqueueNotification(data) {
|
|
32
|
+
function enqueueNotification(deviceId, generation, data) {
|
|
33
|
+
const state = notificationStates.get(deviceId);
|
|
34
|
+
if (!state || state.generation !== generation)
|
|
35
|
+
return;
|
|
47
36
|
const hex = data.toString('hex');
|
|
48
|
-
const receiver = pendingReceivers
|
|
37
|
+
const [receiver] = state.pendingReceivers;
|
|
49
38
|
if (receiver) {
|
|
50
|
-
receiver
|
|
39
|
+
state.pendingReceivers.delete(receiver);
|
|
40
|
+
receiver.resolve(hex);
|
|
51
41
|
return;
|
|
52
42
|
}
|
|
53
|
-
|
|
43
|
+
state.queue.push(hex);
|
|
44
|
+
}
|
|
45
|
+
function createNotificationState(deviceId) {
|
|
46
|
+
const existing = notificationStates.get(deviceId);
|
|
47
|
+
if (existing) {
|
|
48
|
+
const error = new Error(`BLE notification state replaced for ${deviceId}`);
|
|
49
|
+
existing.pendingReceivers.forEach(receiver => receiver.reject(error));
|
|
50
|
+
}
|
|
51
|
+
const generation = (notificationGenerations.get(deviceId) ?? 0) + 1;
|
|
52
|
+
notificationGenerations.set(deviceId, generation);
|
|
53
|
+
const state = {
|
|
54
|
+
generation,
|
|
55
|
+
queue: [],
|
|
56
|
+
pendingReceivers: new Set(),
|
|
57
|
+
};
|
|
58
|
+
notificationStates.set(deviceId, state);
|
|
59
|
+
return state;
|
|
60
|
+
}
|
|
61
|
+
function clearNotificationState(deviceId, reason) {
|
|
62
|
+
const state = notificationStates.get(deviceId);
|
|
63
|
+
if (!state)
|
|
64
|
+
return;
|
|
65
|
+
notificationStates.delete(deviceId);
|
|
66
|
+
const error = new Error(reason);
|
|
67
|
+
state.pendingReceivers.forEach(receiver => receiver.reject(error));
|
|
68
|
+
state.pendingReceivers.clear();
|
|
69
|
+
state.queue.length = 0;
|
|
70
|
+
}
|
|
71
|
+
function waitForNobleCleanup(registerCallback) {
|
|
72
|
+
return new Promise(resolve => {
|
|
73
|
+
let completed = false;
|
|
74
|
+
const complete = () => {
|
|
75
|
+
if (completed)
|
|
76
|
+
return;
|
|
77
|
+
completed = true;
|
|
78
|
+
clearTimeout(timeout);
|
|
79
|
+
resolve();
|
|
80
|
+
};
|
|
81
|
+
const timeout = setTimeout(complete, BLE_CLEANUP_TIMEOUT);
|
|
82
|
+
try {
|
|
83
|
+
registerCallback(complete);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
complete();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
54
89
|
}
|
|
55
90
|
async function initializeNoble() {
|
|
56
91
|
if (!noble) {
|
|
@@ -122,7 +157,7 @@ async function scanDevices(targetDeviceId) {
|
|
|
122
157
|
const onDiscover = (peripheral) => {
|
|
123
158
|
if (targetDeviceId && peripheral.id !== targetDeviceId)
|
|
124
159
|
return;
|
|
125
|
-
if (!
|
|
160
|
+
if (!isOneKeyPeripheral(peripheral))
|
|
126
161
|
return;
|
|
127
162
|
discoveredDevices.set(peripheral.id, peripheral);
|
|
128
163
|
found.set(peripheral.id, peripheral);
|
|
@@ -171,13 +206,7 @@ async function discoverCharacteristics(peripheral) {
|
|
|
171
206
|
resolve(discoveredServices);
|
|
172
207
|
});
|
|
173
208
|
});
|
|
174
|
-
|
|
175
|
-
if (!service) {
|
|
176
|
-
service =
|
|
177
|
-
services.find(s => PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(getBleUuidKey(s.uuid))) ||
|
|
178
|
-
services.find(s => !isGenericBleService(s.uuid)) ||
|
|
179
|
-
services[0];
|
|
180
|
-
}
|
|
209
|
+
const service = services.find(s => (0, hd_shared_1.matchesKnownBleUuid)(s.uuid, ONEKEY_SERVICE_UUID_ALIASES));
|
|
181
210
|
if (!service) {
|
|
182
211
|
throw hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BleServiceNotFound, 'No BLE service found');
|
|
183
212
|
}
|
|
@@ -194,11 +223,10 @@ async function discoverCharacteristics(peripheral) {
|
|
|
194
223
|
let writeCharacteristic;
|
|
195
224
|
let notifyCharacteristic;
|
|
196
225
|
for (const characteristic of characteristics) {
|
|
197
|
-
|
|
198
|
-
if (uuidKey === NORMALIZED_WRITE_UUID) {
|
|
226
|
+
if ((0, hd_shared_1.matchesKnownBleUuid)(characteristic.uuid, ONEKEY_WRITE_UUID_ALIASES)) {
|
|
199
227
|
writeCharacteristic = characteristic;
|
|
200
228
|
}
|
|
201
|
-
else if (
|
|
229
|
+
else if ((0, hd_shared_1.matchesKnownBleUuid)(characteristic.uuid, ONEKEY_NOTIFY_UUID_ALIASES)) {
|
|
202
230
|
notifyCharacteristic = characteristic;
|
|
203
231
|
}
|
|
204
232
|
}
|
|
@@ -210,10 +238,8 @@ async function discoverCharacteristics(peripheral) {
|
|
|
210
238
|
notify: notifyCharacteristic,
|
|
211
239
|
};
|
|
212
240
|
}
|
|
213
|
-
function subscribeNotifications(deviceId, notifyCharacteristic) {
|
|
214
|
-
return
|
|
215
|
-
notifyCharacteristic.unsubscribe(() => resolve());
|
|
216
|
-
})
|
|
241
|
+
function subscribeNotifications(deviceId, generation, notifyCharacteristic) {
|
|
242
|
+
return waitForNobleCleanup(callback => notifyCharacteristic.unsubscribe(callback))
|
|
217
243
|
.then(() => new Promise((resolve, reject) => {
|
|
218
244
|
notifyCharacteristic.subscribe((error) => {
|
|
219
245
|
if (error) {
|
|
@@ -230,7 +256,7 @@ function subscribeNotifications(deviceId, notifyCharacteristic) {
|
|
|
230
256
|
}))
|
|
231
257
|
.then(() => {
|
|
232
258
|
notifyCharacteristic.removeAllListeners('data');
|
|
233
|
-
notifyCharacteristic.on('data', enqueueNotification);
|
|
259
|
+
notifyCharacteristic.on('data', data => enqueueNotification(deviceId, generation, data));
|
|
234
260
|
})
|
|
235
261
|
.catch(error => {
|
|
236
262
|
notifyCharacteristic.removeAllListeners('data');
|
|
@@ -240,9 +266,9 @@ function subscribeNotifications(deviceId, notifyCharacteristic) {
|
|
|
240
266
|
throw hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BleCharacteristicNotifyChangeFailure);
|
|
241
267
|
});
|
|
242
268
|
}
|
|
243
|
-
function writeCharacteristic(characteristic, buffer) {
|
|
269
|
+
function writeCharacteristic(characteristic, buffer, withoutResponse) {
|
|
244
270
|
return new Promise((resolve, reject) => {
|
|
245
|
-
characteristic.write(buffer,
|
|
271
|
+
characteristic.write(buffer, withoutResponse, (error) => {
|
|
246
272
|
if (error) {
|
|
247
273
|
reject(error);
|
|
248
274
|
return;
|
|
@@ -254,21 +280,16 @@ function writeCharacteristic(characteristic, buffer) {
|
|
|
254
280
|
async function disconnectDevice(uuid) {
|
|
255
281
|
const peripheral = connectedDevices.get(uuid);
|
|
256
282
|
const characteristics = deviceCharacteristics.get(uuid);
|
|
283
|
+
clearNotificationState(uuid, `BLE device disconnected: ${uuid}`);
|
|
257
284
|
if (characteristics) {
|
|
258
285
|
characteristics.notify.removeAllListeners('data');
|
|
259
|
-
await
|
|
260
|
-
characteristics.notify.unsubscribe(() => resolve());
|
|
261
|
-
});
|
|
286
|
+
await waitForNobleCleanup(callback => characteristics.notify.unsubscribe(callback));
|
|
262
287
|
}
|
|
263
288
|
connectedDevices.delete(uuid);
|
|
264
289
|
deviceCharacteristics.delete(uuid);
|
|
265
|
-
notificationQueue.length = 0;
|
|
266
|
-
pendingReceivers.splice(0).forEach(resolve => resolve(''));
|
|
267
290
|
if (!peripheral || peripheral.state === 'disconnected')
|
|
268
291
|
return;
|
|
269
|
-
await
|
|
270
|
-
peripheral.disconnect(() => resolve());
|
|
271
|
-
});
|
|
292
|
+
await waitForNobleCleanup(callback => peripheral.disconnect(callback));
|
|
272
293
|
}
|
|
273
294
|
function createNobleBlePlugin() {
|
|
274
295
|
return {
|
|
@@ -293,34 +314,56 @@ function createNobleBlePlugin() {
|
|
|
293
314
|
throw hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.DeviceNotFound, `BLE device not found: ${uuid}`);
|
|
294
315
|
}
|
|
295
316
|
await connectPeripheral(peripheral);
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
317
|
+
let characteristics;
|
|
318
|
+
try {
|
|
319
|
+
characteristics = await discoverCharacteristics(peripheral);
|
|
320
|
+
const notificationState = createNotificationState(uuid);
|
|
321
|
+
await subscribeNotifications(uuid, notificationState.generation, characteristics.notify);
|
|
322
|
+
connectedDevices.set(uuid, peripheral);
|
|
323
|
+
deviceCharacteristics.set(uuid, characteristics);
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
clearNotificationState(uuid, `BLE notification subscription failed: ${uuid}`);
|
|
327
|
+
if (characteristics) {
|
|
328
|
+
characteristics.notify.removeAllListeners('data');
|
|
329
|
+
await waitForNobleCleanup(callback => characteristics?.notify.unsubscribe(callback));
|
|
330
|
+
}
|
|
331
|
+
if (peripheral.state !== 'disconnected') {
|
|
332
|
+
await waitForNobleCleanup(callback => peripheral?.disconnect(callback));
|
|
333
|
+
}
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
300
336
|
},
|
|
301
337
|
async disconnect(uuid) {
|
|
302
338
|
await disconnectDevice(uuid);
|
|
303
339
|
},
|
|
304
|
-
async send(uuid, data) {
|
|
340
|
+
async send(uuid, data, options) {
|
|
305
341
|
const characteristics = deviceCharacteristics.get(uuid);
|
|
306
342
|
if (!characteristics) {
|
|
307
343
|
throw hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BleCharacteristicNotFound, `BLE device is not connected: ${uuid}`);
|
|
308
344
|
}
|
|
309
345
|
const buffer = Buffer.from(data, 'hex');
|
|
346
|
+
const withoutResponse = options?.withoutResponse ?? true;
|
|
310
347
|
for (let offset = 0; offset < buffer.length; offset += BLE_PACKET_SIZE) {
|
|
311
348
|
const chunk = buffer.subarray(offset, Math.min(offset + BLE_PACKET_SIZE, buffer.length));
|
|
312
|
-
await writeCharacteristic(characteristics.write, chunk);
|
|
313
|
-
if (offset + BLE_PACKET_SIZE < buffer.length) {
|
|
314
|
-
await (0, hd_shared_1.wait)(BLE_WRITE_DELAY);
|
|
315
|
-
}
|
|
349
|
+
await writeCharacteristic(characteristics.write, chunk, withoutResponse);
|
|
316
350
|
}
|
|
317
351
|
},
|
|
318
|
-
async receive() {
|
|
319
|
-
const
|
|
352
|
+
async receive(uuid) {
|
|
353
|
+
const resolvedUuid = uuid ??
|
|
354
|
+
(notificationStates.size === 1 ? notificationStates.keys().next().value : undefined);
|
|
355
|
+
if (!resolvedUuid) {
|
|
356
|
+
throw hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.RuntimeError, 'BLE receive requires a device UUID when multiple devices are connected');
|
|
357
|
+
}
|
|
358
|
+
const state = notificationStates.get(resolvedUuid);
|
|
359
|
+
if (!state) {
|
|
360
|
+
throw hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.TransportNotFound, `BLE notification state not found: ${resolvedUuid}`);
|
|
361
|
+
}
|
|
362
|
+
const queued = state.queue.shift();
|
|
320
363
|
if (queued !== undefined)
|
|
321
364
|
return queued;
|
|
322
|
-
return new Promise(resolve => {
|
|
323
|
-
pendingReceivers.
|
|
365
|
+
return new Promise((resolve, reject) => {
|
|
366
|
+
state.pendingReceivers.add({ resolve, reject });
|
|
324
367
|
});
|
|
325
368
|
},
|
|
326
369
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hardware-cli",
|
|
3
|
-
"version": "1.2.0-alpha.
|
|
3
|
+
"version": "1.2.0-alpha.40",
|
|
4
4
|
"description": "OneKey hardware wallet CLI for testing device communication",
|
|
5
5
|
"author": "OneKey",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"start": "node dist/cli.js",
|
|
24
24
|
"search": "node dist/cli.js search",
|
|
25
25
|
"get-features": "node dist/cli.js get-features",
|
|
26
|
+
"get-state": "node dist/cli.js get-state",
|
|
26
27
|
"get-address": "node dist/cli.js get-address",
|
|
27
28
|
"ping": "node dist/cli.js ping",
|
|
28
29
|
"lint": "eslint .",
|
|
@@ -30,12 +31,12 @@
|
|
|
30
31
|
"test": "jest"
|
|
31
32
|
},
|
|
32
33
|
"dependencies": {
|
|
33
|
-
"@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.
|
|
34
|
-
"@onekeyfe/hd-core": "1.2.0-alpha.
|
|
35
|
-
"@onekeyfe/hd-shared": "1.2.0-alpha.
|
|
36
|
-
"@onekeyfe/hd-transport-usb": "1.2.0-alpha.
|
|
34
|
+
"@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.40",
|
|
35
|
+
"@onekeyfe/hd-core": "1.2.0-alpha.40",
|
|
36
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.40",
|
|
37
|
+
"@onekeyfe/hd-transport-usb": "1.2.0-alpha.40",
|
|
37
38
|
"@stoprocent/noble": "2.3.16",
|
|
38
39
|
"commander": "^12.0.0"
|
|
39
40
|
},
|
|
40
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "caf94ecc5e90731ae788ac81b500a245a8497d69"
|
|
41
42
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { selectSearchDevice } from '../deviceSelection';
|
|
2
|
+
|
|
3
|
+
describe('selectSearchDevice', () => {
|
|
4
|
+
it('多设备环境优先选择显式 connectId 对应的设备', () => {
|
|
5
|
+
const devices = [
|
|
6
|
+
{ connectId: 'first-device', name: 'Pro A9CA' },
|
|
7
|
+
{ connectId: 'target-device', name: 'Pro2 6136' },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
expect(selectSearchDevice(devices, 'target-device')).toEqual(devices[1]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('未指定 connectId 时保持选择第一台设备的兼容行为', () => {
|
|
14
|
+
const devices = [
|
|
15
|
+
{ connectId: 'first-device', name: 'Pro2 6136' },
|
|
16
|
+
{ connectId: 'second-device', name: 'Pro2 C445' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
expect(selectSearchDevice(devices)).toEqual(devices[0]);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('显式 connectId 暂未出现在扫描结果时仍保留该目标', () => {
|
|
23
|
+
const devices = [{ connectId: 'other-device', name: 'Pro2 C445' }];
|
|
24
|
+
|
|
25
|
+
expect(selectSearchDevice(devices, 'target-device')).toEqual({
|
|
26
|
+
connectId: 'target-device',
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
});
|