@onekeyfe/hardware-cli 1.2.0-alpha.99 → 1.2.0

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.js CHANGED
@@ -62,15 +62,11 @@ program
62
62
  program
63
63
  .command('upload-wallpaper')
64
64
  .description('Upload and activate a Pro2 wallpaper')
65
- .requiredOption('--rgba <path>', '604x1024 raw RGBA file')
65
+ .requiredOption('--jpeg <path>', '604x1024 JPEG file')
66
66
  .option('--file-name <name>', 'Device wallpaper file name', 'wallpaper-cli')
67
67
  .option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
68
68
  .action(opts => runCommand({}, async ({ sdk, globalOpts, params }) => {
69
- const rgba = readBinaryParam(opts.rgba);
70
- const expectedBytes = 604 * 1024 * 4;
71
- if (rgba.byteLength !== expectedBytes) {
72
- throw new Error(`Invalid RGBA size: expected ${expectedBytes} bytes, received ${rgba.byteLength}`);
73
- }
69
+ const jpegBase64 = (0, node_fs_1.readFileSync)(opts.jpeg).toString('base64');
74
70
  let transferStartedAt;
75
71
  let transferEndedAt;
76
72
  let lastProgress = -1;
@@ -111,9 +107,7 @@ program
111
107
  try {
112
108
  result = await sdk.deviceUploadWallpaper(globalOpts.connectId, {
113
109
  ...params,
114
- width: 604,
115
- height: 1024,
116
- rgba,
110
+ jpegBase64,
117
111
  fileName: opts.fileName,
118
112
  chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
119
113
  });
@@ -938,17 +932,18 @@ async function resolveLegacyFirmwareConnectId(sdk, explicitConnectId, deviceName
938
932
  throw new Error('Unable to scan BLE devices');
939
933
  }
940
934
  const devices = searchResult.payload;
941
- const normalizedName = deviceName?.trim().toLowerCase();
942
- const matches = normalizedName
943
- ? devices.filter(device => device.name?.trim().toLowerCase() === normalizedName)
935
+ const requestedName = deviceName?.trim();
936
+ const matches = requestedName
937
+ ? devices.filter(device => (0, hd_shared_1.isSameOnekeyBleName)(device.name, requestedName) ||
938
+ device.name?.trim().toLowerCase() === requestedName.toLowerCase())
944
939
  : devices.filter(device => device.deviceType?.toLowerCase() === 'classic');
945
940
  if (matches.length === 0) {
946
- throw new Error(normalizedName
941
+ throw new Error(requestedName
947
942
  ? `BLE device not found by name: ${deviceName}`
948
943
  : 'No Classic/Pure BLE device found');
949
944
  }
950
945
  if (matches.length > 1) {
951
- throw new Error(normalizedName
946
+ throw new Error(requestedName
952
947
  ? `Multiple BLE devices found by name: ${deviceName}`
953
948
  : 'Multiple Classic/Pure BLE devices found; specify --device-name');
954
949
  }
@@ -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.0-alpha.99",
3
+ "version": "1.2.0",
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.0-alpha.99",
35
- "@onekeyfe/hd-core": "1.2.0-alpha.99",
36
- "@onekeyfe/hd-shared": "1.2.0-alpha.99",
37
- "@onekeyfe/hd-transport-usb": "1.2.0-alpha.99",
34
+ "@onekeyfe/hd-common-connect-sdk": "1.2.0",
35
+ "@onekeyfe/hd-core": "1.2.0",
36
+ "@onekeyfe/hd-shared": "1.2.0",
37
+ "@onekeyfe/hd-transport-usb": "1.2.0",
38
38
  "@stoprocent/noble": "2.3.16",
39
39
  "commander": "^12.0.0"
40
40
  },
41
- "gitHead": "8924c0e509f84675a2c33c0062166967f5bcb2d0"
41
+ "gitHead": "cc51df0a415bead21304ad1d8bc92b5312c91344"
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) =>
@@ -6,6 +6,7 @@ describe('upload-wallpaper CLI command', () => {
6
6
 
7
7
  expect(command).toBeDefined();
8
8
  expect(command?.description()).toBe('Upload and activate a Pro2 wallpaper');
9
+ expect(command?.options.some(option => option.long === '--jpeg' && option.required)).toBe(true);
9
10
  });
10
11
 
11
12
  test('reports effective transfer speed from encoded bytes and elapsed time', () => {
package/src/cli.ts CHANGED
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
  import { Command } from 'commander';
4
4
  import { UI_EVENT, UI_REQUEST, getDeviceType } from '@onekeyfe/hd-core';
5
- import { EDeviceType } from '@onekeyfe/hd-shared';
5
+ import { EDeviceType, isSameOnekeyBleName } from '@onekeyfe/hd-shared';
6
6
 
7
7
  import {
8
8
  resolveBatchGetAddress,
@@ -99,18 +99,12 @@ program
99
99
  program
100
100
  .command('upload-wallpaper')
101
101
  .description('Upload and activate a Pro2 wallpaper')
102
- .requiredOption('--rgba <path>', '604x1024 raw RGBA file')
102
+ .requiredOption('--jpeg <path>', '604x1024 JPEG file')
103
103
  .option('--file-name <name>', 'Device wallpaper file name', 'wallpaper-cli')
104
104
  .option('--chunk-size <bytes>', 'Transfer chunk size in bytes')
105
105
  .action(opts =>
106
106
  runCommand({}, async ({ sdk, globalOpts, params }) => {
107
- const rgba = readBinaryParam(opts.rgba);
108
- const expectedBytes = 604 * 1024 * 4;
109
- if (rgba.byteLength !== expectedBytes) {
110
- throw new Error(
111
- `Invalid RGBA size: expected ${expectedBytes} bytes, received ${rgba.byteLength}`
112
- );
113
- }
107
+ const jpegBase64 = readFileSync(opts.jpeg).toString('base64');
114
108
 
115
109
  let transferStartedAt: number | undefined;
116
110
  let transferEndedAt: number | undefined;
@@ -159,9 +153,7 @@ program
159
153
  try {
160
154
  result = await sdk.deviceUploadWallpaper(globalOpts.connectId, {
161
155
  ...params,
162
- width: 604,
163
- height: 1024,
164
- rgba,
156
+ jpegBase64,
165
157
  fileName: opts.fileName,
166
158
  chunkSize: opts.chunkSize ? safeParseInt(opts.chunkSize, '--chunk-size') : undefined,
167
159
  });
@@ -1225,21 +1217,25 @@ async function resolveLegacyFirmwareConnectId(
1225
1217
  }
1226
1218
 
1227
1219
  const devices = searchResult.payload as EnrichedSearchDevice[];
1228
- const normalizedName = deviceName?.trim().toLowerCase();
1229
- const matches = normalizedName
1230
- ? devices.filter(device => device.name?.trim().toLowerCase() === normalizedName)
1220
+ const requestedName = deviceName?.trim();
1221
+ const matches = requestedName
1222
+ ? devices.filter(
1223
+ device =>
1224
+ isSameOnekeyBleName(device.name, requestedName) ||
1225
+ device.name?.trim().toLowerCase() === requestedName.toLowerCase()
1226
+ )
1231
1227
  : devices.filter(device => device.deviceType?.toLowerCase() === 'classic');
1232
1228
 
1233
1229
  if (matches.length === 0) {
1234
1230
  throw new Error(
1235
- normalizedName
1231
+ requestedName
1236
1232
  ? `BLE device not found by name: ${deviceName}`
1237
1233
  : 'No Classic/Pure BLE device found'
1238
1234
  );
1239
1235
  }
1240
1236
  if (matches.length > 1) {
1241
1237
  throw new Error(
1242
- normalizedName
1238
+ requestedName
1243
1239
  ? `Multiple BLE devices found by name: ${deviceName}`
1244
1240
  : 'Multiple Classic/Pure BLE devices found; specify --device-name'
1245
1241
  );
@@ -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');