@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('--
|
|
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
|
|
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
|
-
|
|
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
|
|
942
|
-
const matches =
|
|
943
|
-
? devices.filter(device => device.name
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
35
|
-
"@onekeyfe/hd-core": "1.2.0
|
|
36
|
-
"@onekeyfe/hd-shared": "1.2.0
|
|
37
|
-
"@onekeyfe/hd-transport-usb": "1.2.0
|
|
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": "
|
|
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
|
-
|
|
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) =>
|
|
@@ -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('--
|
|
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
|
|
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
|
-
|
|
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
|
|
1229
|
-
const matches =
|
|
1230
|
-
? devices.filter(
|
|
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
|
-
|
|
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
|
-
|
|
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');
|