@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.100",
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.100",
35
- "@onekeyfe/hd-core": "1.2.2-alpha.100",
36
- "@onekeyfe/hd-shared": "1.2.2-alpha.100",
37
- "@onekeyfe/hd-transport-usb": "1.2.2-alpha.100",
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": "c40dad085297b0e3cc2020bd218c146dff6ed3e1"
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
- return {
31
- peripheral: {
32
- id,
33
- state: 'connected',
34
- advertisement: {
35
- localName: `OneKey Pro 2 ${id}`,
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');