@onekeyfe/hardware-cli 1.2.2-alpha.100 → 1.2.2-alpha.12

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.
@@ -1,2 +1,4 @@
1
+ /// <reference types="node" />
1
2
  import type { LowlevelTransportSharedPlugin } from '@onekeyfe/hd-transport';
3
+ export declare function resolveNobleProtocolV2PacketCapacity(mtu: number | null | undefined, platform?: NodeJS.Platform): number;
2
4
  export declare function createNobleBlePlugin(): LowlevelTransportSharedPlugin;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createNobleBlePlugin = void 0;
3
+ exports.createNobleBlePlugin = exports.resolveNobleProtocolV2PacketCapacity = 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
6
  const ONEKEY_SERVICE_UUID_ALIASES = (0, hd_shared_1.createKnownBleUuidAliases)(hd_shared_1.ONEKEY_SERVICE_UUID);
@@ -11,8 +11,22 @@ const DEVICE_SCAN_TIMEOUT = 8000;
11
11
  const CONNECTION_TIMEOUT = 8000;
12
12
  const SERVICE_DISCOVERY_TIMEOUT = 10000;
13
13
  const BLE_CLEANUP_TIMEOUT = 100;
14
- const BLE_PACKET_SIZE = 192;
14
+ const BLE_PACKET_SIZE_FALLBACK = 192;
15
+ const BLE_PACKET_SIZE_MAX = 244;
16
+ const ATT_WRITE_HEADER_SIZE = 3;
15
17
  const BLE_ENCRYPTION_ERROR_PATTERNS = [/encryption is insufficient/i, /insufficient encryption/i];
18
+ function resolveNobleProtocolV2PacketCapacity(mtu, platform = process.platform) {
19
+ if (typeof mtu !== 'number' || !Number.isFinite(mtu) || mtu <= 0) {
20
+ return BLE_PACKET_SIZE_FALLBACK;
21
+ }
22
+ const reportedCapacity = Math.floor(mtu);
23
+ const payloadCapacity = platform === 'linux' ? reportedCapacity - ATT_WRITE_HEADER_SIZE : reportedCapacity;
24
+ if (payloadCapacity <= 0) {
25
+ return BLE_PACKET_SIZE_FALLBACK;
26
+ }
27
+ return Math.min(payloadCapacity, BLE_PACKET_SIZE_MAX);
28
+ }
29
+ exports.resolveNobleProtocolV2PacketCapacity = resolveNobleProtocolV2PacketCapacity;
16
30
  let noble = null;
17
31
  let nobleReadyPromise = null;
18
32
  const discoveredDevices = new Map();
@@ -20,6 +34,7 @@ const connectedDevices = new Map();
20
34
  const deviceCharacteristics = new Map();
21
35
  const notificationStates = new Map();
22
36
  const notificationGenerations = new Map();
