@onekeyfe/hardware-cli 1.2.0-alpha.2 → 1.2.0-alpha.4
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 +293 -9
- package/dist/sdk.d.ts +2 -0
- package/dist/sdk.js +5 -3
- package/dist/transports/nobleBlePlugin.d.ts +2 -0
- package/dist/transports/nobleBlePlugin.js +328 -0
- package/package.json +7 -6
- package/src/cli.ts +423 -15
- package/src/sdk.ts +11 -3
- package/src/transports/nobleBlePlugin.ts +415 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createNobleBlePlugin = void 0;
|
|
4
|
+
const hd_shared_1 = require("@onekeyfe/hd-shared");
|
|
5
|
+
const ONEKEY_SERVICE_UUIDS = [hd_shared_1.ONEKEY_SERVICE_UUID];
|
|
6
|
+
const PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS = new Set(['fffd']);
|
|
7
|
+
const NORMALIZED_WRITE_UUID = '0002';
|
|
8
|
+
const NORMALIZED_NOTIFY_UUID = '0003';
|
|
9
|
+
const NORMALIZED_ONEKEY_SERVICE_UUIDS = new Set([
|
|
10
|
+
...ONEKEY_SERVICE_UUIDS.map(uuid => getBleUuidKey(uuid)),
|
|
11
|
+
'0001',
|
|
12
|
+
]);
|
|
13
|
+
const BLUETOOTH_INIT_TIMEOUT = 10000;
|
|
14
|
+
const DEVICE_SCAN_TIMEOUT = 8000;
|
|
15
|
+
const CONNECTION_TIMEOUT = 8000;
|
|
16
|
+
const SERVICE_DISCOVERY_TIMEOUT = 10000;
|
|
17
|
+
const BLE_PACKET_SIZE = 192;
|
|
18
|
+
const BLE_WRITE_DELAY = 5;
|
|
19
|
+
const BLE_ENCRYPTION_ERROR_PATTERNS = [/encryption is insufficient/i, /insufficient encryption/i];
|
|
20
|
+
let noble = null;
|
|
21
|
+
let nobleReadyPromise = null;
|
|
22
|
+
const discoveredDevices = new Map();
|
|
23
|
+
const connectedDevices = new Map();
|
|
24
|
+
const deviceCharacteristics = new Map();
|
|
25
|
+
const notificationQueue = [];
|
|
26
|
+
const pendingReceivers = [];
|
|
27
|
+
function getBleUuidKey(uuid) {
|
|
28
|
+
const normalized = (uuid ?? '').replace(/-/g, '').toLowerCase();
|
|
29
|
+
return normalized.length >= 8 ? normalized.substring(4, 8) : normalized;
|
|
30
|
+
}
|
|
31
|
+
function isGenericBleService(uuid) {
|
|
32
|
+
return ['1800', '1801', '180a', '180f'].includes(getBleUuidKey(uuid));
|
|
33
|
+
}
|
|
34
|
+
function hasOneKeyAdvertisementService(peripheral) {
|
|
35
|
+
const serviceUuids = peripheral.advertisement?.serviceUuids ?? [];
|
|
36
|
+
return serviceUuids.some(uuid => {
|
|
37
|
+
const uuidKey = getBleUuidKey(uuid);
|
|
38
|
+
return (NORMALIZED_ONEKEY_SERVICE_UUIDS.has(uuidKey) ||
|
|
39
|
+
PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(uuidKey));
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function isOneKeyPeripheral(peripheral) {
|
|
43
|
+
const deviceName = peripheral.advertisement?.localName || null;
|
|
44
|
+
return (0, hd_shared_1.isOnekeyDevice)(deviceName, peripheral.id) || hasOneKeyAdvertisementService(peripheral);
|
|
45
|
+
}
|
|
46
|
+
function enqueueNotification(data) {
|
|
47
|
+
const hex = data.toString('hex');
|
|
48
|
+
const receiver = pendingReceivers.shift();
|
|
49
|
+
if (receiver) {
|
|
50
|
+
receiver(hex);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
notificationQueue.push(hex);
|
|
54
|
+
}
|
|
55
|
+
async function initializeNoble() {
|
|
56
|
+
if (!noble) {
|
|
57
|
+
try {
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
|
|
59
|
+
noble = require('@stoprocent/noble');
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
throw hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BleUnsupported, error instanceof Error ? error.message : String(error));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (noble.state === 'poweredOn')
|
|
66
|
+
return;
|
|
67
|
+
if (nobleReadyPromise) {
|
|
68
|
+
await nobleReadyPromise;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
nobleReadyPromise = new Promise((resolve, reject) => {
|
|
72
|
+
const timeout = setTimeout(() => {
|
|
73
|
+
noble?.removeListener('stateChange', onStateChange);
|
|
74
|
+
reject(hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BlePoweredOff, 'Bluetooth is not powered on'));
|
|
75
|
+
}, BLUETOOTH_INIT_TIMEOUT);
|
|
76
|
+
const onStateChange = (state) => {
|
|
77
|
+
if (state === 'poweredOn') {
|
|
78
|
+
clearTimeout(timeout);
|
|
79
|
+
noble?.removeListener('stateChange', onStateChange);
|
|
80
|
+
resolve();
|
|
81
|
+
}
|
|
82
|
+
else if (state === 'unsupported') {
|
|
83
|
+
clearTimeout(timeout);
|
|
84
|
+
noble?.removeListener('stateChange', onStateChange);
|
|
85
|
+
reject(hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BleUnsupported));
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
noble?.on('stateChange', onStateChange);
|
|
89
|
+
}).finally(() => {
|
|
90
|
+
nobleReadyPromise = null;
|
|
91
|
+
});
|
|
92
|
+
await nobleReadyPromise;
|
|
93
|
+
}
|
|
94
|
+
function stopScanning() {
|
|
95
|
+
try {
|
|
96
|
+
noble?.stopScanning();
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// ignore best-effort scan cleanup
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async function scanDevices(targetDeviceId) {
|
|
103
|
+
await initializeNoble();
|
|
104
|
+
if (!noble) {
|
|
105
|
+
throw hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.RuntimeError, 'Noble not initialized');
|
|
106
|
+
}
|
|
107
|
+
if (!targetDeviceId) {
|
|
108
|
+
discoveredDevices.clear();
|
|
109
|
+
}
|
|
110
|
+
const nobleInstance = noble;
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
const found = new Map();
|
|
113
|
+
const cleanup = () => {
|
|
114
|
+
clearTimeout(timeout);
|
|
115
|
+
nobleInstance.removeListener('discover', onDiscover);
|
|
116
|
+
stopScanning();
|
|
117
|
+
};
|
|
118
|
+
const finish = () => {
|
|
119
|
+
cleanup();
|
|
120
|
+
resolve([...found.values()]);
|
|
121
|
+
};
|
|
122
|
+
const onDiscover = (peripheral) => {
|
|
123
|
+
if (targetDeviceId && peripheral.id !== targetDeviceId)
|
|
124
|
+
return;
|
|
125
|
+
if (!targetDeviceId && !isOneKeyPeripheral(peripheral))
|
|
126
|
+
return;
|
|
127
|
+
discoveredDevices.set(peripheral.id, peripheral);
|
|
128
|
+
found.set(peripheral.id, peripheral);
|
|
129
|
+
if (targetDeviceId) {
|
|
130
|
+
finish();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const timeout = setTimeout(finish, DEVICE_SCAN_TIMEOUT);
|
|
134
|
+
nobleInstance.on('discover', onDiscover);
|
|
135
|
+
nobleInstance.startScanning([], false, (error) => {
|
|
136
|
+
if (error) {
|
|
137
|
+
cleanup();
|
|
138
|
+
reject(hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BleScanError, error.message));
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
function connectPeripheral(peripheral) {
|
|
144
|
+
if (peripheral.state === 'connected')
|
|
145
|
+
return Promise.resolve();
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
const timeout = setTimeout(() => {
|
|
148
|
+
reject(hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BleConnectedError, 'Connection timeout'));
|
|
149
|
+
}, CONNECTION_TIMEOUT);
|
|
150
|
+
peripheral.connect((error) => {
|
|
151
|
+
clearTimeout(timeout);
|
|
152
|
+
if (error) {
|
|
153
|
+
reject(hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BleConnectedError, error.message));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
resolve();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
async function discoverCharacteristics(peripheral) {
|
|
161
|
+
const services = await new Promise((resolve, reject) => {
|
|
162
|
+
const timeout = setTimeout(() => {
|
|
163
|
+
reject(hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BleServiceNotFound, 'Service discovery timeout'));
|
|
164
|
+
}, SERVICE_DISCOVERY_TIMEOUT);
|
|
165
|
+
peripheral.discoverServices([], (error, discoveredServices) => {
|
|
166
|
+
clearTimeout(timeout);
|
|
167
|
+
if (error) {
|
|
168
|
+
reject(hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BleServiceNotFound, error.message));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
resolve(discoveredServices);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
let service = services.find(s => NORMALIZED_ONEKEY_SERVICE_UUIDS.has(getBleUuidKey(s.uuid)));
|
|
175
|
+
if (!service) {
|
|
176
|
+
service =
|
|
177
|
+
services.find(s => PRO2_ADVERTISEMENT_SERVICE_UUID_KEYS.has(getBleUuidKey(s.uuid))) ||
|
|
178
|
+
services.find(s => !isGenericBleService(s.uuid)) ||
|
|
179
|
+
services[0];
|
|
180
|
+
}
|
|
181
|
+
if (!service) {
|
|
182
|
+
throw hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BleServiceNotFound, 'No BLE service found');
|
|
183
|
+
}
|
|
184
|
+
const selectedService = service;
|
|
185
|
+
const characteristics = await new Promise((resolve, reject) => {
|
|
186
|
+
selectedService.discoverCharacteristics([], (error, discoveredCharacteristics) => {
|
|
187
|
+
if (error) {
|
|
188
|
+
reject(hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BleCharacteristicNotFound, error.message));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
resolve(discoveredCharacteristics);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
let writeCharacteristic;
|
|
195
|
+
let notifyCharacteristic;
|
|
196
|
+
for (const characteristic of characteristics) {
|
|
197
|
+
const uuidKey = getBleUuidKey(characteristic.uuid);
|
|
198
|
+
if (uuidKey === NORMALIZED_WRITE_UUID) {
|
|
199
|
+
writeCharacteristic = characteristic;
|
|
200
|
+
}
|
|
201
|
+
else if (uuidKey === NORMALIZED_NOTIFY_UUID) {
|
|
202
|
+
notifyCharacteristic = characteristic;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (!writeCharacteristic || !notifyCharacteristic) {
|
|
206
|
+
throw hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BleCharacteristicNotFound, 'Required OneKey BLE characteristics not found');
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
write: writeCharacteristic,
|
|
210
|
+
notify: notifyCharacteristic,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function subscribeNotifications(deviceId, notifyCharacteristic) {
|
|
214
|
+
return new Promise(resolve => {
|
|
215
|
+
notifyCharacteristic.unsubscribe(() => resolve());
|
|
216
|
+
})
|
|
217
|
+
.then(() => new Promise((resolve, reject) => {
|
|
218
|
+
notifyCharacteristic.subscribe((error) => {
|
|
219
|
+
if (error) {
|
|
220
|
+
const errorMessage = error.message || String(error);
|
|
221
|
+
if (BLE_ENCRYPTION_ERROR_PATTERNS.some(pattern => pattern.test(errorMessage))) {
|
|
222
|
+
reject(hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BleDeviceNotBonded, `BLE device ${deviceId} is not paired or the encrypted link is not ready: ${errorMessage}`));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
reject(hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BleCharacteristicNotifyChangeFailure, `Failed to subscribe notifications for ${deviceId}: ${errorMessage}`));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
resolve();
|
|
229
|
+
});
|
|
230
|
+
}))
|
|
231
|
+
.then(() => {
|
|
232
|
+
notifyCharacteristic.removeAllListeners('data');
|
|
233
|
+
notifyCharacteristic.on('data', enqueueNotification);
|
|
234
|
+
})
|
|
235
|
+
.catch(error => {
|
|
236
|
+
notifyCharacteristic.removeAllListeners('data');
|
|
237
|
+
if (error) {
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
throw hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BleCharacteristicNotifyChangeFailure);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
function writeCharacteristic(characteristic, buffer) {
|
|
244
|
+
return new Promise((resolve, reject) => {
|
|
245
|
+
characteristic.write(buffer, true, (error) => {
|
|
246
|
+
if (error) {
|
|
247
|
+
reject(error);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
resolve();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
async function disconnectDevice(uuid) {
|
|
255
|
+
const peripheral = connectedDevices.get(uuid);
|
|
256
|
+
const characteristics = deviceCharacteristics.get(uuid);
|
|
257
|
+
if (characteristics) {
|
|
258
|
+
characteristics.notify.removeAllListeners('data');
|
|
259
|
+
await new Promise(resolve => {
|
|
260
|
+
characteristics.notify.unsubscribe(() => resolve());
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
connectedDevices.delete(uuid);
|
|
264
|
+
deviceCharacteristics.delete(uuid);
|
|
265
|
+
notificationQueue.length = 0;
|
|
266
|
+
pendingReceivers.splice(0).forEach(resolve => resolve(''));
|
|
267
|
+
if (!peripheral || peripheral.state === 'disconnected')
|
|
268
|
+
return;
|
|
269
|
+
await new Promise(resolve => {
|
|
270
|
+
peripheral.disconnect(() => resolve());
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
function createNobleBlePlugin() {
|
|
274
|
+
return {
|
|
275
|
+
version: 'OneKey-CLI-Noble-1.0',
|
|
276
|
+
async init() {
|
|
277
|
+
await initializeNoble();
|
|
278
|
+
},
|
|
279
|
+
async enumerate() {
|
|
280
|
+
const devices = await scanDevices();
|
|
281
|
+
return devices.map(device => ({
|
|
282
|
+
commType: 'ble',
|
|
283
|
+
id: device.id,
|
|
284
|
+
name: device.advertisement?.localName || 'Unknown BLE Device',
|
|
285
|
+
}));
|
|
286
|
+
},
|
|
287
|
+
async connect(uuid) {
|
|
288
|
+
let peripheral = discoveredDevices.get(uuid);
|
|
289
|
+
if (!peripheral) {
|
|
290
|
+
[peripheral] = await scanDevices(uuid);
|
|
291
|
+
}
|
|
292
|
+
if (!peripheral) {
|
|
293
|
+
throw hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.DeviceNotFound, `BLE device not found: ${uuid}`);
|
|
294
|
+
}
|
|
295
|
+
await connectPeripheral(peripheral);
|
|
296
|
+
const characteristics = await discoverCharacteristics(peripheral);
|
|
297
|
+
await subscribeNotifications(uuid, characteristics.notify);
|
|
298
|
+
connectedDevices.set(uuid, peripheral);
|
|
299
|
+
deviceCharacteristics.set(uuid, characteristics);
|
|
300
|
+
},
|
|
301
|
+
async disconnect(uuid) {
|
|
302
|
+
await disconnectDevice(uuid);
|
|
303
|
+
},
|
|
304
|
+
async send(uuid, data) {
|
|
305
|
+
const characteristics = deviceCharacteristics.get(uuid);
|
|
306
|
+
if (!characteristics) {
|
|
307
|
+
throw hd_shared_1.ERRORS.TypedError(hd_shared_1.HardwareErrorCode.BleCharacteristicNotFound, `BLE device is not connected: ${uuid}`);
|
|
308
|
+
}
|
|
309
|
+
const buffer = Buffer.from(data, 'hex');
|
|
310
|
+
for (let offset = 0; offset < buffer.length; offset += BLE_PACKET_SIZE) {
|
|
311
|
+
const chunk = buffer.subarray(offset, Math.min(offset + BLE_PACKET_SIZE, buffer.length));
|
|
312
|
+
await writeCharacteristic(characteristics.write, chunk);
|
|
313
|
+
if (offset + BLE_PACKET_SIZE < buffer.length) {
|
|
314
|
+
await (0, hd_shared_1.wait)(BLE_WRITE_DELAY);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
async receive() {
|
|
319
|
+
const queued = notificationQueue.shift();
|
|
320
|
+
if (queued !== undefined)
|
|
321
|
+
return queued;
|
|
322
|
+
return new Promise(resolve => {
|
|
323
|
+
pendingReceivers.push(resolve);
|
|
324
|
+
});
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
exports.createNobleBlePlugin = createNobleBlePlugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hardware-cli",
|
|
3
|
-
"version": "1.2.0-alpha.
|
|
3
|
+
"version": "1.2.0-alpha.4",
|
|
4
4
|
"description": "OneKey hardware wallet CLI for testing device communication",
|
|
5
5
|
"author": "OneKey",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -30,11 +30,12 @@
|
|
|
30
30
|
"test": "jest"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.
|
|
34
|
-
"@onekeyfe/hd-core": "1.2.0-alpha.
|
|
35
|
-
"@onekeyfe/hd-shared": "1.2.0-alpha.
|
|
36
|
-
"@onekeyfe/hd-transport-usb": "1.2.0-alpha.
|
|
33
|
+
"@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.4",
|
|
34
|
+
"@onekeyfe/hd-core": "1.2.0-alpha.4",
|
|
35
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.4",
|
|
36
|
+
"@onekeyfe/hd-transport-usb": "1.2.0-alpha.4",
|
|
37
|
+
"@stoprocent/noble": "2.3.16",
|
|
37
38
|
"commander": "^12.0.0"
|
|
38
39
|
},
|
|
39
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "6f83d130f022a75003385bc4b2386cd0b2b539b5"
|
|
40
41
|
}
|