@onekeyfe/hardware-cli 1.2.2-alpha.100 → 1.2.2-alpha.3
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.
|
@@ -20,6 +20,7 @@ const connectedDevices = new Map();
|
|
|
20
20
|
const deviceCharacteristics = new Map();
|
|
21
21
|
const notificationStates = new Map();
|
|
22
22
|
const notificationGenerations = new Map();
|
|
23
|
+
const disconnectListeners = new Map();
|
|
23
24
|
function isOneKeyPeripheral(peripheral) {
|
|
24
25
|
const serviceUuids = peripheral.advertisement?.serviceUuids;
|
|
25
26
|
return ((0, hd_shared_1.hasOnekeyCommunicationService)(serviceUuids) &&
|
|
@@ -63,11 +64,32 @@ function clearNotificationState(deviceId, reason) {
|
|
|
63
64
|
if (!state)
|
|
64
65
|
return;
|
|
65
66
|
notificationStates.delete(deviceId);
|
|
66
|
-
const error = new Error(reason);
|
|
67
|
+
const error = reason instanceof Error ? reason : new Error(reason);
|
|
67
68
|
state.pendingReceivers.forEach(receiver => receiver.reject(error));
|
|
68
69
|
state.pendingReceivers.clear();
|
|
69
70
|
state.queue.length = 0;
|
|
70
71
|
}
|
|
72
|
+
function removeDisconnectListener(deviceId) {
|
|
73
|
+
const tracked = disconnectListeners.get(deviceId);
|
|
74
|
+
if (!tracked)
|
|
75
|
+
return;
|
|
76
|
+
tracked.peripheral.removeListener('disconnect', tracked.listener);
|
|
77
|
+
disconnectListeners.delete(deviceId);
|
|
78
|
+
}
|
|
79
|
+
function trackUnexpectedDisconnect(deviceId, peripheral) {
|
|
80
|
+
removeDisconnectListener(deviceId);
|
|
81
|
+
const listener = (reason) => {
|
|
82
|
+
removeDisconnectListener(deviceId);
|
|
83
|
+
if (connectedDevices.get(deviceId) !== peripheral)
|
|
84
|
+
return;
|
|
85
|
+
deviceCharacteristics.get(deviceId)?.notify.removeAllListeners('data');
|
|
86
|
+
connectedDevices.delete(deviceId);
|
|
87
|
+
deviceCharacteristics.delete(deviceId);
|
|
88
|
+
clearNotificationState(deviceId, hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BleDeviceDisconnected, reason || `BLE device disconnected: ${deviceId}`));
|
|
89
|
+
};
|
|
90
|
+
disconnectListeners.set(deviceId, { peripheral, listener });
|
|
91
|
+
peripheral.on('disconnect', listener);
|
|
92
|
+
}
|
|
71
93
|
function waitForNobleCleanup(registerCallback) {
|
|
72
94
|
return new Promise(resolve => {
|
|
73
95
|
let completed = false;
|
|
@@ -280,6 +302,7 @@ function writeCharacteristic(characteristic, buffer, withoutResponse) {
|
|
|
280
302
|
async function disconnectDevice(uuid) {
|
|
281
303
|
const peripheral = connectedDevices.get(uuid);
|
|
282
304
|
const characteristics = deviceCharacteristics.get(uuid);
|
|
305
|
+
removeDisconnectListener(uuid);
|
|
283
306
|
clearNotificationState(uuid, `BLE device disconnected: ${uuid}`);
|
|
284
307
|
if (characteristics) {
|
|
285
308
|
characteristics.notify.removeAllListeners('data');
|
|
@@ -321,8 +344,10 @@ function createNobleBlePlugin() {
|
|
|
321
344
|
await subscribeNotifications(uuid, notificationState.generation, characteristics.notify);
|
|
322
345
|
connectedDevices.set(uuid, peripheral);
|
|
323
346
|
deviceCharacteristics.set(uuid, characteristics);
|
|
347
|
+
trackUnexpectedDisconnect(uuid, peripheral);
|
|
324
348
|
}
|
|
325
349
|
catch (error) {
|
|
350
|
+
removeDisconnectListener(uuid);
|
|
326
351
|
clearNotificationState(uuid, `BLE notification subscription failed: ${uuid}`);
|
|
327
352
|
if (characteristics) {
|
|
328
353
|
characteristics.notify.removeAllListeners('data');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hardware-cli",
|
|
3
|
-
"version": "1.2.2-alpha.
|
|
3
|
+
"version": "1.2.2-alpha.3",
|
|
4
4
|
"description": "OneKey hardware wallet CLI for testing device communication",
|
|
5
5
|
"author": "OneKey",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -31,12 +31,12 @@
|
|
|
31
31
|
"test": "jest"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@onekeyfe/hd-common-connect-sdk": "1.2.2-alpha.
|
|
35
|
-
"@onekeyfe/hd-core": "1.2.2-alpha.
|
|
36
|
-
"@onekeyfe/hd-shared": "1.2.2-alpha.
|
|
37
|
-
"@onekeyfe/hd-transport-usb": "1.2.2-alpha.
|
|
34
|
+
"@onekeyfe/hd-common-connect-sdk": "1.2.2-alpha.3",
|
|
35
|
+
"@onekeyfe/hd-core": "1.2.2-alpha.3",
|
|
36
|
+
"@onekeyfe/hd-shared": "1.2.2-alpha.3",
|
|
37
|
+
"@onekeyfe/hd-transport-usb": "1.2.2-alpha.3",
|
|
38
38
|
"@stoprocent/noble": "2.3.16",
|
|
39
39
|
"commander": "^12.0.0"
|
|
40
40
|
},
|
|
41
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "4efa66df8851607586d0ae6a431a63404beff51b"
|
|
42
42
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from 'events';
|
|
2
|
+
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
3
|
|
|
3
4
|
type MockCharacteristic = EventEmitter & {
|
|
4
5
|
uuid: string;
|
|
@@ -27,18 +28,19 @@ const createPeripheral = (id: string) => {
|
|
|
27
28
|
uuid: '0001',
|
|
28
29
|
discoverCharacteristics: jest.fn((_uuids, callback) => callback(null, [write, notify])),
|
|
29
30
|
};
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
serviceUuids: ['0001'],
|
|
37
|
-
},
|
|
38
|
-
discoverServices: jest.fn((_uuids, callback) => callback(null, [service])),
|
|
39
|
-
connect: jest.fn(callback => callback()),
|
|
40
|
-
disconnect: jest.fn(callback => callback()),
|
|
31
|
+
const peripheral = Object.assign(new EventEmitter(), {
|
|
32
|
+
id,
|
|
33
|
+
state: 'connected',
|
|
34
|
+
advertisement: {
|
|
35
|
+
localName: `OneKey Pro 2 ${id}`,
|
|
36
|
+
serviceUuids: ['0001'],
|
|
41
37
|
},
|
|
38
|
+
discoverServices: jest.fn((_uuids, callback) => callback(null, [service])),
|
|
39
|
+
connect: jest.fn(callback => callback()),
|
|
40
|
+
disconnect: jest.fn(callback => callback()),
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
peripheral,
|
|
42
44
|
service,
|
|
43
45
|
write,
|
|
44
46
|
notify,
|
|
@@ -177,6 +179,38 @@ describe('Noble BLE plugin notification routing', () => {
|
|
|
177
179
|
expect(result).toBe('completed');
|
|
178
180
|
});
|
|
179
181
|
|
|
182
|
+
test('rejects a pending receive when the peripheral disconnects unexpectedly', async () => {
|
|
183
|
+
const device = createPeripheral('device-a');
|
|
184
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
185
|
+
state: string;
|
|
186
|
+
startScanning: jest.Mock;
|
|
187
|
+
stopScanning: jest.Mock;
|
|
188
|
+
};
|
|
189
|
+
noble.state = 'poweredOn';
|
|
190
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
191
|
+
callback?.();
|
|
192
|
+
noble.emit('discover', device.peripheral);
|
|
193
|
+
});
|
|
194
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
195
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
196
|
+
|
|
197
|
+
const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
|
|
198
|
+
const plugin = createNobleBlePlugin();
|
|
199
|
+
await plugin.init();
|
|
200
|
+
await plugin.connect('device-a');
|
|
201
|
+
|
|
202
|
+
const receive = plugin.receive('device-a');
|
|
203
|
+
device.peripheral.state = 'disconnected';
|
|
204
|
+
device.peripheral.emit('disconnect', 'Remote User Terminated Connection');
|
|
205
|
+
|
|
206
|
+
await expect(receive).rejects.toMatchObject({
|
|
207
|
+
errorCode: HardwareErrorCode.BleDeviceDisconnected,
|
|
208
|
+
});
|
|
209
|
+
await expect(plugin.receive('device-a')).rejects.toMatchObject({
|
|
210
|
+
errorCode: HardwareErrorCode.TransportNotFound,
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
180
214
|
test('disconnects an untracked peripheral when service discovery fails', async () => {
|
|
181
215
|
const device = createPeripheral('device-a');
|
|
182
216
|
device.peripheral.discoverServices.mockImplementation((_uuids, callback) =>
|
|
@@ -43,6 +43,11 @@ type NobleNotificationState = {
|
|
|
43
43
|
pendingReceivers: Set<NoblePendingReceiver>;
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
type NobleDisconnectListener = {
|
|
47
|
+
peripheral: Peripheral;
|
|
48
|
+
listener: (reason: string) => void;
|
|
49
|
+
};
|
|
50
|
+
|
|
46
51
|
const ONEKEY_SERVICE_UUIDS = [ONEKEY_SERVICE_UUID];
|
|
47
52
|
const ONEKEY_SERVICE_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_SERVICE_UUID);
|
|
48
53
|
const ONEKEY_WRITE_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_WRITE_CHARACTERISTIC_UUID);
|
|
@@ -63,6 +68,7 @@ const connectedDevices = new Map<string, Peripheral>();
|
|
|
63
68
|
const deviceCharacteristics = new Map<string, CharacteristicPair>();
|
|
64
69
|
const notificationStates = new Map<string, NobleNotificationState>();
|
|
65
70
|
const notificationGenerations = new Map<string, number>();
|
|
71
|
+
const disconnectListeners = new Map<string, NobleDisconnectListener>();
|
|
66
72
|
|
|
67
73
|
function isOneKeyPeripheral(peripheral: Peripheral) {
|
|
68
74
|
const serviceUuids = peripheral.advertisement?.serviceUuids;
|
|
@@ -108,17 +114,45 @@ function createNotificationState(deviceId: string) {
|
|
|
108
114
|
return state;
|
|
109
115
|
}
|
|
110
116
|
|
|
111
|
-
function clearNotificationState(deviceId: string, reason: string) {
|
|
117
|
+
function clearNotificationState(deviceId: string, reason: string | Error) {
|
|
112
118
|
const state = notificationStates.get(deviceId);
|
|
113
119
|
if (!state) return;
|
|
114
120
|
|
|
115
121
|
notificationStates.delete(deviceId);
|
|
116
|
-
const error = new Error(reason);
|
|
122
|
+
const error = reason instanceof Error ? reason : new Error(reason);
|
|
117
123
|
state.pendingReceivers.forEach(receiver => receiver.reject(error));
|
|
118
124
|
state.pendingReceivers.clear();
|
|
119
125
|
state.queue.length = 0;
|
|
120
126
|
}
|
|
121
127
|
|
|
128
|
+
function removeDisconnectListener(deviceId: string) {
|
|
129
|
+
const tracked = disconnectListeners.get(deviceId);
|
|
130
|
+
if (!tracked) return;
|
|
131
|
+
tracked.peripheral.removeListener('disconnect', tracked.listener);
|
|
132
|
+
disconnectListeners.delete(deviceId);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function trackUnexpectedDisconnect(deviceId: string, peripheral: Peripheral) {
|
|
136
|
+
removeDisconnectListener(deviceId);
|
|
137
|
+
const listener = (reason: string) => {
|
|
138
|
+
removeDisconnectListener(deviceId);
|
|
139
|
+
if (connectedDevices.get(deviceId) !== peripheral) return;
|
|
140
|
+
|
|
141
|
+
deviceCharacteristics.get(deviceId)?.notify.removeAllListeners('data');
|
|
142
|
+
connectedDevices.delete(deviceId);
|
|
143
|
+
deviceCharacteristics.delete(deviceId);
|
|
144
|
+
clearNotificationState(
|
|
145
|
+
deviceId,
|
|
146
|
+
ERRORS.TypedError(
|
|
147
|
+
HardwareErrorCode.BleDeviceDisconnected,
|
|
148
|
+
reason || `BLE device disconnected: ${deviceId}`
|
|
149
|
+
)
|
|
150
|
+
);
|
|
151
|
+
};
|
|
152
|
+
disconnectListeners.set(deviceId, { peripheral, listener });
|
|
153
|
+
peripheral.on('disconnect', listener);
|
|
154
|
+
}
|
|
155
|
+
|
|
122
156
|
function waitForNobleCleanup(registerCallback: (callback: () => void) => void) {
|
|
123
157
|
return new Promise<void>(resolve => {
|
|
124
158
|
let completed = false;
|
|
@@ -377,6 +411,7 @@ function writeCharacteristic(
|
|
|
377
411
|
async function disconnectDevice(uuid: string) {
|
|
378
412
|
const peripheral = connectedDevices.get(uuid);
|
|
379
413
|
const characteristics = deviceCharacteristics.get(uuid);
|
|
414
|
+
removeDisconnectListener(uuid);
|
|
380
415
|
clearNotificationState(uuid, `BLE device disconnected: ${uuid}`);
|
|
381
416
|
if (characteristics) {
|
|
382
417
|
characteristics.notify.removeAllListeners('data');
|
|
@@ -425,7 +460,9 @@ export function createNobleBlePlugin(): LowlevelTransportSharedPlugin {
|
|
|
425
460
|
await subscribeNotifications(uuid, notificationState.generation, characteristics.notify);
|
|
426
461
|
connectedDevices.set(uuid, peripheral);
|
|
427
462
|
deviceCharacteristics.set(uuid, characteristics);
|
|
463
|
+
trackUnexpectedDisconnect(uuid, peripheral);
|
|
428
464
|
} catch (error) {
|
|
465
|
+
removeDisconnectListener(uuid);
|
|
429
466
|
clearNotificationState(uuid, `BLE notification subscription failed: ${uuid}`);
|
|
430
467
|
if (characteristics) {
|
|
431
468
|
characteristics.notify.removeAllListeners('data');
|