37
+ const disconnectListeners = new Map();
23
38
  function isOneKeyPeripheral(peripheral) {
24
39
  const serviceUuids = peripheral.advertisement?.serviceUuids;
25
40
  return ((0, hd_shared_1.hasOnekeyCommunicationService)(serviceUuids) &&
@@ -63,11 +78,32 @@ function clearNotificationState(deviceId, reason) {
63
78
  if (!state)
64
79
  return;
65
80
  notificationStates.delete(deviceId);
66
- const error = new Error(reason);
81
+ const error = reason instanceof Error ? reason : new Error(reason);
67
82
  state.pendingReceivers.forEach(receiver => receiver.reject(error));
68
83
  state.pendingReceivers.clear();
69
84
  state.queue.length = 0;
70
85
  }
86
+ function removeDisconnectListener(deviceId) {
87
+ const tracked = disconnectListeners.get(deviceId);
88
+ if (!tracked)
89
+ return;
90
+ tracked.peripheral.removeListener('disconnect', tracked.listener);
91
+ disconnectListeners.delete(deviceId);
92
+ }
93
+ function trackUnexpectedDisconnect(deviceId, peripheral) {
94
+ removeDisconnectListener(deviceId);
95
+ const listener = (reason) => {
96
+ removeDisconnectListener(deviceId);
97
+ if (connectedDevices.get(deviceId) !== peripheral)
98
+ return;
99
+ deviceCharacteristics.get(deviceId)?.notify.removeAllListeners('data');
100
+ connectedDevices.delete(deviceId);
101
+ deviceCharacteristics.delete(deviceId);
102
+ clearNotificationState(deviceId, hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BleDeviceDisconnected, reason || `BLE device disconnected: ${deviceId}`));
103
+ };
104
+ disconnectListeners.set(deviceId, { peripheral, listener });
105
+ peripheral.on('disconnect', listener);
106
+ }
71
107
  function waitForNobleCleanup(registerCallback) {
72
108
  return new Promise(resolve => {
73
109
  let completed = false;
@@ -280,6 +316,7 @@ function writeCharacteristic(characteristic, buffer, withoutResponse) {
280
316
  async function disconnectDevice(uuid) {
281
317
  const peripheral = connectedDevices.get(uuid);
282
318
  const characteristics = deviceCharacteristics.get(uuid);
319
+ removeDisconnectListener(uuid);
283
320
  clearNotificationState(uuid, `BLE device disconnected: ${uuid}`);
284
321
  if (characteristics) {
285
322
  characteristics.notify.removeAllListeners('data');
@@ -321,8 +358,10 @@ function createNobleBlePlugin() {
321
358
  await subscribeNotifications(uuid, notificationState.generation, characteristics.notify);
322
359
  connectedDevices.set(uuid, peripheral);
323
360
  deviceCharacteristics.set(uuid, characteristics);
361
+ trackUnexpectedDisconnect(uuid, peripheral);
324
362
  }
325
363
  catch (error) {
364
+ removeDisconnectListener(uuid);
326
365
  clearNotificationState(uuid, `BLE notification subscription failed: ${uuid}`);
327
366
  if (characteristics) {
328
367
  characteristics.notify.removeAllListeners('data');
@@ -337,6 +376,9 @@ function createNobleBlePlugin() {
337
376
  async disconnect(uuid) {
338
377
  await disconnectDevice(uuid);
339
378
  },
379
+ getProtocolV2PacketCapacity(uuid) {
380
+ return resolveNobleProtocolV2PacketCapacity(connectedDevices.get(uuid)?.mtu);
381
+ },
340
382
  async send(uuid, data, options) {
341
383
  const characteristics = deviceCharacteristics.get(uuid);
342
384
  if (!characteristics) {
@@ -344,8 +386,9 @@ function createNobleBlePlugin() {
344
386
  }
345
387
  const buffer = Buffer.from(data, 'hex');
346
388
  const withoutResponse = options?.withoutResponse ?? true;
347
- for (let offset = 0; offset < buffer.length; offset += BLE_PACKET_SIZE) {
348
- const chunk = buffer.subarray(offset, Math.min(offset + BLE_PACKET_SIZE, buffer.length));
389
+ const packetCapacity = resolveNobleProtocolV2PacketCapacity(connectedDevices.get(uuid)?.mtu);
390
+ for (let offset = 0; offset < buffer.length; offset += packetCapacity) {
391
+ const chunk = buffer.subarray(offset, Math.min(offset + packetCapacity, buffer.length));
349
392
  await writeCharacteristic(characteristics.write, chunk, withoutResponse);
350
393
  }
351
394
  },
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.12",
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.12",
35
+ "@onekeyfe/hd-core": "1.2.2-alpha.12",
36
+ "@onekeyfe/hd-shared": "1.2.2-alpha.12",
37
+ "@onekeyfe/hd-transport-usb": "1.2.2-alpha.12",
38
38
  "@stoprocent/noble": "2.3.16",
39
39
  "commander": "^12.0.0"
40
40
  },
41
- "gitHead": "c40dad085297b0e3cc2020bd218c146dff6ed3e1"
41
+ "gitHead": "9b5a45bc55819d22ed752899b4c40aad70de4527"
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,20 @@ 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
+ mtu: null as number | null,
35
+ advertisement: {
36
+ localName: `OneKey Pro 2 ${id}`,
37
+ serviceUuids: ['0001'],
41
38
  },
39
+ discoverServices: jest.fn((_uuids, callback) => callback(null, [service])),
40
+ connect: jest.fn(callback => callback()),
41
+ disconnect: jest.fn(callback => callback()),
42
+ });
43
+ return {
44
+ peripheral,
42
45
  service,
43
46
  write,
44
47
  notify,
@@ -52,6 +55,15 @@ describe('Noble BLE plugin notification routing', () => {
52
55
  jest.clearAllMocks();
53
56
  });
54
57
 
58
+ test('normalizes the platform-specific Noble MTU to a safe write capacity', async () => {
59
+ const { resolveNobleProtocolV2PacketCapacity } = await import('../transports/nobleBlePlugin');
60
+
61
+ expect(resolveNobleProtocolV2PacketCapacity(null, 'darwin')).toBe(192);
62
+ expect(resolveNobleProtocolV2PacketCapacity(244, 'darwin')).toBe(244);
63
+ expect(resolveNobleProtocolV2PacketCapacity(247, 'linux')).toBe(244);
64
+ expect(resolveNobleProtocolV2PacketCapacity(512, 'win32')).toBe(244);
65
+ });
66
+
55
67
  test('does not enumerate Find My advertisements that expose FFFD', async () => {
56
68
  jest.useFakeTimers({ doNotFake: ['performance'] });
57
69
  const oneKey = createPeripheral('onekey-device');
@@ -177,6 +189,38 @@ describe('Noble BLE plugin notification routing', () => {
177
189
  expect(result).toBe('completed');
178
190
  });
179
191
 
192
+ test('rejects a pending receive when the peripheral disconnects unexpectedly', async () => {
193
+ const device = createPeripheral('device-a');
194
+ const noble = new EventEmitter() as EventEmitter & {
195
+ state: string;
196
+ startScanning: jest.Mock;
197
+ stopScanning: jest.Mock;
198
+ };
199
+ noble.state = 'poweredOn';
200
+ noble.startScanning = jest.fn((_services, _duplicates, callback) => {
201
+ callback?.();
202
+ noble.emit('discover', device.peripheral);
203
+ });
204
+ noble.stopScanning = jest.fn(callback => callback?.());
205
+ jest.doMock('@stoprocent/noble', () => noble);
206
+
207
+ const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
208
+ const plugin = createNobleBlePlugin();
209
+ await plugin.init();
210
+ await plugin.connect('device-a');
211
+
212
+ const receive = plugin.receive('device-a');
213
+ device.peripheral.state = 'disconnected';
214
+ device.peripheral.emit('disconnect', 'Remote User Terminated Connection');
215
+
216
+ await expect(receive).rejects.toMatchObject({
217
+ errorCode: HardwareErrorCode.BleDeviceDisconnected,
218
+ });
219
+ await expect(plugin.receive('device-a')).rejects.toMatchObject({
220
+ errorCode: HardwareErrorCode.TransportNotFound,
221
+ });
222
+ });
223
+
180
224
  test('disconnects an untracked peripheral when service discovery fails', async () => {
181
225
  const device = createPeripheral('device-a');
182
226
  device.peripheral.discoverServices.mockImplementation((_uuids, callback) =>
@@ -342,6 +386,33 @@ describe('Noble BLE plugin notification routing', () => {
342
386
  expect(wait).not.toHaveBeenCalled();
343
387
  });
344
388
 
389
+ test('uses the connected peripheral write capacity without padding', async () => {
390
+ const device = createPeripheral('device-a');
391
+ device.peripheral.mtu = process.platform === 'linux' ? 247 : 244;
392
+ const noble = new EventEmitter() as EventEmitter & {
393
+ state: string;
394
+ startScanning: jest.Mock;
395
+ stopScanning: jest.Mock;
396
+ };
397
+ noble.state = 'poweredOn';
398
+ noble.startScanning = jest.fn((_services, _duplicates, callback) => {
399
+ callback?.();
400
+ noble.emit('discover', device.peripheral);
401
+ });
402
+ noble.stopScanning = jest.fn(callback => callback?.());
403
+ jest.doMock('@stoprocent/noble', () => noble);
404
+
405
+ const { createNobleBlePlugin } = await import('../transports/nobleBlePlugin');
406
+ const plugin = createNobleBlePlugin();
407
+ await plugin.init();
408
+ await plugin.connect('device-a');
409
+
410
+ await plugin.send('device-a', 'aa'.repeat(245));
411
+
412
+ expect(plugin.getProtocolV2PacketCapacity?.('device-a')).toBe(244);
413
+ expect(device.write.write.mock.calls.map(([packet]) => packet.length)).toEqual([244, 1]);
414
+ });
415
+
345
416
  test('preserves a short final BLE packet without padding', async () => {
346
417
  const device = createPeripheral('device-a');
347
418
  const noble = new EventEmitter() as EventEmitter & {
@@ -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);
@@ -53,9 +58,27 @@ const DEVICE_SCAN_TIMEOUT = 8_000;
53
58
  const CONNECTION_TIMEOUT = 8_000;
54
59
  const SERVICE_DISCOVERY_TIMEOUT = 10_000;
55
60
  const BLE_CLEANUP_TIMEOUT = 100;
56
- const BLE_PACKET_SIZE = 192;
61
+ const BLE_PACKET_SIZE_FALLBACK = 192;
62
+ const BLE_PACKET_SIZE_MAX = 244;
63
+ const ATT_WRITE_HEADER_SIZE = 3;
57
64
  const BLE_ENCRYPTION_ERROR_PATTERNS = [/encryption is insufficient/i, /insufficient encryption/i];
58
65
 
66
+ export function resolveNobleProtocolV2PacketCapacity(
67
+ mtu: number | null | undefined,
68
+ platform: NodeJS.Platform = process.platform
69
+ ) {
70
+ if (typeof mtu !== 'number' || !Number.isFinite(mtu) || mtu <= 0) {
71
+ return BLE_PACKET_SIZE_FALLBACK;
72
+ }
73
+ const reportedCapacity = Math.floor(mtu);
74
+ const payloadCapacity =
75
+ platform === 'linux' ? reportedCapacity - ATT_WRITE_HEADER_SIZE : reportedCapacity;
76
+ if (payloadCapacity <= 0) {
77
+ return BLE_PACKET_SIZE_FALLBACK;
78
+ }
79
+ return Math.min(payloadCapacity, BLE_PACKET_SIZE_MAX);
80
+ }
81
+
59
82
  let noble: NobleModule | null = null;
60
83
  let nobleReadyPromise: Promise<void> | null = null;
61
84
  const discoveredDevices = new Map<string, Peripheral>();
@@ -63,6 +86,7 @@ const connectedDevices = new Map<string, Peripheral>();
63
86
  const deviceCharacteristics = new Map<string, CharacteristicPair>();
64
87
  const notificationStates = new Map<string, NobleNotificationState>();
65
88
  const notificationGenerations = new Map<string, number>();
89
+ const disconnectListeners = new Map<string, NobleDisconnectListener>();
66
90
 
67
91
  function isOneKeyPeripheral(peripheral: Peripheral) {
68
92
  const serviceUuids = peripheral.advertisement?.serviceUuids;
@@ -108,17 +132,45 @@ function createNotificationState(deviceId: string) {
108
132
  return state;
109
133
  }
110
134
 
111
- function clearNotificationState(deviceId: string, reason: string) {
135
+ function clearNotificationState(deviceId: string, reason: string | Error) {
112
136
  const state = notificationStates.get(deviceId);
113
137
  if (!state) return;
114
138
 
115
139
  notificationStates.delete(deviceId);
116
- const error = new Error(reason);
140
+ const error = reason instanceof Error ? reason : new Error(reason);
117
141
  state.pendingReceivers.forEach(receiver => receiver.reject(error));
118
142
  state.pendingReceivers.clear();
119
143
  state.queue.length = 0;
120
144
  }
121
145
 
146
+ function removeDisconnectListener(deviceId: string) {
147
+ const tracked = disconnectListeners.get(deviceId);
148
+ if (!tracked) return;
149
+ tracked.peripheral.removeListener('disconnect', tracked.listener);
150
+ disconnectListeners.delete(deviceId);
151
+ }
152
+
153
+ function trackUnexpectedDisconnect(deviceId: string, peripheral: Peripheral) {
154
+ removeDisconnectListener(deviceId);
155
+ const listener = (reason: string) => {
156
+ removeDisconnectListener(deviceId);
157
+ if (connectedDevices.get(deviceId) !== peripheral) return;
158
+
159
+ deviceCharacteristics.get(deviceId)?.notify.removeAllListeners('data');
160
+ connectedDevices.delete(deviceId);
161
+ deviceCharacteristics.delete(deviceId);
162
+ clearNotificationState(
163
+ deviceId,
164
+ ERRORS.TypedError(
165
+ HardwareErrorCode.BleDeviceDisconnected,
166
+ reason || `BLE device disconnected: ${deviceId}`
167
+ )
168
+ );
169
+ };
170
+ disconnectListeners.set(deviceId, { peripheral, listener });
171
+ peripheral.on('disconnect', listener);
172
+ }
173
+
122
174
  function waitForNobleCleanup(registerCallback: (callback: () => void) => void) {
123
175
  return new Promise<void>(resolve => {
124
176
  let completed = false;
@@ -377,6 +429,7 @@ function writeCharacteristic(
377
429
  async function disconnectDevice(uuid: string) {
378
430
  const peripheral = connectedDevices.get(uuid);
379
431
  const characteristics = deviceCharacteristics.get(uuid);
432
+ removeDisconnectListener(uuid);
380
433
  clearNotificationState(uuid, `BLE device disconnected: ${uuid}`);
381
434
  if (characteristics) {
382
435
  characteristics.notify.removeAllListeners('data');
@@ -425,7 +478,9 @@ export function createNobleBlePlugin(): LowlevelTransportSharedPlugin {
425
478
  await subscribeNotifications(uuid, notificationState.generation, characteristics.notify);
426
479
  connectedDevices.set(uuid, peripheral);
427
480
  deviceCharacteristics.set(uuid, characteristics);
481
+ trackUnexpectedDisconnect(uuid, peripheral);
428
482
  } catch (error) {
483
+ removeDisconnectListener(uuid);
429
484
  clearNotificationState(uuid, `BLE notification subscription failed: ${uuid}`);
430
485
  if (characteristics) {
431
486
  characteristics.notify.removeAllListeners('data');
@@ -442,6 +497,10 @@ export function createNobleBlePlugin(): LowlevelTransportSharedPlugin {
442
497
  await disconnectDevice(uuid);
443
498
  },
444
499
 
500
+ getProtocolV2PacketCapacity(uuid: string) {
501
+ return resolveNobleProtocolV2PacketCapacity(connectedDevices.get(uuid)?.mtu);
502
+ },
503
+
445
504
  async send(uuid: string, data: string, options?: { withoutResponse?: boolean }) {
446
505
  const characteristics = deviceCharacteristics.get(uuid);
447
506
  if (!characteristics) {
@@ -453,8 +512,9 @@ export function createNobleBlePlugin(): LowlevelTransportSharedPlugin {
453
512
 
454
513
  const buffer = Buffer.from(data, 'hex');
455
514
  const withoutResponse = options?.withoutResponse ?? true;
456
- for (let offset = 0; offset < buffer.length; offset += BLE_PACKET_SIZE) {
457
- const chunk = buffer.subarray(offset, Math.min(offset + BLE_PACKET_SIZE, buffer.length));
515
+ const packetCapacity = resolveNobleProtocolV2PacketCapacity(connectedDevices.get(uuid)?.mtu);
516
+ for (let offset = 0; offset < buffer.length; offset += packetCapacity) {
517
+ const chunk = buffer.subarray(offset, Math.min(offset + packetCapacity, buffer.length));
458
518
  await writeCharacteristic(characteristics.write, chunk, withoutResponse);
459
519
  }
460
520
  },