@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,13 @@
|
|
|
1
|
+
export function selectSearchDevice<T extends { connectId?: string | null }>(
|
|
2
|
+
devices: T[],
|
|
3
|
+
preferredConnectId?: string
|
|
4
|
+
): T | undefined {
|
|
5
|
+
if (preferredConnectId) {
|
|
6
|
+
return (
|
|
7
|
+
devices.find(device => device.connectId === preferredConnectId) ??
|
|
8
|
+
({ connectId: preferredConnectId } as T)
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return devices[0];
|
|
13
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { CoreApi, DeviceStateScope, KnownDevice, SearchDevice } from '@onekeyfe/hd-core';
|
|
2
|
+
|
|
3
|
+
type DeviceStateSdk = Pick<CoreApi, 'searchDevices' | 'getDeviceState' | 'getFeatures'>;
|
|
4
|
+
|
|
5
|
+
type DiscoveredDevice = SearchDevice & Partial<Pick<KnownDevice, 'features' | 'state'>>;
|
|
6
|
+
|
|
7
|
+
const createDeviceNotFoundResult = (connectId?: string) => ({
|
|
8
|
+
success: false as const,
|
|
9
|
+
payload: {
|
|
10
|
+
code: 'DEVICE_NOT_FOUND',
|
|
11
|
+
error: connectId ? `Device not found: ${connectId}` : 'No device found',
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const resolveSearchDevice = async (sdk: DeviceStateSdk, connectId?: string) => {
|
|
16
|
+
const searchResult = await sdk.searchDevices();
|
|
17
|
+
if (!searchResult.success) return searchResult;
|
|
18
|
+
|
|
19
|
+
const devices = searchResult.payload as DiscoveredDevice[];
|
|
20
|
+
const device = connectId ? devices.find(item => item.connectId === connectId) : devices[0];
|
|
21
|
+
|
|
22
|
+
if (!device?.connectId) return createDeviceNotFoundResult(connectId);
|
|
23
|
+
return { success: true as const, payload: device };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Unified state entry for the new CLI. Resolve the user-facing connectId through
|
|
28
|
+
* discovery so V1 serial IDs map to the process-local USB path.
|
|
29
|
+
*/
|
|
30
|
+
export const getCanonicalDeviceState = async (
|
|
31
|
+
sdk: DeviceStateSdk,
|
|
32
|
+
connectId: string | undefined,
|
|
33
|
+
scope: DeviceStateScope
|
|
34
|
+
) => {
|
|
35
|
+
const deviceResult = await resolveSearchDevice(sdk, connectId);
|
|
36
|
+
if (!deviceResult.success) return deviceResult;
|
|
37
|
+
|
|
38
|
+
if (scope === 'runtime' && deviceResult.payload.state) {
|
|
39
|
+
return { success: true as const, payload: deviceResult.payload.state };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const resolvedConnectId = deviceResult.payload.connectId ?? undefined;
|
|
43
|
+
if (!resolvedConnectId) return createDeviceNotFoundResult();
|
|
44
|
+
return sdk.getDeviceState(resolvedConnectId, { scope });
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Legacy CLI only: retain getFeatures for V1 and reuse the discovery projection for V2.
|
|
49
|
+
* Public SDK.getFeatures remains V1-only.
|
|
50
|
+
*/
|
|
51
|
+
export const getCompatibleFeatures = async (sdk: DeviceStateSdk, connectId?: string) => {
|
|
52
|
+
const deviceResult = await resolveSearchDevice(sdk, connectId);
|
|
53
|
+
if (!deviceResult.success) return deviceResult;
|
|
54
|
+
|
|
55
|
+
const device = deviceResult.payload;
|
|
56
|
+
const protocol = device.state?.protocol ?? device.features?.protocol;
|
|
57
|
+
if (protocol === 'V2') {
|
|
58
|
+
if (!device.features) {
|
|
59
|
+
return {
|
|
60
|
+
success: false as const,
|
|
61
|
+
payload: {
|
|
62
|
+
code: 'DEVICE_FEATURES_UNAVAILABLE',
|
|
63
|
+
error: 'Protocol V2 compatibility features are unavailable',
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return { success: true as const, payload: device.features };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return sdk.getFeatures(device.connectId ?? '');
|
|
71
|
+
};
|
package/src/pinentry.ts
CHANGED
package/src/sdk.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
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
|
|
|
12
12
|
import * as readline from 'node:readline';
|
|
@@ -16,7 +16,7 @@ import { DEVICE, UI_EVENT, UI_REQUEST, UI_RESPONSE } from '@onekeyfe/hd-core';
|
|
|
16
16
|
import { promptPassphraseViaPinentry } from './pinentry';
|
|
17
17
|
import { createNobleBlePlugin } from './transports/nobleBlePlugin';
|
|
18
18
|
|
|
19
|
-
import type { ConnectSettings } from '@onekeyfe/hd-core';
|
|
19
|
+
import type { ConnectSettings, KnownDevice } from '@onekeyfe/hd-core';
|
|
20
20
|
import type { PinentryResult } from './pinentry';
|
|
21
21
|
|
|
22
22
|
export interface SDKOptions {
|
|
@@ -59,9 +59,15 @@ let sdkReadyPromise: Promise<typeof HardwareSDK> | null = null;
|
|
|
59
59
|
* 2. Hidden wallet — enter passphrase via pinentry (secure OS dialog)
|
|
60
60
|
* 3. Hidden wallet — enter passphrase on device screen
|
|
61
61
|
*/
|
|
62
|
-
function resolvePassphraseByChoice(choice: '1' | '2' | '3'): Promise<PinentryResult> {
|
|
62
|
+
export function resolvePassphraseByChoice(choice: '1' | '2' | '3' | '4'): Promise<PinentryResult> {
|
|
63
63
|
if (choice === '1') return Promise.resolve({ value: '', passphraseOnDevice: false });
|
|
64
64
|
if (choice === '2') return promptPassphraseViaPinentry();
|
|
65
|
+
if (choice === '4')
|
|
66
|
+
return Promise.resolve({
|
|
67
|
+
value: '',
|
|
68
|
+
passphraseOnDevice: false,
|
|
69
|
+
attachPinOnDevice: true,
|
|
70
|
+
});
|
|
65
71
|
return Promise.resolve({ value: '', passphraseOnDevice: true });
|
|
66
72
|
}
|
|
67
73
|
|
|
@@ -84,18 +90,19 @@ function promptPassphraseMode(): Promise<PinentryResult> {
|
|
|
84
90
|
' 1. Standard wallet (no passphrase)',
|
|
85
91
|
' 2. Hidden wallet — enter passphrase on this computer (pinentry)',
|
|
86
92
|
' 3. Hidden wallet — enter passphrase on device screen',
|
|
93
|
+
' 4. Attach PIN wallet — enter Attach PIN on device screen',
|
|
87
94
|
'',
|
|
88
95
|
].join('\n')
|
|
89
96
|
);
|
|
90
97
|
|
|
91
98
|
rl.question('Enter selection [1/2/3]: ', answer => {
|
|
92
|
-
const n = answer.trim() as '1' | '2' | '3';
|
|
93
|
-
if (n === '1' || n === '2' || n === '3') {
|
|
99
|
+
const n = answer.trim() as '1' | '2' | '3' | '4';
|
|
100
|
+
if (n === '1' || n === '2' || n === '3' || n === '4') {
|
|
94
101
|
rl.close();
|
|
95
102
|
resolvePassphraseByChoice(n).then(resolve);
|
|
96
103
|
return;
|
|
97
104
|
}
|
|
98
|
-
process.stderr.write('Invalid selection. Enter 1, 2, or
|
|
105
|
+
process.stderr.write('Invalid selection. Enter 1, 2, 3, or 4.\n');
|
|
99
106
|
prompt();
|
|
100
107
|
});
|
|
101
108
|
};
|
|
@@ -143,6 +150,7 @@ function registerEventHandlers(sdk: typeof HardwareSDK): void {
|
|
|
143
150
|
payload: {
|
|
144
151
|
value: result.value,
|
|
145
152
|
passphraseOnDevice: result.passphraseOnDevice,
|
|
153
|
+
attachPinOnDevice: result.attachPinOnDevice,
|
|
146
154
|
save: false,
|
|
147
155
|
},
|
|
148
156
|
});
|
|
@@ -169,12 +177,12 @@ function registerEventHandlers(sdk: typeof HardwareSDK): void {
|
|
|
169
177
|
}
|
|
170
178
|
});
|
|
171
179
|
|
|
172
|
-
sdk.on(DEVICE.CONNECT, (device:
|
|
180
|
+
sdk.on(DEVICE.CONNECT, ({ device }: { device: KnownDevice }) => {
|
|
173
181
|
const name = device?.label || device?.name;
|
|
174
182
|
if (name) process.stderr.write(`[onekey-hw] Device connected: ${name}\n`);
|
|
175
183
|
});
|
|
176
184
|
|
|
177
|
-
sdk.on(DEVICE.DISCONNECT, (device:
|
|
185
|
+
sdk.on(DEVICE.DISCONNECT, ({ device }: { device: KnownDevice }) => {
|
|
178
186
|
const name = device?.label || device?.name;
|
|
179
187
|
if (name) process.stderr.write(`[onekey-hw] Device disconnected: ${name}\n`);
|
|
180
188
|
});
|
|
@@ -231,7 +239,7 @@ export async function disposeSDK(): Promise<void> {
|
|
|
231
239
|
if (!sdkReadyPromise) return;
|
|
232
240
|
try {
|
|
233
241
|
const sdk = await sdkReadyPromise;
|
|
234
|
-
sdk.dispose();
|
|
242
|
+
await Promise.resolve(sdk.dispose());
|
|
235
243
|
} catch {
|
|
236
244
|
// ignore errors during cleanup
|
|
237
245
|
} finally {
|
package/src/session.ts
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
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
|
import { preloadSessionCache } from '@onekeyfe/hd-core';
|
|
@@ -55,25 +52,6 @@ export async function preloadSessionFromKeychain(deviceId: string): Promise<stri
|
|
|
55
52
|
return undefined;
|
|
56
53
|
}
|
|
57
54
|
|
|
58
|
-
/**
|
|
59
|
-
* Save passphraseState + sessionId to keychain for next CLI invocation.
|
|
60
|
-
*/
|
|
61
|
-
export async function saveSessionToKeychain(
|
|
62
|
-
deviceId: string,
|
|
63
|
-
passphraseState: string,
|
|
64
|
-
sessionId: string
|
|
65
|
-
): Promise<void> {
|
|
66
|
-
try {
|
|
67
|
-
const storage = getStorage();
|
|
68
|
-
await Promise.all([
|
|
69
|
-
storage.set(psKey(deviceId), Buffer.from(passphraseState, 'utf-8')),
|
|
70
|
-
storage.set(sidKey(deviceId), Buffer.from(sessionId, 'utf-8')),
|
|
71
|
-
]);
|
|
72
|
-
} catch {
|
|
73
|
-
// Non-fatal — session still works in-memory for this invocation
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
55
|
/**
|
|
78
56
|
* Clear cached session from keychain.
|
|
79
57
|
*/
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ERRORS,
|
|
3
3
|
HardwareErrorCode,
|
|
4
|
+
ONEKEY_NOTIFY_CHARACTERISTIC_UUID,
|
|
4
5
|
ONEKEY_SERVICE_UUID,
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
ONEKEY_WRITE_CHARACTERISTIC_UUID,
|
|
7
|
+
createKnownBleUuidAliases,
|
|
8
|
+
hasOnekeyCommunicationService,
|
|
9
|
+
isOnekeyBluetoothDevice,
|
|
10
|
+
matchesKnownBleUuid,
|
|
7
11
|
} from '@onekeyfe/hd-shared';
|
|
8
12
|
|
|
9
13
|
import type { LowLevelDevice, LowlevelTransportSharedPlugin } from '@onekeyfe/hd-transport';
|
|
@@ -28,21 +32,28 @@ type CharacteristicPair = {
|
|
|
28
32
|
notify: Characteristic;
|
|
29
33
|
};
|
|
30
34
|
|
|
35
|
+
type NoblePendingReceiver = {
|
|
36
|
+
resolve: (data: string) => void;
|
|
37
|
+
reject: (error: Error) => void;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type NobleNotificationState = {
|
|
41
|
+
generation: number;
|
|
42
|
+
queue: string[];
|
|
43
|
+
pendingReceivers: Set<NoblePendingReceiver>;
|
|
44
|
+
};
|
|
45
|
+
|
|
31
46
|
const ONEKEY_SERVICE_UUIDS = [ONEKEY_SERVICE_UUID];
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const NORMALIZED_ONEKEY_SERVICE_UUIDS = new Set([
|
|
36
|
-
...ONEKEY_SERVICE_UUIDS.map(uuid => getBleUuidKey(uuid)),
|
|
37
|
-
'0001',
|
|
38
|
-
]);
|
|
47
|
+
const ONEKEY_SERVICE_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_SERVICE_UUID);
|
|
48
|
+
const ONEKEY_WRITE_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_WRITE_CHARACTERISTIC_UUID);
|
|
49
|
+
const ONEKEY_NOTIFY_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_NOTIFY_CHARACTERISTIC_UUID);
|
|
39
50
|
|
|
40
51
|
const BLUETOOTH_INIT_TIMEOUT = 10_000;
|
|
41
52
|
const DEVICE_SCAN_TIMEOUT = 8_000;
|
|
42
53
|
const CONNECTION_TIMEOUT = 8_000;
|
|
43
54
|
const SERVICE_DISCOVERY_TIMEOUT = 10_000;
|
|
55
|
+
const BLE_CLEANUP_TIMEOUT = 100;
|
|
44
56
|
const BLE_PACKET_SIZE = 192;
|
|
45
|
-
const BLE_WRITE_DELAY = 5;
|
|
46
57
|
const BLE_ENCRYPTION_ERROR_PATTERNS = [/encryption is insufficient/i, /insufficient encryption/i];
|
|
47
58
|
|
|
48
59
|
let noble: NobleModule | null = null;
|
|
@@ -50,42 +61,80 @@ let nobleReadyPromise: Promise<void> | null = null;
|
|
|
50
61
|
const discoveredDevices = new Map<string, Peripheral>();
|
|
51
62
|
const connectedDevices = new Map<string, Peripheral>();
|
|
52
63
|
const deviceCharacteristics = new Map<string, CharacteristicPair>();
|
|
53
|
-
const
|
|
54
|
-
const
|
|
64
|
+
const notificationStates = new Map<string, NobleNotificationState>();
|
|
65
|
+
const notificationGenerations = new Map<string, number>();
|
|
55
66
|
|
|
56
|
-
function
|
|
57
|
-
const
|
|
58
|
-
return
|
|
67
|
+
function isOneKeyPeripheral(peripheral: Peripheral) {
|
|
68
|
+
const serviceUuids = peripheral.advertisement?.serviceUuids;
|
|
69
|
+
return (
|
|
70
|
+
hasOnekeyCommunicationService(serviceUuids) &&
|
|
71
|
+
isOnekeyBluetoothDevice({
|
|
72
|
+
id: peripheral.id,
|
|
73
|
+
localName: peripheral.advertisement?.localName,
|
|
74
|
+
serviceUuids,
|
|
75
|
+
})
|
|
76
|
+
);
|
|
59
77
|
}
|
|
60
78
|
|
|
61
|
-
function
|
|
62
|
-
|
|
79
|
+
function enqueueNotification(deviceId: string, generation: number, data: Buffer) {
|
|
80
|
+
const state = notificationStates.get(deviceId);
|
|
81
|
+
if (!state || state.generation !== generation) return;
|
|
82
|
+
|
|
83
|
+
const hex = data.toString('hex');
|
|
84
|
+
const [receiver] = state.pendingReceivers;
|
|
85
|
+
if (receiver) {
|
|
86
|
+
state.pendingReceivers.delete(receiver);
|
|
87
|
+
receiver.resolve(hex);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
state.queue.push(hex);
|
|
63
91
|
}
|
|
64
92
|
|
|
65
|
-
function
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
93
|
+
function createNotificationState(deviceId: string) {
|
|
94
|
+
const existing = notificationStates.get(deviceId);
|
|
95
|
+
if (existing) {
|
|
96
|
+
const error = new Error(`BLE notification state replaced for ${deviceId}`);
|
|
97
|
+
existing.pendingReceivers.forEach(receiver => receiver.reject(error));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const generation = (notificationGenerations.get(deviceId) ?? 0) + 1;
|
|
101
|
+
notificationGenerations.set(deviceId, generation);
|
|
102
|
+
const state: NobleNotificationState = {
|
|
103
|
+
generation,
|
|
104
|
+
queue: [],
|
|
105
|
+
pendingReceivers: new Set(),
|
|
106
|
+
};
|
|
107
|
+
notificationStates.set(deviceId, state);
|
|
108
|
+
return state;
|
|
74
109
|
}
|
|
75
110
|
|
|
76
|
-
function
|
|
77
|
-
const
|
|
78
|
-
|
|
111
|
+
function clearNotificationState(deviceId: string, reason: string) {
|
|
112
|
+
const state = notificationStates.get(deviceId);
|
|
113
|
+
if (!state) return;
|
|
114
|
+
|
|
115
|
+
notificationStates.delete(deviceId);
|
|
116
|
+
const error = new Error(reason);
|
|
117
|
+
state.pendingReceivers.forEach(receiver => receiver.reject(error));
|
|
118
|
+
state.pendingReceivers.clear();
|
|
119
|
+
state.queue.length = 0;
|
|
79
120
|
}
|
|
80
121
|
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
122
|
+
function waitForNobleCleanup(registerCallback: (callback: () => void) => void) {
|
|
123
|
+
return new Promise<void>(resolve => {
|
|
124
|
+
let completed = false;
|
|
125
|
+
const complete = () => {
|
|
126
|
+
if (completed) return;
|
|
127
|
+
completed = true;
|
|
128
|
+
clearTimeout(timeout);
|
|
129
|
+
resolve();
|
|
130
|
+
};
|
|
131
|
+
const timeout = setTimeout(complete, BLE_CLEANUP_TIMEOUT);
|
|
132
|
+
try {
|
|
133
|
+
registerCallback(complete);
|
|
134
|
+
} catch {
|
|
135
|
+
complete();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
89
138
|
}
|
|
90
139
|
|
|
91
140
|
async function initializeNoble() {
|
|
@@ -169,7 +218,7 @@ async function scanDevices(targetDeviceId?: string) {
|
|
|
169
218
|
|
|
170
219
|
const onDiscover = (peripheral: Peripheral) => {
|
|
171
220
|
if (targetDeviceId && peripheral.id !== targetDeviceId) return;
|
|
172
|
-
if (!
|
|
221
|
+
if (!isOneKeyPeripheral(peripheral)) return;
|
|
173
222
|
|
|
174
223
|
discoveredDevices.set(peripheral.id, peripheral);
|
|
175
224
|
found.set(peripheral.id, peripheral);
|
|
@@ -224,13 +273,7 @@ async function discoverCharacteristics(peripheral: Peripheral): Promise<Characte
|
|
|
224
273
|
});
|
|
225
274
|
});
|
|
226
275
|
|
|
227
|
-
|
|
228
|
-
if (!service) {
|
|
229
|
-
service =
|
|
230
|
-
services.find(s => PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(getBleUuidKey(s.uuid))) ||
|
|
231
|
-
services.find(s => !isGenericBleService(s.uuid)) ||
|
|
232
|
-
services[0];
|
|
233
|
-
}
|
|
276
|
+
const service = services.find(s => matchesKnownBleUuid(s.uuid, ONEKEY_SERVICE_UUID_ALIASES));
|
|
234
277
|
if (!service) {
|
|
235
278
|
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No BLE service found');
|
|
236
279
|
}
|
|
@@ -249,10 +292,9 @@ async function discoverCharacteristics(peripheral: Peripheral): Promise<Characte
|
|
|
249
292
|
let writeCharacteristic: Characteristic | undefined;
|
|
250
293
|
let notifyCharacteristic: Characteristic | undefined;
|
|
251
294
|
for (const characteristic of characteristics) {
|
|
252
|
-
|
|
253
|
-
if (uuidKey === NORMALIZED_WRITE_UUID) {
|
|
295
|
+
if (matchesKnownBleUuid(characteristic.uuid, ONEKEY_WRITE_UUID_ALIASES)) {
|
|
254
296
|
writeCharacteristic = characteristic;
|
|
255
|
-
} else if (
|
|
297
|
+
} else if (matchesKnownBleUuid(characteristic.uuid, ONEKEY_NOTIFY_UUID_ALIASES)) {
|
|
256
298
|
notifyCharacteristic = characteristic;
|
|
257
299
|
}
|
|
258
300
|
}
|
|
@@ -270,10 +312,12 @@ async function discoverCharacteristics(peripheral: Peripheral): Promise<Characte
|
|
|
270
312
|
};
|
|
271
313
|
}
|
|
272
314
|
|
|
273
|
-
function subscribeNotifications(
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
315
|
+
function subscribeNotifications(
|
|
316
|
+
deviceId: string,
|
|
317
|
+
generation: number,
|
|
318
|
+
notifyCharacteristic: Characteristic
|
|
319
|
+
) {
|
|
320
|
+
return waitForNobleCleanup(callback => notifyCharacteristic.unsubscribe(callback))
|
|
277
321
|
.then(
|
|
278
322
|
() =>
|
|
279
323
|
new Promise<void>((resolve, reject) => {
|
|
@@ -303,7 +347,7 @@ function subscribeNotifications(deviceId: string, notifyCharacteristic: Characte
|
|
|
303
347
|
)
|
|
304
348
|
.then(() => {
|
|
305
349
|
notifyCharacteristic.removeAllListeners('data');
|
|
306
|
-
notifyCharacteristic.on('data', enqueueNotification);
|
|
350
|
+
notifyCharacteristic.on('data', data => enqueueNotification(deviceId, generation, data));
|
|
307
351
|
})
|
|
308
352
|
.catch(error => {
|
|
309
353
|
notifyCharacteristic.removeAllListeners('data');
|
|
@@ -314,9 +358,13 @@ function subscribeNotifications(deviceId: string, notifyCharacteristic: Characte
|
|
|
314
358
|
});
|
|
315
359
|
}
|
|
316
360
|
|
|
317
|
-
function writeCharacteristic(
|
|
361
|
+
function writeCharacteristic(
|
|
362
|
+
characteristic: Characteristic,
|
|
363
|
+
buffer: Buffer,
|
|
364
|
+
withoutResponse: boolean
|
|
365
|
+
) {
|
|
318
366
|
return new Promise<void>((resolve, reject) => {
|
|
319
|
-
characteristic.write(buffer,
|
|
367
|
+
characteristic.write(buffer, withoutResponse, (error?: Error) => {
|
|
320
368
|
if (error) {
|
|
321
369
|
reject(error);
|
|
322
370
|
return;
|
|
@@ -329,23 +377,18 @@ function writeCharacteristic(characteristic: Characteristic, buffer: Buffer) {
|
|
|
329
377
|
async function disconnectDevice(uuid: string) {
|
|
330
378
|
const peripheral = connectedDevices.get(uuid);
|
|
331
379
|
const characteristics = deviceCharacteristics.get(uuid);
|
|
380
|
+
clearNotificationState(uuid, `BLE device disconnected: ${uuid}`);
|
|
332
381
|
if (characteristics) {
|
|
333
382
|
characteristics.notify.removeAllListeners('data');
|
|
334
|
-
await
|
|
335
|
-
characteristics.notify.unsubscribe(() => resolve());
|
|
336
|
-
});
|
|
383
|
+
await waitForNobleCleanup(callback => characteristics.notify.unsubscribe(callback));
|
|
337
384
|
}
|
|
338
385
|
|
|
339
386
|
connectedDevices.delete(uuid);
|
|
340
387
|
deviceCharacteristics.delete(uuid);
|
|
341
|
-
notificationQueue.length = 0;
|
|
342
|
-
pendingReceivers.splice(0).forEach(resolve => resolve(''));
|
|
343
388
|
|
|
344
389
|
if (!peripheral || peripheral.state === 'disconnected') return;
|
|
345
390
|
|
|
346
|
-
await
|
|
347
|
-
peripheral.disconnect(() => resolve());
|
|
348
|
-
});
|
|
391
|
+
await waitForNobleCleanup(callback => peripheral.disconnect(callback));
|
|
349
392
|
}
|
|
350
393
|
|
|
351
394
|
export function createNobleBlePlugin(): LowlevelTransportSharedPlugin {
|
|
@@ -375,17 +418,31 @@ export function createNobleBlePlugin(): LowlevelTransportSharedPlugin {
|
|
|
375
418
|
}
|
|
376
419
|
|
|
377
420
|
await connectPeripheral(peripheral);
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
421
|
+
let characteristics: CharacteristicPair | undefined;
|
|
422
|
+
try {
|
|
423
|
+
characteristics = await discoverCharacteristics(peripheral);
|
|
424
|
+
const notificationState = createNotificationState(uuid);
|
|
425
|
+
await subscribeNotifications(uuid, notificationState.generation, characteristics.notify);
|
|
426
|
+
connectedDevices.set(uuid, peripheral);
|
|
427
|
+
deviceCharacteristics.set(uuid, characteristics);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
clearNotificationState(uuid, `BLE notification subscription failed: ${uuid}`);
|
|
430
|
+
if (characteristics) {
|
|
431
|
+
characteristics.notify.removeAllListeners('data');
|
|
432
|
+
await waitForNobleCleanup(callback => characteristics?.notify.unsubscribe(callback));
|
|
433
|
+
}
|
|
434
|
+
if (peripheral.state !== 'disconnected') {
|
|
435
|
+
await waitForNobleCleanup(callback => peripheral?.disconnect(callback));
|
|
436
|
+
}
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
382
439
|
},
|
|
383
440
|
|
|
384
441
|
async disconnect(uuid: string) {
|
|
385
442
|
await disconnectDevice(uuid);
|
|
386
443
|
},
|
|
387
444
|
|
|
388
|
-
async send(uuid: string, data: string) {
|
|
445
|
+
async send(uuid: string, data: string, options?: { withoutResponse?: boolean }) {
|
|
389
446
|
const characteristics = deviceCharacteristics.get(uuid);
|
|
390
447
|
if (!characteristics) {
|
|
391
448
|
throw ERRORS.TypedError(
|
|
@@ -395,20 +452,35 @@ export function createNobleBlePlugin(): LowlevelTransportSharedPlugin {
|
|
|
395
452
|
}
|
|
396
453
|
|
|
397
454
|
const buffer = Buffer.from(data, 'hex');
|
|
455
|
+
const withoutResponse = options?.withoutResponse ?? true;
|
|
398
456
|
for (let offset = 0; offset < buffer.length; offset += BLE_PACKET_SIZE) {
|
|
399
457
|
const chunk = buffer.subarray(offset, Math.min(offset + BLE_PACKET_SIZE, buffer.length));
|
|
400
|
-
await writeCharacteristic(characteristics.write, chunk);
|
|
401
|
-
if (offset + BLE_PACKET_SIZE < buffer.length) {
|
|
402
|
-
await wait(BLE_WRITE_DELAY);
|
|
403
|
-
}
|
|
458
|
+
await writeCharacteristic(characteristics.write, chunk, withoutResponse);
|
|
404
459
|
}
|
|
405
460
|
},
|
|
406
461
|
|
|
407
|
-
async receive() {
|
|
408
|
-
const
|
|
462
|
+
async receive(uuid?: string) {
|
|
463
|
+
const resolvedUuid =
|
|
464
|
+
uuid ??
|
|
465
|
+
(notificationStates.size === 1 ? notificationStates.keys().next().value : undefined);
|
|
466
|
+
if (!resolvedUuid) {
|
|
467
|
+
throw ERRORS.TypedError(
|
|
468
|
+
HardwareErrorCode.RuntimeError,
|
|
469
|
+
'BLE receive requires a device UUID when multiple devices are connected'
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const state = notificationStates.get(resolvedUuid);
|
|
474
|
+
if (!state) {
|
|
475
|
+
throw ERRORS.TypedError(
|
|
476
|
+
HardwareErrorCode.TransportNotFound,
|
|
477
|
+
`BLE notification state not found: ${resolvedUuid}`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
const queued = state.queue.shift();
|
|
409
481
|
if (queued !== undefined) return queued;
|
|
410
|
-
return new Promise<string>(resolve => {
|
|
411
|
-
pendingReceivers.
|
|
482
|
+
return new Promise<string>((resolve, reject) => {
|
|
483
|
+
state.pendingReceivers.add({ resolve, reject });
|
|
412
484
|
});
|
|
413
485
|
},
|
|
414
486
|
};
|