@ledgerhq/react-native-hw-transport-ble 6.28.3-nightly.0 → 6.28.4-next.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +20 -3
- package/lib/BleTransport.d.ts +67 -37
- package/lib/BleTransport.d.ts.map +1 -1
- package/lib/BleTransport.js +257 -198
- package/lib/BleTransport.js.map +1 -1
- package/lib/BleTransport.test.d.ts +2 -0
- package/lib/BleTransport.test.d.ts.map +1 -0
- package/lib/BleTransport.test.js +370 -0
- package/lib/BleTransport.test.js.map +1 -0
- package/lib/remapErrors.d.ts +4 -0
- package/lib/remapErrors.d.ts.map +1 -1
- package/lib/remapErrors.js +20 -1
- package/lib/remapErrors.js.map +1 -1
- package/lib/types.d.ts +4 -0
- package/lib/types.d.ts.map +1 -1
- package/lib-es/BleTransport.d.ts +67 -37
- package/lib-es/BleTransport.d.ts.map +1 -1
- package/lib-es/BleTransport.js +257 -198
- package/lib-es/BleTransport.js.map +1 -1
- package/lib-es/BleTransport.test.d.ts +2 -0
- package/lib-es/BleTransport.test.d.ts.map +1 -0
- package/lib-es/BleTransport.test.js +365 -0
- package/lib-es/BleTransport.test.js.map +1 -0
- package/lib-es/remapErrors.d.ts +4 -0
- package/lib-es/remapErrors.d.ts.map +1 -1
- package/lib-es/remapErrors.js +19 -1
- package/lib-es/remapErrors.js.map +1 -1
- package/lib-es/types.d.ts +4 -0
- package/lib-es/types.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/BleTransport.test.ts +275 -0
- package/src/BleTransport.ts +250 -172
- package/src/remapErrors.ts +33 -2
- package/src/types.ts +5 -0
package/src/BleTransport.ts
CHANGED
|
@@ -1,33 +1,38 @@
|
|
|
1
|
-
/* eslint-disable prefer-template */
|
|
2
1
|
import Transport from "@ledgerhq/hw-transport";
|
|
2
|
+
// ---------------------------------------------------------------------------------------------
|
|
3
|
+
// Since this is a react-native library and metro bundler does not support
|
|
4
|
+
// package exports yet (see: https://github.com/facebook/metro/issues/670)
|
|
5
|
+
// we need to import the file directly from the lib folder.
|
|
6
|
+
// Otherwise it would force the consumer of the lib to manually "tell" metro to resolve to /lib.
|
|
7
|
+
//
|
|
8
|
+
// TLDR: /!\ Do not remove the /lib part in the import statements below (@ledgerhq/devices/lib) ! /!\
|
|
9
|
+
// See: https://github.com/LedgerHQ/ledger-live/pull/879
|
|
10
|
+
import { sendAPDU } from "@ledgerhq/devices/lib/ble/sendAPDU";
|
|
11
|
+
import { receiveAPDU } from "@ledgerhq/devices/lib/ble/receiveAPDU";
|
|
12
|
+
|
|
3
13
|
import type {
|
|
4
14
|
Subscription as TransportSubscription,
|
|
5
15
|
Observer as TransportObserver,
|
|
6
|
-
DescriptorEvent,
|
|
7
16
|
} from "@ledgerhq/hw-transport";
|
|
8
17
|
import {
|
|
9
18
|
BleManager,
|
|
10
19
|
ConnectionPriority,
|
|
11
20
|
BleErrorCode,
|
|
21
|
+
LogLevel,
|
|
22
|
+
DeviceId,
|
|
23
|
+
Device,
|
|
24
|
+
Characteristic,
|
|
12
25
|
BleError,
|
|
26
|
+
Subscription,
|
|
13
27
|
} from "react-native-ble-plx";
|
|
14
28
|
import {
|
|
29
|
+
BluetoothInfos,
|
|
15
30
|
getBluetoothServiceUuids,
|
|
16
31
|
getInfosForServiceUuid,
|
|
17
32
|
} from "@ledgerhq/devices";
|
|
18
33
|
import type { DeviceModel } from "@ledgerhq/devices";
|
|
19
|
-
// ---------------------------------------------------------------------------------------------
|
|
20
|
-
// Since this is a react-native library and metro bundler does not support
|
|
21
|
-
// package exports yet (see: https://github.com/facebook/metro/issues/670)
|
|
22
|
-
// we need to import the file directly from the lib folder.
|
|
23
|
-
// Otherwise it would force the consumer of the lib to manually "tell" metro to resolve to /lib.
|
|
24
|
-
//
|
|
25
|
-
// TLDR: /!\ Do not remove the /lib part in the import statements below (@ledgerhq/devices/lib) ! /!\
|
|
26
|
-
// See: https://github.com/LedgerHQ/ledger-live/pull/879
|
|
27
|
-
import { sendAPDU } from "@ledgerhq/devices/lib/ble/sendAPDU";
|
|
28
|
-
import { receiveAPDU } from "@ledgerhq/devices/lib/ble/receiveAPDU";
|
|
29
34
|
import { log } from "@ledgerhq/logs";
|
|
30
|
-
import { Observable, defer, merge, from, of, throwError } from "rxjs";
|
|
35
|
+
import { Observable, defer, merge, from, of, throwError, Observer } from "rxjs";
|
|
31
36
|
import {
|
|
32
37
|
share,
|
|
33
38
|
ignoreElements,
|
|
@@ -42,35 +47,32 @@ import {
|
|
|
42
47
|
DisconnectedDeviceDuringOperation,
|
|
43
48
|
PairingFailed,
|
|
44
49
|
HwTransportError,
|
|
45
|
-
HwTransportErrorType,
|
|
46
50
|
} from "@ledgerhq/errors";
|
|
47
|
-
import type { Device, Characteristic } from "./types";
|
|
48
51
|
import { monitorCharacteristic } from "./monitorCharacteristic";
|
|
49
52
|
import { awaitsBleOn } from "./awaitsBleOn";
|
|
50
|
-
import {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
};
|
|
55
|
-
|
|
53
|
+
import {
|
|
54
|
+
decoratePromiseErrors,
|
|
55
|
+
mapBleErrorToHwTransportError,
|
|
56
|
+
remapError,
|
|
57
|
+
} from "./remapErrors";
|
|
58
|
+
import { ReconnectionConfig } from "./types";
|
|
56
59
|
|
|
57
|
-
let _bleManager: BleManager | null = null;
|
|
58
60
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* Do not use _bleManager directly
|
|
63
|
-
* Only use this instance getter inside BleTransport
|
|
61
|
+
* This is potentially not needed anymore, to be checked if the bug is still
|
|
62
|
+
* happening.
|
|
64
63
|
*/
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
let reconnectionConfig: ReconnectionConfig | null | undefined = {
|
|
65
|
+
pairingThreshold: 1000,
|
|
66
|
+
delayAfterFirstPairing: 4000,
|
|
67
|
+
};
|
|
69
68
|
|
|
70
|
-
|
|
69
|
+
export const setReconnectionConfig = (
|
|
70
|
+
config: ReconnectionConfig | null | undefined
|
|
71
|
+
): void => {
|
|
72
|
+
reconnectionConfig = config;
|
|
71
73
|
};
|
|
72
74
|
|
|
73
|
-
const retrieveInfos = (device) => {
|
|
75
|
+
const retrieveInfos = (device: Device | null) => {
|
|
74
76
|
if (!device || !device.serviceUUIDs) return;
|
|
75
77
|
const [serviceUUID] = device.serviceUUIDs;
|
|
76
78
|
if (!serviceUUID) return;
|
|
@@ -79,67 +81,82 @@ const retrieveInfos = (device) => {
|
|
|
79
81
|
return infos;
|
|
80
82
|
};
|
|
81
83
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
const delay = (ms: number | undefined) =>
|
|
85
|
+
new Promise((success) => setTimeout(success, ms));
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A cache of Bluetooth transport instances associated with device IDs.
|
|
89
|
+
* Allows efficient storage and retrieval of previously initialized transports.
|
|
90
|
+
* @type {Object.<string, BluetoothTransport>}
|
|
91
|
+
*/
|
|
92
|
+
const transportsCache: { [deviceId: string]: BleTransport } = {};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Returns the instance of the Bluetooth Low Energy Manager. It initializes it only
|
|
96
|
+
* when it's first needed, preventing the permission prompt happening prematurely.
|
|
97
|
+
* Important: Do NOT access the _bleManager variable directly.
|
|
98
|
+
* Use this function instead.
|
|
99
|
+
* @returns {BleManager} - The instance of the BleManager.
|
|
100
|
+
*/
|
|
101
|
+
let _bleManager: BleManager | null = null;
|
|
102
|
+
const bleManagerInstance = (): BleManager => {
|
|
103
|
+
if (!_bleManager) {
|
|
104
|
+
_bleManager = new BleManager();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return _bleManager;
|
|
89
108
|
};
|
|
90
|
-
export function setReconnectionConfig(
|
|
91
|
-
config: ReconnectionConfig | null | undefined
|
|
92
|
-
) {
|
|
93
|
-
reconnectionConfig = config;
|
|
94
|
-
}
|
|
95
109
|
|
|
96
|
-
const
|
|
110
|
+
const clearDisconnectTimeout = (deviceId: string): void => {
|
|
111
|
+
const cachedTransport = transportsCache[deviceId];
|
|
112
|
+
if (cachedTransport && cachedTransport.disconnectTimeout) {
|
|
113
|
+
log(TAG, "Clearing queued disconnect");
|
|
114
|
+
clearTimeout(cachedTransport.disconnectTimeout);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
97
117
|
|
|
98
118
|
async function open(deviceOrId: Device | string, needsReconnect: boolean) {
|
|
99
|
-
let device;
|
|
119
|
+
let device: Device;
|
|
120
|
+
log(TAG, `open with ${deviceOrId}`);
|
|
100
121
|
|
|
101
122
|
if (typeof deviceOrId === "string") {
|
|
102
123
|
if (transportsCache[deviceOrId]) {
|
|
103
|
-
log(
|
|
124
|
+
log(TAG, "Transport in cache, using that.");
|
|
125
|
+
clearDisconnectTimeout(deviceOrId);
|
|
104
126
|
return transportsCache[deviceOrId];
|
|
105
127
|
}
|
|
106
128
|
|
|
107
|
-
log(
|
|
129
|
+
log(TAG, `Tries to open device: ${deviceOrId}`);
|
|
108
130
|
await awaitsBleOn(bleManagerInstance());
|
|
109
131
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
[device] = devices;
|
|
115
|
-
}
|
|
132
|
+
// Returns a list of known devices by their identifiers
|
|
133
|
+
const devices = await bleManagerInstance().devices([deviceOrId]);
|
|
134
|
+
log(TAG, `found ${devices.length} devices`);
|
|
135
|
+
[device] = devices;
|
|
116
136
|
|
|
117
137
|
if (!device) {
|
|
138
|
+
// Returns a list of the peripherals currently connected to the system
|
|
139
|
+
// which have discovered services, connected to system doesn't mean
|
|
140
|
+
// connected to our app, we check that below.
|
|
118
141
|
const connectedDevices = await bleManagerInstance().connectedDevices(
|
|
119
142
|
getBluetoothServiceUuids()
|
|
120
143
|
);
|
|
121
144
|
const connectedDevicesFiltered = connectedDevices.filter(
|
|
122
145
|
(d) => d.id === deviceOrId
|
|
123
146
|
);
|
|
124
|
-
log(
|
|
125
|
-
"ble-verbose",
|
|
126
|
-
`found ${connectedDevicesFiltered.length} connected devices`
|
|
127
|
-
);
|
|
147
|
+
log(TAG, `found ${connectedDevicesFiltered.length} connected devices`);
|
|
128
148
|
[device] = connectedDevicesFiltered;
|
|
129
149
|
}
|
|
130
150
|
|
|
131
151
|
if (!device) {
|
|
132
|
-
|
|
133
|
-
|
|
152
|
+
// We still don't have a device, so we attempt to connect to it.
|
|
153
|
+
log(TAG, `connectToDevice(${deviceOrId})`);
|
|
154
|
+
// Nb ConnectionOptions dropped since it's not used internally by ble-plx.
|
|
134
155
|
try {
|
|
135
|
-
device = await bleManagerInstance().connectToDevice(
|
|
136
|
-
deviceOrId,
|
|
137
|
-
connectOptions
|
|
138
|
-
);
|
|
156
|
+
device = await bleManagerInstance().connectToDevice(deviceOrId);
|
|
139
157
|
} catch (e: any) {
|
|
158
|
+
log(TAG, `error code ${e.errorCode}`);
|
|
140
159
|
if (e.errorCode === BleErrorCode.DeviceMTUChangeFailed) {
|
|
141
|
-
// eslint-disable-next-line require-atomic-updates
|
|
142
|
-
connectOptions = {};
|
|
143
160
|
device = await bleManagerInstance().connectToDevice(deviceOrId);
|
|
144
161
|
} else {
|
|
145
162
|
throw e;
|
|
@@ -151,18 +168,17 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
|
|
|
151
168
|
throw new CantOpenDevice();
|
|
152
169
|
}
|
|
153
170
|
} else {
|
|
171
|
+
// It was already a Device
|
|
154
172
|
device = deviceOrId;
|
|
155
173
|
}
|
|
156
174
|
|
|
157
175
|
if (!(await device.isConnected())) {
|
|
158
|
-
log(
|
|
159
|
-
|
|
176
|
+
log(TAG, "not connected. connecting...");
|
|
160
177
|
try {
|
|
161
|
-
await device.connect(
|
|
178
|
+
await device.connect();
|
|
162
179
|
} catch (e: any) {
|
|
163
180
|
if (e.errorCode === BleErrorCode.DeviceMTUChangeFailed) {
|
|
164
|
-
//
|
|
165
|
-
connectOptions = {};
|
|
181
|
+
// Retry once for this specific error.
|
|
166
182
|
await device.connect();
|
|
167
183
|
} else {
|
|
168
184
|
throw e;
|
|
@@ -171,8 +187,8 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
|
|
|
171
187
|
}
|
|
172
188
|
|
|
173
189
|
await device.discoverAllServicesAndCharacteristics();
|
|
174
|
-
let res = retrieveInfos(device);
|
|
175
|
-
let characteristics;
|
|
190
|
+
let res: BluetoothInfos | undefined = retrieveInfos(device);
|
|
191
|
+
let characteristics: Characteristic[] | undefined;
|
|
176
192
|
|
|
177
193
|
if (!res) {
|
|
178
194
|
for (const uuid of getBluetoothServiceUuids()) {
|
|
@@ -200,9 +216,9 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
|
|
|
200
216
|
throw new TransportError("service not found", "BLEServiceNotFound");
|
|
201
217
|
}
|
|
202
218
|
|
|
203
|
-
let writeC;
|
|
204
|
-
let writeCmdC;
|
|
205
|
-
let notifyC;
|
|
219
|
+
let writeC: Characteristic | null | undefined;
|
|
220
|
+
let writeCmdC: Characteristic | undefined;
|
|
221
|
+
let notifyC: Characteristic | null | undefined;
|
|
206
222
|
|
|
207
223
|
for (const c of characteristics) {
|
|
208
224
|
if (c.uuid === writeUuid) {
|
|
@@ -217,28 +233,28 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
|
|
|
217
233
|
if (!writeC) {
|
|
218
234
|
throw new TransportError(
|
|
219
235
|
"write characteristic not found",
|
|
220
|
-
"
|
|
236
|
+
"BLECharacteristicNotFound"
|
|
221
237
|
);
|
|
222
238
|
}
|
|
223
239
|
|
|
224
240
|
if (!notifyC) {
|
|
225
241
|
throw new TransportError(
|
|
226
242
|
"notify characteristic not found",
|
|
227
|
-
"
|
|
243
|
+
"BLECharacteristicNotFound"
|
|
228
244
|
);
|
|
229
245
|
}
|
|
230
246
|
|
|
231
247
|
if (!writeC.isWritableWithResponse) {
|
|
232
248
|
throw new TransportError(
|
|
233
249
|
"write characteristic not writableWithResponse",
|
|
234
|
-
"
|
|
250
|
+
"BLECharacteristicInvalid"
|
|
235
251
|
);
|
|
236
252
|
}
|
|
237
253
|
|
|
238
254
|
if (!notifyC.isNotifiable) {
|
|
239
255
|
throw new TransportError(
|
|
240
256
|
"notify characteristic not notifiable",
|
|
241
|
-
"
|
|
257
|
+
"BLECharacteristicInvalid"
|
|
242
258
|
);
|
|
243
259
|
}
|
|
244
260
|
|
|
@@ -246,12 +262,12 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
|
|
|
246
262
|
if (!writeCmdC.isWritableWithoutResponse) {
|
|
247
263
|
throw new TransportError(
|
|
248
264
|
"write cmd characteristic not writableWithoutResponse",
|
|
249
|
-
"
|
|
265
|
+
"BLECharacteristicInvalid"
|
|
250
266
|
);
|
|
251
267
|
}
|
|
252
268
|
}
|
|
253
269
|
|
|
254
|
-
log(
|
|
270
|
+
log(TAG, `device.mtu=${device.mtu}`);
|
|
255
271
|
const notifyObservable = monitorCharacteristic(notifyC).pipe(
|
|
256
272
|
catchError((e) => {
|
|
257
273
|
// LL-9033 fw 2.0.2 introduced this case, we silence the inner unhandled error.
|
|
@@ -267,7 +283,7 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
|
|
|
267
283
|
share()
|
|
268
284
|
);
|
|
269
285
|
const notif = notifyObservable.subscribe();
|
|
270
|
-
const transport = new
|
|
286
|
+
const transport = new BleTransport(
|
|
271
287
|
device,
|
|
272
288
|
writeC,
|
|
273
289
|
writeCmdC,
|
|
@@ -275,24 +291,29 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
|
|
|
275
291
|
deviceModel
|
|
276
292
|
);
|
|
277
293
|
|
|
278
|
-
await transport.requestConnectionPriority("High");
|
|
279
|
-
|
|
280
|
-
|
|
294
|
+
// await transport.requestConnectionPriority("High");
|
|
295
|
+
// eslint-disable-next-line prefer-const
|
|
296
|
+
let disconnectedSub: Subscription;
|
|
297
|
+
const onDisconnect = (e: BleError | null) => {
|
|
298
|
+
transport.isConnected = false;
|
|
281
299
|
transport.notYetDisconnected = false;
|
|
282
300
|
notif.unsubscribe();
|
|
283
|
-
disconnectedSub
|
|
301
|
+
disconnectedSub?.remove();
|
|
302
|
+
|
|
303
|
+
clearDisconnectTimeout(transport.id);
|
|
284
304
|
delete transportsCache[transport.id];
|
|
285
|
-
log(
|
|
305
|
+
log(TAG, `BleTransport(${transport.id}) disconnected`);
|
|
286
306
|
transport.emit("disconnect", e);
|
|
287
307
|
};
|
|
288
308
|
|
|
289
309
|
// eslint-disable-next-line require-atomic-updates
|
|
290
310
|
transportsCache[transport.id] = transport;
|
|
291
|
-
const
|
|
311
|
+
const beforeMTUTime = Date.now();
|
|
312
|
+
|
|
313
|
+
disconnectedSub = device.onDisconnected((e) => {
|
|
292
314
|
if (!transport.notYetDisconnected) return;
|
|
293
315
|
onDisconnect(e);
|
|
294
316
|
});
|
|
295
|
-
const beforeMTUTime = Date.now();
|
|
296
317
|
|
|
297
318
|
try {
|
|
298
319
|
await transport.inferMTU();
|
|
@@ -309,7 +330,7 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
|
|
|
309
330
|
|
|
310
331
|
if (needsReconnect) {
|
|
311
332
|
// necessary time for the bonding workaround
|
|
312
|
-
await
|
|
333
|
+
await BleTransport.disconnect(transport.id).catch(() => {});
|
|
313
334
|
await delay(reconnectionConfig.delayAfterFirstPairing);
|
|
314
335
|
}
|
|
315
336
|
} else {
|
|
@@ -327,9 +348,11 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
|
|
|
327
348
|
/**
|
|
328
349
|
* react-native bluetooth BLE implementation
|
|
329
350
|
* @example
|
|
330
|
-
* import
|
|
351
|
+
* import BleTransport from "@ledgerhq/react-native-hw-transport-ble";
|
|
331
352
|
*/
|
|
332
|
-
|
|
353
|
+
const TAG = "ble-verbose";
|
|
354
|
+
export default class BleTransport extends Transport {
|
|
355
|
+
static disconnectTimeoutMs = 5000;
|
|
333
356
|
/**
|
|
334
357
|
*
|
|
335
358
|
*/
|
|
@@ -339,17 +362,36 @@ export default class BluetoothTransport extends Transport {
|
|
|
339
362
|
/**
|
|
340
363
|
*
|
|
341
364
|
*/
|
|
342
|
-
static
|
|
343
|
-
|
|
365
|
+
static list = (): Promise<void[]> => {
|
|
366
|
+
throw new Error("not implemented");
|
|
344
367
|
};
|
|
345
368
|
|
|
346
369
|
/**
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
370
|
+
* Exposed method from the ble-plx library
|
|
371
|
+
* Sets new log level for native module's logging mechanism.
|
|
372
|
+
* @param string logLevel New log level to be set.
|
|
350
373
|
*/
|
|
351
|
-
static
|
|
352
|
-
|
|
374
|
+
static setLogLevel = (logLevel: string): void => {
|
|
375
|
+
if (Object.values<string>(LogLevel).includes(logLevel)) {
|
|
376
|
+
bleManagerInstance().setLogLevel(logLevel as LogLevel);
|
|
377
|
+
} else {
|
|
378
|
+
throw new Error(`${logLevel} is not a valid LogLevel`);
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Listen to state changes on the bleManagerInstance and notify the
|
|
384
|
+
* specified observer.
|
|
385
|
+
* @param observer
|
|
386
|
+
* @returns TransportSubscription
|
|
387
|
+
*/
|
|
388
|
+
static observeState(
|
|
389
|
+
observer: Observer<{
|
|
390
|
+
type: string;
|
|
391
|
+
available: boolean;
|
|
392
|
+
}>
|
|
393
|
+
): TransportSubscription {
|
|
394
|
+
const emitFromState = (type: string) => {
|
|
353
395
|
observer.next({
|
|
354
396
|
type,
|
|
355
397
|
available: type === "PoweredOn",
|
|
@@ -357,24 +399,23 @@ export default class BluetoothTransport extends Transport {
|
|
|
357
399
|
};
|
|
358
400
|
|
|
359
401
|
bleManagerInstance().onStateChange(emitFromState, true);
|
|
402
|
+
|
|
360
403
|
return {
|
|
361
404
|
unsubscribe: () => {},
|
|
362
405
|
};
|
|
363
406
|
}
|
|
364
407
|
|
|
365
|
-
static list = (): any => {
|
|
366
|
-
throw new Error("not implemented");
|
|
367
|
-
};
|
|
368
|
-
|
|
369
408
|
/**
|
|
370
409
|
* Scan for bluetooth Ledger devices
|
|
410
|
+
* @param observer Device is partial in order to avoid the live-common/this dep
|
|
411
|
+
* @returns TransportSubscription
|
|
371
412
|
*/
|
|
372
413
|
static listen(
|
|
373
|
-
observer: TransportObserver<
|
|
414
|
+
observer: TransportObserver<any, HwTransportError>
|
|
374
415
|
): TransportSubscription {
|
|
375
|
-
log(
|
|
416
|
+
log(TAG, "listening for devices");
|
|
376
417
|
|
|
377
|
-
let unsubscribed;
|
|
418
|
+
let unsubscribed: boolean;
|
|
378
419
|
|
|
379
420
|
const stateSub = bleManagerInstance().onStateChange(async (state) => {
|
|
380
421
|
if (state === "PoweredOn") {
|
|
@@ -383,29 +424,32 @@ export default class BluetoothTransport extends Transport {
|
|
|
383
424
|
getBluetoothServiceUuids()
|
|
384
425
|
);
|
|
385
426
|
if (unsubscribed) return;
|
|
386
|
-
|
|
387
|
-
devices
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
427
|
+
if (devices.length) {
|
|
428
|
+
log(TAG, "disconnecting from devices");
|
|
429
|
+
|
|
430
|
+
await Promise.all(
|
|
431
|
+
devices.map((d) => BleTransport.disconnect(d.id).catch(() => {}))
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
391
435
|
if (unsubscribed) return;
|
|
392
436
|
bleManagerInstance().startDeviceScan(
|
|
393
437
|
getBluetoothServiceUuids(),
|
|
394
438
|
null,
|
|
395
|
-
(bleError,
|
|
439
|
+
(bleError: BleError | null, scannedDevice: Device | null) => {
|
|
396
440
|
if (bleError) {
|
|
397
441
|
observer.error(mapBleErrorToHwTransportError(bleError));
|
|
398
442
|
unsubscribe();
|
|
399
443
|
return;
|
|
400
444
|
}
|
|
401
445
|
|
|
402
|
-
const res = retrieveInfos(
|
|
446
|
+
const res = retrieveInfos(scannedDevice);
|
|
403
447
|
const deviceModel = res && res.deviceModel;
|
|
404
448
|
|
|
405
|
-
if (
|
|
449
|
+
if (scannedDevice) {
|
|
406
450
|
observer.next({
|
|
407
451
|
type: "add",
|
|
408
|
-
descriptor:
|
|
452
|
+
descriptor: scannedDevice,
|
|
409
453
|
deviceModel,
|
|
410
454
|
});
|
|
411
455
|
}
|
|
@@ -418,7 +462,8 @@ export default class BluetoothTransport extends Transport {
|
|
|
418
462
|
unsubscribed = true;
|
|
419
463
|
bleManagerInstance().stopDeviceScan();
|
|
420
464
|
stateSub.remove();
|
|
421
|
-
|
|
465
|
+
|
|
466
|
+
log(TAG, "done listening.");
|
|
422
467
|
};
|
|
423
468
|
|
|
424
469
|
return {
|
|
@@ -428,32 +473,37 @@ export default class BluetoothTransport extends Transport {
|
|
|
428
473
|
|
|
429
474
|
/**
|
|
430
475
|
* Open a BLE transport
|
|
431
|
-
* @param {
|
|
476
|
+
* @param {Device | string} deviceOrId
|
|
432
477
|
*/
|
|
433
|
-
static async open(deviceOrId: Device | string) {
|
|
478
|
+
static async open(deviceOrId: Device | string): Promise<BleTransport> {
|
|
434
479
|
return open(deviceOrId, true);
|
|
435
480
|
}
|
|
436
481
|
|
|
437
482
|
/**
|
|
438
|
-
*
|
|
483
|
+
* Exposed method from the ble-plx library
|
|
484
|
+
* Disconnects from {@link Device} if it's connected or cancels pending connection.
|
|
439
485
|
*/
|
|
440
|
-
static disconnect = async (id:
|
|
441
|
-
log(
|
|
486
|
+
static disconnect = async (id: DeviceId): Promise<void> => {
|
|
487
|
+
log(TAG, `user disconnect(${id})`);
|
|
442
488
|
await bleManagerInstance().cancelDeviceConnection(id);
|
|
489
|
+
log(TAG, "disconnected");
|
|
443
490
|
};
|
|
444
|
-
|
|
491
|
+
|
|
445
492
|
device: Device;
|
|
493
|
+
deviceModel: DeviceModel;
|
|
494
|
+
disconnectTimeout: null | ReturnType<typeof setTimeout> = null;
|
|
495
|
+
id: string;
|
|
496
|
+
isConnected = true;
|
|
446
497
|
mtuSize = 20;
|
|
447
|
-
writeCharacteristic: Characteristic;
|
|
448
|
-
writeCmdCharacteristic: Characteristic;
|
|
449
498
|
notifyObservable: Observable<any>;
|
|
450
|
-
deviceModel: DeviceModel;
|
|
451
499
|
notYetDisconnected = true;
|
|
500
|
+
writeCharacteristic: Characteristic;
|
|
501
|
+
writeCmdCharacteristic: Characteristic | undefined;
|
|
452
502
|
|
|
453
503
|
constructor(
|
|
454
504
|
device: Device,
|
|
455
505
|
writeCharacteristic: Characteristic,
|
|
456
|
-
writeCmdCharacteristic: Characteristic,
|
|
506
|
+
writeCmdCharacteristic: Characteristic | undefined,
|
|
457
507
|
notifyObservable: Observable<any>,
|
|
458
508
|
deviceModel: DeviceModel
|
|
459
509
|
) {
|
|
@@ -464,24 +514,31 @@ export default class BluetoothTransport extends Transport {
|
|
|
464
514
|
this.writeCmdCharacteristic = writeCmdCharacteristic;
|
|
465
515
|
this.notifyObservable = notifyObservable;
|
|
466
516
|
this.deviceModel = deviceModel;
|
|
467
|
-
|
|
517
|
+
|
|
518
|
+
log(TAG, `BleTransport(${String(this.id)}) new instance`);
|
|
519
|
+
clearDisconnectTimeout(this.id);
|
|
468
520
|
}
|
|
469
521
|
|
|
470
522
|
/**
|
|
471
|
-
*
|
|
523
|
+
* Send data to the device using a low level API.
|
|
524
|
+
* It's recommended to use the "send" method for a higher level API.
|
|
525
|
+
* @param {Buffer} apdu - The data to send.
|
|
526
|
+
* @returns {Promise<Buffer>} A promise that resolves with the response data from the device.
|
|
472
527
|
*/
|
|
473
528
|
exchange = (apdu: Buffer): Promise<any> =>
|
|
474
529
|
this.exchangeAtomicImpl(async () => {
|
|
475
530
|
try {
|
|
476
531
|
const msgIn = apdu.toString("hex");
|
|
477
532
|
log("apdu", `=> ${msgIn}`);
|
|
533
|
+
|
|
478
534
|
const data = await merge(
|
|
479
|
-
// $FlowFixMe
|
|
480
535
|
this.notifyObservable.pipe(receiveAPDU),
|
|
481
536
|
sendAPDU(this.write, apdu, this.mtuSize)
|
|
482
537
|
).toPromise();
|
|
538
|
+
|
|
483
539
|
const msgOut = data.toString("hex");
|
|
484
540
|
log("apdu", `<= ${msgOut}`);
|
|
541
|
+
|
|
485
542
|
return data;
|
|
486
543
|
} catch (e: any) {
|
|
487
544
|
log("ble-error", "exchange got " + String(e));
|
|
@@ -497,9 +554,13 @@ export default class BluetoothTransport extends Transport {
|
|
|
497
554
|
}
|
|
498
555
|
});
|
|
499
556
|
|
|
500
|
-
|
|
501
|
-
|
|
557
|
+
/**
|
|
558
|
+
* Negotiate with the device the maximum transfer unit for the ble frames
|
|
559
|
+
* @returns Promise<number>
|
|
560
|
+
*/
|
|
561
|
+
async inferMTU(): Promise<number> {
|
|
502
562
|
let { mtu } = this.device;
|
|
563
|
+
|
|
503
564
|
await this.exchangeAtomicImpl(async () => {
|
|
504
565
|
try {
|
|
505
566
|
mtu =
|
|
@@ -516,7 +577,8 @@ export default class BluetoothTransport extends Transport {
|
|
|
516
577
|
)
|
|
517
578
|
).toPromise()) + 3;
|
|
518
579
|
} catch (e: any) {
|
|
519
|
-
log("ble-error", "inferMTU got " +
|
|
580
|
+
log("ble-error", "inferMTU got " + JSON.stringify(e));
|
|
581
|
+
|
|
520
582
|
await bleManagerInstance()
|
|
521
583
|
.cancelDeviceConnection(this.id)
|
|
522
584
|
.catch(() => {}); // but we ignore if disconnect worked.
|
|
@@ -527,31 +589,37 @@ export default class BluetoothTransport extends Transport {
|
|
|
527
589
|
|
|
528
590
|
if (mtu > 23) {
|
|
529
591
|
const mtuSize = mtu - 3;
|
|
530
|
-
log(
|
|
531
|
-
"ble-verbose",
|
|
532
|
-
`BleTransport(${String(this.id)}) mtu set to ${String(mtuSize)}`
|
|
533
|
-
);
|
|
592
|
+
log(TAG, `BleTransport(${this.id}) mtu set to ${mtuSize}`);
|
|
534
593
|
this.mtuSize = mtuSize;
|
|
535
594
|
}
|
|
536
595
|
|
|
537
596
|
return this.mtuSize;
|
|
538
597
|
}
|
|
539
598
|
|
|
599
|
+
/**
|
|
600
|
+
* Exposed method from the ble-plx library
|
|
601
|
+
* Request the connection priority for the given device.
|
|
602
|
+
* @param {"Balanced" | "High" | "LowPower"} connectionPriority: Connection priority.
|
|
603
|
+
* @returns {Promise<Device>} Connected device.
|
|
604
|
+
*/
|
|
540
605
|
async requestConnectionPriority(
|
|
541
606
|
connectionPriority: "Balanced" | "High" | "LowPower"
|
|
542
|
-
) {
|
|
543
|
-
await decoratePromiseErrors(
|
|
607
|
+
): Promise<Device> {
|
|
608
|
+
return await decoratePromiseErrors(
|
|
544
609
|
this.device.requestConnectionPriority(
|
|
545
|
-
ConnectionPriority[connectionPriority]
|
|
610
|
+
ConnectionPriority[connectionPriority as keyof ConnectionPriority]
|
|
546
611
|
)
|
|
547
612
|
);
|
|
548
613
|
}
|
|
549
614
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
615
|
+
/**
|
|
616
|
+
* Do not call this directly unless you know what you're doing. Communication
|
|
617
|
+
* with a Ledger device should be through the {@link exchange} method.
|
|
618
|
+
* @param buffer
|
|
619
|
+
* @param txid
|
|
620
|
+
*/
|
|
621
|
+
write = async (buffer: Buffer, txid?: string | undefined): Promise<void> => {
|
|
553
622
|
log("ble-frame", "=> " + buffer.toString("hex"));
|
|
554
|
-
|
|
555
623
|
if (!this.writeCmdCharacteristic) {
|
|
556
624
|
try {
|
|
557
625
|
await this.writeCharacteristic.writeWithResponse(
|
|
@@ -573,32 +641,42 @@ export default class BluetoothTransport extends Transport {
|
|
|
573
641
|
}
|
|
574
642
|
};
|
|
575
643
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
644
|
+
/**
|
|
645
|
+
* We intentionally do not immediately close a transport connection.
|
|
646
|
+
* Instead, we queue the disconnect and wait for a future connection to dismiss the event.
|
|
647
|
+
* This approach prevents unnecessary disconnects and reconnects. We use the isConnected
|
|
648
|
+
* flag to ensure that we do not trigger a disconnect if the current cached transport has
|
|
649
|
+
* already been disconnected.
|
|
650
|
+
* @returns {Promise<void>}
|
|
651
|
+
*/
|
|
652
|
+
async close(): Promise<void> {
|
|
653
|
+
let resolve: (value: void | PromiseLike<void>) => void;
|
|
654
|
+
const disconnectPromise = new Promise<void>((innerResolve) => {
|
|
655
|
+
resolve = innerResolve;
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
clearDisconnectTimeout(this.id);
|
|
659
|
+
|
|
660
|
+
log(TAG, "Queuing a disconnect");
|
|
661
|
+
|
|
662
|
+
this.disconnectTimeout = setTimeout(() => {
|
|
663
|
+
log(TAG, `Triggering a disconnect from ${this.id}`);
|
|
664
|
+
if (this.isConnected) {
|
|
665
|
+
BleTransport.disconnect(this.id)
|
|
666
|
+
.catch(() => {})
|
|
667
|
+
.finally(resolve);
|
|
668
|
+
} else {
|
|
669
|
+
resolve();
|
|
670
|
+
}
|
|
671
|
+
}, BleTransport.disconnectTimeoutMs);
|
|
672
|
+
|
|
673
|
+
// The closure will occur no later than 5s, triggered either by disconnection
|
|
674
|
+
// or the actual response of the apdu.
|
|
675
|
+
await Promise.race([
|
|
676
|
+
this.exchangeBusyPromise || Promise.resolve(),
|
|
677
|
+
disconnectPromise,
|
|
678
|
+
]);
|
|
679
|
+
|
|
680
|
+
return;
|
|
580
681
|
}
|
|
581
682
|
}
|
|
582
|
-
|
|
583
|
-
const bleErrorToHwTransportError = new Map([
|
|
584
|
-
[BleErrorCode.ScanStartFailed, HwTransportErrorType.BleScanStartFailed],
|
|
585
|
-
[
|
|
586
|
-
BleErrorCode.LocationServicesDisabled,
|
|
587
|
-
HwTransportErrorType.BleLocationServicesDisabled,
|
|
588
|
-
],
|
|
589
|
-
[
|
|
590
|
-
BleErrorCode.BluetoothUnauthorized,
|
|
591
|
-
HwTransportErrorType.BleBluetoothUnauthorized,
|
|
592
|
-
],
|
|
593
|
-
]);
|
|
594
|
-
|
|
595
|
-
const mapBleErrorToHwTransportError = (
|
|
596
|
-
bleError: BleError
|
|
597
|
-
): HwTransportError => {
|
|
598
|
-
const message = `${bleError.message}. Origin: ${bleError.errorCode}`;
|
|
599
|
-
|
|
600
|
-
const inferedType = bleErrorToHwTransportError.get(bleError.errorCode);
|
|
601
|
-
const type = !inferedType ? HwTransportErrorType.Unknown : inferedType;
|
|
602
|
-
|
|
603
|
-
return new HwTransportError(type, message);
|
|
604
|
-
};
|