@onekeyfe/hardware-cli 1.2.0-alpha.1 → 1.2.0-alpha.100
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.d.ts +83 -1
- package/dist/cli.js +501 -142
- package/dist/deviceSelection.d.ts +3 -0
- package/dist/deviceSelection.js +11 -0
- package/dist/deviceStateCommands.d.ts +19 -0
- package/dist/deviceStateCommands.js +62 -0
- package/dist/pinentry.d.ts +1 -0
- package/dist/sdk.d.ts +12 -2
- package/dist/sdk.js +22 -11
- package/dist/session.d.ts +2 -9
- package/dist/session.js +3 -22
- package/dist/transports/nobleBlePlugin.d.ts +2 -0
- package/dist/transports/nobleBlePlugin.js +371 -0
- package/package.json +8 -6
- package/src/__tests__/cli-version.test.ts +8 -0
- package/src/__tests__/device-selection.test.ts +29 -0
- package/src/__tests__/device-state-commands.test.ts +118 -0
- package/src/__tests__/firmware-update-legacy-command.test.ts +18 -0
- package/src/__tests__/firmware-update-v4-command.test.ts +101 -0
- package/src/__tests__/noble-ble-plugin.test.ts +370 -0
- package/src/__tests__/wallet-session.test.ts +64 -0
- package/src/__tests__/wallpaper-upload-command.test.ts +46 -0
- package/src/cli.ts +687 -173
- package/src/deviceSelection.ts +13 -0
- package/src/deviceStateCommands.ts +71 -0
- package/src/pinentry.ts +1 -0
- package/src/sdk.ts +29 -13
- package/src/session.ts +2 -24
- package/src/transports/nobleBlePlugin.ts +487 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ERRORS,
|
|
3
|
+
HardwareErrorCode,
|
|
4
|
+
ONEKEY_NOTIFY_CHARACTERISTIC_UUID,
|
|
5
|
+
ONEKEY_SERVICE_UUID,
|
|
6
|
+
ONEKEY_WRITE_CHARACTERISTIC_UUID,
|
|
7
|
+
createKnownBleUuidAliases,
|
|
8
|
+
hasOnekeyCommunicationService,
|
|
9
|
+
isOnekeyBluetoothDevice,
|
|
10
|
+
matchesKnownBleUuid,
|
|
11
|
+
} from '@onekeyfe/hd-shared';
|
|
12
|
+
|
|
13
|
+
import type { LowLevelDevice, LowlevelTransportSharedPlugin } from '@onekeyfe/hd-transport';
|
|
14
|
+
import type { Characteristic, Peripheral, Service } from '@stoprocent/noble';
|
|
15
|
+
|
|
16
|
+
type NobleModule = {
|
|
17
|
+
state: string;
|
|
18
|
+
startScanning(
|
|
19
|
+
serviceUUIDs: string[],
|
|
20
|
+
allowDuplicates: boolean,
|
|
21
|
+
callback?: (error?: Error) => void
|
|
22
|
+
): void;
|
|
23
|
+
stopScanning(callback?: () => void): void;
|
|
24
|
+
on(event: 'stateChange', listener: (state: string) => void): void;
|
|
25
|
+
on(event: 'discover', listener: (peripheral: Peripheral) => void): void;
|
|
26
|
+
removeListener(event: 'stateChange', listener: (state: string) => void): void;
|
|
27
|
+
removeListener(event: 'discover', listener: (peripheral: Peripheral) => void): void;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type CharacteristicPair = {
|
|
31
|
+
write: Characteristic;
|
|
32
|
+
notify: Characteristic;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type NoblePendingReceiver = {
|
|
36
|
+
resolve: (data: string) => void;
|
|
37
|
+
reject: (error: Error) => void;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type NobleNotificationState = {
|
|
41
|
+
generation: number;
|
|
42
|
+
queue: string[];
|
|
43
|
+
pendingReceivers: Set<NoblePendingReceiver>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const ONEKEY_SERVICE_UUIDS = [ONEKEY_SERVICE_UUID];
|
|
47
|
+
const ONEKEY_SERVICE_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_SERVICE_UUID);
|
|
48
|
+
const ONEKEY_WRITE_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_WRITE_CHARACTERISTIC_UUID);
|
|
49
|
+
const ONEKEY_NOTIFY_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_NOTIFY_CHARACTERISTIC_UUID);
|
|
50
|
+
|
|
51
|
+
const BLUETOOTH_INIT_TIMEOUT = 10_000;
|
|
52
|
+
const DEVICE_SCAN_TIMEOUT = 8_000;
|
|
53
|
+
const CONNECTION_TIMEOUT = 8_000;
|
|
54
|
+
const SERVICE_DISCOVERY_TIMEOUT = 10_000;
|
|
55
|
+
const BLE_CLEANUP_TIMEOUT = 100;
|
|
56
|
+
const BLE_PACKET_SIZE = 192;
|
|
57
|
+
const BLE_ENCRYPTION_ERROR_PATTERNS = [/encryption is insufficient/i, /insufficient encryption/i];
|
|
58
|
+
|
|
59
|
+
let noble: NobleModule | null = null;
|
|
60
|
+
let nobleReadyPromise: Promise<void> | null = null;
|
|
61
|
+
const discoveredDevices = new Map<string, Peripheral>();
|
|
62
|
+
const connectedDevices = new Map<string, Peripheral>();
|
|
63
|
+
const deviceCharacteristics = new Map<string, CharacteristicPair>();
|
|
64
|
+
const notificationStates = new Map<string, NobleNotificationState>();
|
|
65
|
+
const notificationGenerations = new Map<string, number>();
|
|
66
|
+
|
|
67
|
+
function isOneKeyPeripheral(peripheral: Peripheral) {
|
|
68
|
+
const serviceUuids = peripheral.advertisement?.serviceUuids;
|
|
69
|
+
return (
|
|
70
|
+
hasOnekeyCommunicationService(serviceUuids) &&
|
|
71
|
+
isOnekeyBluetoothDevice({
|
|
72
|
+
id: peripheral.id,
|
|
73
|
+
localName: peripheral.advertisement?.localName,
|
|
74
|
+
serviceUuids,
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function enqueueNotification(deviceId: string, generation: number, data: Buffer) {
|
|
80
|
+
const state = notificationStates.get(deviceId);
|
|
81
|
+
if (!state || state.generation !== generation) return;
|
|
82
|
+
|
|
83
|
+
const hex = data.toString('hex');
|
|
84
|
+
const [receiver] = state.pendingReceivers;
|
|
85
|
+
if (receiver) {
|
|
86
|
+
state.pendingReceivers.delete(receiver);
|
|
87
|
+
receiver.resolve(hex);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
state.queue.push(hex);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createNotificationState(deviceId: string) {
|
|
94
|
+
const existing = notificationStates.get(deviceId);
|
|
95
|
+
if (existing) {
|
|
96
|
+
const error = new Error(`BLE notification state replaced for ${deviceId}`);
|
|
97
|
+
existing.pendingReceivers.forEach(receiver => receiver.reject(error));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const generation = (notificationGenerations.get(deviceId) ?? 0) + 1;
|
|
101
|
+
notificationGenerations.set(deviceId, generation);
|
|
102
|
+
const state: NobleNotificationState = {
|
|
103
|
+
generation,
|
|
104
|
+
queue: [],
|
|
105
|
+
pendingReceivers: new Set(),
|
|
106
|
+
};
|
|
107
|
+
notificationStates.set(deviceId, state);
|
|
108
|
+
return state;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function clearNotificationState(deviceId: string, reason: string) {
|
|
112
|
+
const state = notificationStates.get(deviceId);
|
|
113
|
+
if (!state) return;
|
|
114
|
+
|
|
115
|
+
notificationStates.delete(deviceId);
|
|
116
|
+
const error = new Error(reason);
|
|
117
|
+
state.pendingReceivers.forEach(receiver => receiver.reject(error));
|
|
118
|
+
state.pendingReceivers.clear();
|
|
119
|
+
state.queue.length = 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function waitForNobleCleanup(registerCallback: (callback: () => void) => void) {
|
|
123
|
+
return new Promise<void>(resolve => {
|
|
124
|
+
let completed = false;
|
|
125
|
+
const complete = () => {
|
|
126
|
+
if (completed) return;
|
|
127
|
+
completed = true;
|
|
128
|
+
clearTimeout(timeout);
|
|
129
|
+
resolve();
|
|
130
|
+
};
|
|
131
|
+
const timeout = setTimeout(complete, BLE_CLEANUP_TIMEOUT);
|
|
132
|
+
try {
|
|
133
|
+
registerCallback(complete);
|
|
134
|
+
} catch {
|
|
135
|
+
complete();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function initializeNoble() {
|
|
141
|
+
if (!noble) {
|
|
142
|
+
try {
|
|
143
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
|
|
144
|
+
noble = require('@stoprocent/noble') as NobleModule;
|
|
145
|
+
} catch (error) {
|
|
146
|
+
throw ERRORS.TypedError(
|
|
147
|
+
HardwareErrorCode.BleUnsupported,
|
|
148
|
+
error instanceof Error ? error.message : String(error)
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (noble.state === 'poweredOn') return;
|
|
154
|
+
|
|
155
|
+
if (nobleReadyPromise) {
|
|
156
|
+
await nobleReadyPromise;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
nobleReadyPromise = new Promise<void>((resolve, reject) => {
|
|
161
|
+
const timeout = setTimeout(() => {
|
|
162
|
+
noble?.removeListener('stateChange', onStateChange);
|
|
163
|
+
reject(ERRORS.TypedError(HardwareErrorCode.BlePoweredOff, 'Bluetooth is not powered on'));
|
|
164
|
+
}, BLUETOOTH_INIT_TIMEOUT);
|
|
165
|
+
|
|
166
|
+
const onStateChange = (state: string) => {
|
|
167
|
+
if (state === 'poweredOn') {
|
|
168
|
+
clearTimeout(timeout);
|
|
169
|
+
noble?.removeListener('stateChange', onStateChange);
|
|
170
|
+
resolve();
|
|
171
|
+
} else if (state === 'unsupported') {
|
|
172
|
+
clearTimeout(timeout);
|
|
173
|
+
noble?.removeListener('stateChange', onStateChange);
|
|
174
|
+
reject(ERRORS.TypedError(HardwareErrorCode.BleUnsupported));
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
noble?.on('stateChange', onStateChange);
|
|
179
|
+
}).finally(() => {
|
|
180
|
+
nobleReadyPromise = null;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await nobleReadyPromise;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function stopScanning() {
|
|
187
|
+
try {
|
|
188
|
+
noble?.stopScanning();
|
|
189
|
+
} catch {
|
|
190
|
+
// ignore best-effort scan cleanup
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function scanDevices(targetDeviceId?: string) {
|
|
195
|
+
await initializeNoble();
|
|
196
|
+
if (!noble) {
|
|
197
|
+
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not initialized');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!targetDeviceId) {
|
|
201
|
+
discoveredDevices.clear();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const nobleInstance = noble;
|
|
205
|
+
return new Promise<Peripheral[]>((resolve, reject) => {
|
|
206
|
+
const found = new Map<string, Peripheral>();
|
|
207
|
+
|
|
208
|
+
const cleanup = () => {
|
|
209
|
+
clearTimeout(timeout);
|
|
210
|
+
nobleInstance.removeListener('discover', onDiscover);
|
|
211
|
+
stopScanning();
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const finish = () => {
|
|
215
|
+
cleanup();
|
|
216
|
+
resolve([...found.values()]);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const onDiscover = (peripheral: Peripheral) => {
|
|
220
|
+
if (targetDeviceId && peripheral.id !== targetDeviceId) return;
|
|
221
|
+
if (!isOneKeyPeripheral(peripheral)) return;
|
|
222
|
+
|
|
223
|
+
discoveredDevices.set(peripheral.id, peripheral);
|
|
224
|
+
found.set(peripheral.id, peripheral);
|
|
225
|
+
if (targetDeviceId) {
|
|
226
|
+
finish();
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const timeout = setTimeout(finish, DEVICE_SCAN_TIMEOUT);
|
|
231
|
+
nobleInstance.on('discover', onDiscover);
|
|
232
|
+
nobleInstance.startScanning([], false, (error?: Error) => {
|
|
233
|
+
if (error) {
|
|
234
|
+
cleanup();
|
|
235
|
+
reject(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.message));
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function connectPeripheral(peripheral: Peripheral) {
|
|
242
|
+
if (peripheral.state === 'connected') return Promise.resolve();
|
|
243
|
+
|
|
244
|
+
return new Promise<void>((resolve, reject) => {
|
|
245
|
+
const timeout = setTimeout(() => {
|
|
246
|
+
reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'Connection timeout'));
|
|
247
|
+
}, CONNECTION_TIMEOUT);
|
|
248
|
+
|
|
249
|
+
peripheral.connect((error?: Error) => {
|
|
250
|
+
clearTimeout(timeout);
|
|
251
|
+
if (error) {
|
|
252
|
+
reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError, error.message));
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
resolve();
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function discoverCharacteristics(peripheral: Peripheral): Promise<CharacteristicPair> {
|
|
261
|
+
const services = await new Promise<Service[]>((resolve, reject) => {
|
|
262
|
+
const timeout = setTimeout(() => {
|
|
263
|
+
reject(ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'Service discovery timeout'));
|
|
264
|
+
}, SERVICE_DISCOVERY_TIMEOUT);
|
|
265
|
+
|
|
266
|
+
peripheral.discoverServices([], (error, discoveredServices) => {
|
|
267
|
+
clearTimeout(timeout);
|
|
268
|
+
if (error) {
|
|
269
|
+
reject(ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, error.message));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
resolve(discoveredServices);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const service = services.find(s => matchesKnownBleUuid(s.uuid, ONEKEY_SERVICE_UUID_ALIASES));
|
|
277
|
+
if (!service) {
|
|
278
|
+
throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No BLE service found');
|
|
279
|
+
}
|
|
280
|
+
const selectedService = service;
|
|
281
|
+
|
|
282
|
+
const characteristics = await new Promise<Characteristic[]>((resolve, reject) => {
|
|
283
|
+
selectedService.discoverCharacteristics([], (error, discoveredCharacteristics) => {
|
|
284
|
+
if (error) {
|
|
285
|
+
reject(ERRORS.TypedError(HardwareErrorCode.BleCharacteristicNotFound, error.message));
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
resolve(discoveredCharacteristics);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
let writeCharacteristic: Characteristic | undefined;
|
|
293
|
+
let notifyCharacteristic: Characteristic | undefined;
|
|
294
|
+
for (const characteristic of characteristics) {
|
|
295
|
+
if (matchesKnownBleUuid(characteristic.uuid, ONEKEY_WRITE_UUID_ALIASES)) {
|
|
296
|
+
writeCharacteristic = characteristic;
|
|
297
|
+
} else if (matchesKnownBleUuid(characteristic.uuid, ONEKEY_NOTIFY_UUID_ALIASES)) {
|
|
298
|
+
notifyCharacteristic = characteristic;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (!writeCharacteristic || !notifyCharacteristic) {
|
|
303
|
+
throw ERRORS.TypedError(
|
|
304
|
+
HardwareErrorCode.BleCharacteristicNotFound,
|
|
305
|
+
'Required OneKey BLE characteristics not found'
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
write: writeCharacteristic,
|
|
311
|
+
notify: notifyCharacteristic,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function subscribeNotifications(
|
|
316
|
+
deviceId: string,
|
|
317
|
+
generation: number,
|
|
318
|
+
notifyCharacteristic: Characteristic
|
|
319
|
+
) {
|
|
320
|
+
return waitForNobleCleanup(callback => notifyCharacteristic.unsubscribe(callback))
|
|
321
|
+
.then(
|
|
322
|
+
() =>
|
|
323
|
+
new Promise<void>((resolve, reject) => {
|
|
324
|
+
notifyCharacteristic.subscribe((error?: Error) => {
|
|
325
|
+
if (error) {
|
|
326
|
+
const errorMessage = error.message || String(error);
|
|
327
|
+
if (BLE_ENCRYPTION_ERROR_PATTERNS.some(pattern => pattern.test(errorMessage))) {
|
|
328
|
+
reject(
|
|
329
|
+
ERRORS.TypedError(
|
|
330
|
+
HardwareErrorCode.BleDeviceNotBonded,
|
|
331
|
+
`BLE device ${deviceId} is not paired or the encrypted link is not ready: ${errorMessage}`
|
|
332
|
+
)
|
|
333
|
+
);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
reject(
|
|
337
|
+
ERRORS.TypedError(
|
|
338
|
+
HardwareErrorCode.BleCharacteristicNotifyChangeFailure,
|
|
339
|
+
`Failed to subscribe notifications for ${deviceId}: ${errorMessage}`
|
|
340
|
+
)
|
|
341
|
+
);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
resolve();
|
|
345
|
+
});
|
|
346
|
+
})
|
|
347
|
+
)
|
|
348
|
+
.then(() => {
|
|
349
|
+
notifyCharacteristic.removeAllListeners('data');
|
|
350
|
+
notifyCharacteristic.on('data', data => enqueueNotification(deviceId, generation, data));
|
|
351
|
+
})
|
|
352
|
+
.catch(error => {
|
|
353
|
+
notifyCharacteristic.removeAllListeners('data');
|
|
354
|
+
if (error) {
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
throw ERRORS.TypedError(HardwareErrorCode.BleCharacteristicNotifyChangeFailure);
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function writeCharacteristic(
|
|
362
|
+
characteristic: Characteristic,
|
|
363
|
+
buffer: Buffer,
|
|
364
|
+
withoutResponse: boolean
|
|
365
|
+
) {
|
|
366
|
+
return new Promise<void>((resolve, reject) => {
|
|
367
|
+
characteristic.write(buffer, withoutResponse, (error?: Error) => {
|
|
368
|
+
if (error) {
|
|
369
|
+
reject(error);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
resolve();
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async function disconnectDevice(uuid: string) {
|
|
378
|
+
const peripheral = connectedDevices.get(uuid);
|
|
379
|
+
const characteristics = deviceCharacteristics.get(uuid);
|
|
380
|
+
clearNotificationState(uuid, `BLE device disconnected: ${uuid}`);
|
|
381
|
+
if (characteristics) {
|
|
382
|
+
characteristics.notify.removeAllListeners('data');
|
|
383
|
+
await waitForNobleCleanup(callback => characteristics.notify.unsubscribe(callback));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
connectedDevices.delete(uuid);
|
|
387
|
+
deviceCharacteristics.delete(uuid);
|
|
388
|
+
|
|
389
|
+
if (!peripheral || peripheral.state === 'disconnected') return;
|
|
390
|
+
|
|
391
|
+
await waitForNobleCleanup(callback => peripheral.disconnect(callback));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function createNobleBlePlugin(): LowlevelTransportSharedPlugin {
|
|
395
|
+
return {
|
|
396
|
+
version: 'OneKey-CLI-Noble-1.0',
|
|
397
|
+
|
|
398
|
+
async init() {
|
|
399
|
+
await initializeNoble();
|
|
400
|
+
},
|
|
401
|
+
|
|
402
|
+
async enumerate(): Promise<LowLevelDevice[]> {
|
|
403
|
+
const devices = await scanDevices();
|
|
404
|
+
return devices.map(device => ({
|
|
405
|
+
commType: 'ble',
|
|
406
|
+
id: device.id,
|
|
407
|
+
name: device.advertisement?.localName || 'Unknown BLE Device',
|
|
408
|
+
}));
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
async connect(uuid: string) {
|
|
412
|
+
let peripheral = discoveredDevices.get(uuid);
|
|
413
|
+
if (!peripheral) {
|
|
414
|
+
[peripheral] = await scanDevices(uuid);
|
|
415
|
+
}
|
|
416
|
+
if (!peripheral) {
|
|
417
|
+
throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `BLE device not found: ${uuid}`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
await connectPeripheral(peripheral);
|
|
421
|
+
let characteristics: CharacteristicPair | undefined;
|
|
422
|
+
try {
|
|
423
|
+
characteristics = await discoverCharacteristics(peripheral);
|
|
424
|
+
const notificationState = createNotificationState(uuid);
|
|
425
|
+
await subscribeNotifications(uuid, notificationState.generation, characteristics.notify);
|
|
426
|
+
connectedDevices.set(uuid, peripheral);
|
|
427
|
+
deviceCharacteristics.set(uuid, characteristics);
|
|
428
|
+
} catch (error) {
|
|
429
|
+
clearNotificationState(uuid, `BLE notification subscription failed: ${uuid}`);
|
|
430
|
+
if (characteristics) {
|
|
431
|
+
characteristics.notify.removeAllListeners('data');
|
|
432
|
+
await waitForNobleCleanup(callback => characteristics?.notify.unsubscribe(callback));
|
|
433
|
+
}
|
|
434
|
+
if (peripheral.state !== 'disconnected') {
|
|
435
|
+
await waitForNobleCleanup(callback => peripheral?.disconnect(callback));
|
|
436
|
+
}
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
|
|
441
|
+
async disconnect(uuid: string) {
|
|
442
|
+
await disconnectDevice(uuid);
|
|
443
|
+
},
|
|
444
|
+
|
|
445
|
+
async send(uuid: string, data: string, options?: { withoutResponse?: boolean }) {
|
|
446
|
+
const characteristics = deviceCharacteristics.get(uuid);
|
|
447
|
+
if (!characteristics) {
|
|
448
|
+
throw ERRORS.TypedError(
|
|
449
|
+
HardwareErrorCode.BleCharacteristicNotFound,
|
|
450
|
+
`BLE device is not connected: ${uuid}`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const buffer = Buffer.from(data, 'hex');
|
|
455
|
+
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));
|
|
458
|
+
await writeCharacteristic(characteristics.write, chunk, withoutResponse);
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
async receive(uuid?: string) {
|
|
463
|
+
const resolvedUuid =
|
|
464
|
+
uuid ??
|
|
465
|
+
(notificationStates.size === 1 ? notificationStates.keys().next().value : undefined);
|
|
466
|
+
if (!resolvedUuid) {
|
|
467
|
+
throw ERRORS.TypedError(
|
|
468
|
+
HardwareErrorCode.RuntimeError,
|
|
469
|
+
'BLE receive requires a device UUID when multiple devices are connected'
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const state = notificationStates.get(resolvedUuid);
|
|
474
|
+
if (!state) {
|
|
475
|
+
throw ERRORS.TypedError(
|
|
476
|
+
HardwareErrorCode.TransportNotFound,
|
|
477
|
+
`BLE notification state not found: ${resolvedUuid}`
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
const queued = state.queue.shift();
|
|
481
|
+
if (queued !== undefined) return queued;
|
|
482
|
+
return new Promise<string>((resolve, reject) => {
|
|
483
|
+
state.pendingReceivers.add({ resolve, reject });
|
|
484
|
+
});
|
|
485
|
+
},
|
|
486
|
+
};
|
|
487
|
+
}
|