@ledgerhq/react-native-hw-transport-ble 6.28.3 → 6.28.4-next.1
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 +30 -0
- package/lib/BleTransport.d.ts +67 -37
- package/lib/BleTransport.d.ts.map +1 -1
- package/lib/BleTransport.js +269 -202
- 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 +21 -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 +269 -202
- 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 +20 -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 +16 -5
- package/src/BleTransport.test.ts +275 -0
- package/src/BleTransport.ts +272 -180
- package/src/remapErrors.ts +34 -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,66 +81,95 @@ const retrieveInfos = (device) => {
|
|
|
79
81
|
return infos;
|
|
80
82
|
};
|
|
81
83
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
+
// connectOptions is actually used by react-native-ble-plx even if comment above ConnectionOptions says it's not used
|
|
95
|
+
let connectOptions: Record<string, unknown> = {
|
|
96
|
+
// 156 bytes to max the iOS < 10 limit (158 bytes)
|
|
97
|
+
// (185 bytes for iOS >= 10)(up to 512 bytes for Android, but could be blocked at 23 bytes)
|
|
98
|
+
requestMTU: 156,
|
|
99
|
+
// Priority 1 = high. TODO: Check firmware update over BLE PR before merging
|
|
100
|
+
connectionPriority: 1,
|
|
85
101
|
};
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Returns the instance of the Bluetooth Low Energy Manager. It initializes it only
|
|
105
|
+
* when it's first needed, preventing the permission prompt happening prematurely.
|
|
106
|
+
* Important: Do NOT access the _bleManager variable directly.
|
|
107
|
+
* Use this function instead.
|
|
108
|
+
* @returns {BleManager} - The instance of the BleManager.
|
|
109
|
+
*/
|
|
110
|
+
let _bleManager: BleManager | null = null;
|
|
111
|
+
const bleManagerInstance = (): BleManager => {
|
|
112
|
+
if (!_bleManager) {
|
|
113
|
+
_bleManager = new BleManager();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return _bleManager;
|
|
89
117
|
};
|
|
90
|
-
export function setReconnectionConfig(
|
|
91
|
-
config: ReconnectionConfig | null | undefined
|
|
92
|
-
) {
|
|
93
|
-
reconnectionConfig = config;
|
|
94
|
-
}
|
|
95
118
|
|
|
96
|
-
const
|
|
119
|
+
const clearDisconnectTimeout = (deviceId: string): void => {
|
|
120
|
+
const cachedTransport = transportsCache[deviceId];
|
|
121
|
+
if (cachedTransport && cachedTransport.disconnectTimeout) {
|
|
122
|
+
log(TAG, "Clearing queued disconnect");
|
|
123
|
+
clearTimeout(cachedTransport.disconnectTimeout);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
97
126
|
|
|
98
127
|
async function open(deviceOrId: Device | string, needsReconnect: boolean) {
|
|
99
|
-
let device;
|
|
128
|
+
let device: Device;
|
|
129
|
+
log(TAG, `open with ${deviceOrId}`);
|
|
100
130
|
|
|
101
131
|
if (typeof deviceOrId === "string") {
|
|
102
132
|
if (transportsCache[deviceOrId]) {
|
|
103
|
-
log(
|
|
133
|
+
log(TAG, "Transport in cache, using that.");
|
|
134
|
+
clearDisconnectTimeout(deviceOrId);
|
|
104
135
|
return transportsCache[deviceOrId];
|
|
105
136
|
}
|
|
106
137
|
|
|
107
|
-
log(
|
|
138
|
+
log(TAG, `Tries to open device: ${deviceOrId}`);
|
|
108
139
|
await awaitsBleOn(bleManagerInstance());
|
|
109
140
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
[device] = devices;
|
|
115
|
-
}
|
|
141
|
+
// Returns a list of known devices by their identifiers
|
|
142
|
+
const devices = await bleManagerInstance().devices([deviceOrId]);
|
|
143
|
+
log(TAG, `found ${devices.length} devices`);
|
|
144
|
+
[device] = devices;
|
|
116
145
|
|
|
117
146
|
if (!device) {
|
|
147
|
+
// Returns a list of the peripherals currently connected to the system
|
|
148
|
+
// which have discovered services, connected to system doesn't mean
|
|
149
|
+
// connected to our app, we check that below.
|
|
118
150
|
const connectedDevices = await bleManagerInstance().connectedDevices(
|
|
119
151
|
getBluetoothServiceUuids()
|
|
120
152
|
);
|
|
121
153
|
const connectedDevicesFiltered = connectedDevices.filter(
|
|
122
154
|
(d) => d.id === deviceOrId
|
|
123
155
|
);
|
|
124
|
-
log(
|
|
125
|
-
"ble-verbose",
|
|
126
|
-
`found ${connectedDevicesFiltered.length} connected devices`
|
|
127
|
-
);
|
|
156
|
+
log(TAG, `found ${connectedDevicesFiltered.length} connected devices`);
|
|
128
157
|
[device] = connectedDevicesFiltered;
|
|
129
158
|
}
|
|
130
159
|
|
|
131
160
|
if (!device) {
|
|
132
|
-
|
|
133
|
-
|
|
161
|
+
// We still don't have a device, so we attempt to connect to it.
|
|
162
|
+
log(TAG, `connectToDevice(${deviceOrId})`);
|
|
163
|
+
// Nb ConnectionOptions dropped since it's not used internally by ble-plx.
|
|
134
164
|
try {
|
|
135
165
|
device = await bleManagerInstance().connectToDevice(
|
|
136
166
|
deviceOrId,
|
|
137
167
|
connectOptions
|
|
138
168
|
);
|
|
139
169
|
} catch (e: any) {
|
|
170
|
+
log(TAG, `error code ${e.errorCode}`);
|
|
140
171
|
if (e.errorCode === BleErrorCode.DeviceMTUChangeFailed) {
|
|
141
|
-
//
|
|
172
|
+
// If the MTU update did not work, we try to connect without requesting for a specific MTU
|
|
142
173
|
connectOptions = {};
|
|
143
174
|
device = await bleManagerInstance().connectToDevice(deviceOrId);
|
|
144
175
|
} else {
|
|
@@ -151,17 +182,17 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
|
|
|
151
182
|
throw new CantOpenDevice();
|
|
152
183
|
}
|
|
153
184
|
} else {
|
|
185
|
+
// It was already a Device
|
|
154
186
|
device = deviceOrId;
|
|
155
187
|
}
|
|
156
188
|
|
|
157
189
|
if (!(await device.isConnected())) {
|
|
158
|
-
log(
|
|
159
|
-
|
|
190
|
+
log(TAG, "not connected. connecting...");
|
|
160
191
|
try {
|
|
161
192
|
await device.connect(connectOptions);
|
|
162
193
|
} catch (e: any) {
|
|
163
194
|
if (e.errorCode === BleErrorCode.DeviceMTUChangeFailed) {
|
|
164
|
-
//
|
|
195
|
+
// If the MTU update did not work, we try to connect without requesting for a specific MTU
|
|
165
196
|
connectOptions = {};
|
|
166
197
|
await device.connect();
|
|
167
198
|
} else {
|
|
@@ -171,8 +202,8 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
|
|
|
171
202
|
}
|
|
172
203
|
|
|
173
204
|
await device.discoverAllServicesAndCharacteristics();
|
|
174
|
-
let res = retrieveInfos(device);
|
|
175
|
-
let characteristics;
|
|
205
|
+
let res: BluetoothInfos | undefined = retrieveInfos(device);
|
|
206
|
+
let characteristics: Characteristic[] | undefined;
|
|
176
207
|
|
|
177
208
|
if (!res) {
|
|
178
209
|
for (const uuid of getBluetoothServiceUuids()) {
|
|
@@ -200,9 +231,9 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
|
|
|
200
231
|
throw new TransportError("service not found", "BLEServiceNotFound");
|
|
201
232
|
}
|
|
202
233
|
|
|
203
|
-
let writeC;
|
|
204
|
-
let writeCmdC;
|
|
205
|
-
let notifyC;
|
|
234
|
+
let writeC: Characteristic | null | undefined;
|
|
235
|
+
let writeCmdC: Characteristic | undefined;
|
|
236
|
+
let notifyC: Characteristic | null | undefined;
|
|
206
237
|
|
|
207
238
|
for (const c of characteristics) {
|
|
208
239
|
if (c.uuid === writeUuid) {
|
|
@@ -217,28 +248,28 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
|
|
|
217
248
|
if (!writeC) {
|
|
218
249
|
throw new TransportError(
|
|
219
250
|
"write characteristic not found",
|
|
220
|
-
"
|
|
251
|
+
"BLECharacteristicNotFound"
|
|
221
252
|
);
|
|
222
253
|
}
|
|
223
254
|
|
|
224
255
|
if (!notifyC) {
|
|
225
256
|
throw new TransportError(
|
|
226
257
|
"notify characteristic not found",
|
|
227
|
-
"
|
|
258
|
+
"BLECharacteristicNotFound"
|
|
228
259
|
);
|
|
229
260
|
}
|
|
230
261
|
|
|
231
262
|
if (!writeC.isWritableWithResponse) {
|
|
232
263
|
throw new TransportError(
|
|
233
264
|
"write characteristic not writableWithResponse",
|
|
234
|
-
"
|
|
265
|
+
"BLECharacteristicInvalid"
|
|
235
266
|
);
|
|
236
267
|
}
|
|
237
268
|
|
|
238
269
|
if (!notifyC.isNotifiable) {
|
|
239
270
|
throw new TransportError(
|
|
240
271
|
"notify characteristic not notifiable",
|
|
241
|
-
"
|
|
272
|
+
"BLECharacteristicInvalid"
|
|
242
273
|
);
|
|
243
274
|
}
|
|
244
275
|
|
|
@@ -246,12 +277,12 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
|
|
|
246
277
|
if (!writeCmdC.isWritableWithoutResponse) {
|
|
247
278
|
throw new TransportError(
|
|
248
279
|
"write cmd characteristic not writableWithoutResponse",
|
|
249
|
-
"
|
|
280
|
+
"BLECharacteristicInvalid"
|
|
250
281
|
);
|
|
251
282
|
}
|
|
252
283
|
}
|
|
253
284
|
|
|
254
|
-
log(
|
|
285
|
+
log(TAG, `device.mtu=${device.mtu}`);
|
|
255
286
|
const notifyObservable = monitorCharacteristic(notifyC).pipe(
|
|
256
287
|
catchError((e) => {
|
|
257
288
|
// LL-9033 fw 2.0.2 introduced this case, we silence the inner unhandled error.
|
|
@@ -267,7 +298,7 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
|
|
|
267
298
|
share()
|
|
268
299
|
);
|
|
269
300
|
const notif = notifyObservable.subscribe();
|
|
270
|
-
const transport = new
|
|
301
|
+
const transport = new BleTransport(
|
|
271
302
|
device,
|
|
272
303
|
writeC,
|
|
273
304
|
writeCmdC,
|
|
@@ -275,24 +306,30 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
|
|
|
275
306
|
deviceModel
|
|
276
307
|
);
|
|
277
308
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
309
|
+
// Keeping it as a comment for now but if no new bluetooth issues occur, we will be able to remove it
|
|
310
|
+
// await transport.requestConnectionPriority("High");
|
|
311
|
+
// eslint-disable-next-line prefer-const
|
|
312
|
+
let disconnectedSub: Subscription;
|
|
313
|
+
const onDisconnect = (e: BleError | null) => {
|
|
314
|
+
transport.isConnected = false;
|
|
281
315
|
transport.notYetDisconnected = false;
|
|
282
316
|
notif.unsubscribe();
|
|
283
|
-
disconnectedSub
|
|
317
|
+
disconnectedSub?.remove();
|
|
318
|
+
|
|
319
|
+
clearDisconnectTimeout(transport.id);
|
|
284
320
|
delete transportsCache[transport.id];
|
|
285
|
-
log(
|
|
321
|
+
log(TAG, `BleTransport(${transport.id}) disconnected`);
|
|
286
322
|
transport.emit("disconnect", e);
|
|
287
323
|
};
|
|
288
324
|
|
|
289
325
|
// eslint-disable-next-line require-atomic-updates
|
|
290
326
|
transportsCache[transport.id] = transport;
|
|
291
|
-
const
|
|
327
|
+
const beforeMTUTime = Date.now();
|
|
328
|
+
|
|
329
|
+
disconnectedSub = device.onDisconnected((e) => {
|
|
292
330
|
if (!transport.notYetDisconnected) return;
|
|
293
331
|
onDisconnect(e);
|
|
294
332
|
});
|
|
295
|
-
const beforeMTUTime = Date.now();
|
|
296
333
|
|
|
297
334
|
try {
|
|
298
335
|
await transport.inferMTU();
|
|
@@ -309,7 +346,7 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
|
|
|
309
346
|
|
|
310
347
|
if (needsReconnect) {
|
|
311
348
|
// necessary time for the bonding workaround
|
|
312
|
-
await
|
|
349
|
+
await BleTransport.disconnect(transport.id).catch(() => {});
|
|
313
350
|
await delay(reconnectionConfig.delayAfterFirstPairing);
|
|
314
351
|
}
|
|
315
352
|
} else {
|
|
@@ -327,9 +364,11 @@ async function open(deviceOrId: Device | string, needsReconnect: boolean) {
|
|
|
327
364
|
/**
|
|
328
365
|
* react-native bluetooth BLE implementation
|
|
329
366
|
* @example
|
|
330
|
-
* import
|
|
367
|
+
* import BleTransport from "@ledgerhq/react-native-hw-transport-ble";
|
|
331
368
|
*/
|
|
332
|
-
|
|
369
|
+
const TAG = "ble-verbose";
|
|
370
|
+
export default class BleTransport extends Transport {
|
|
371
|
+
static disconnectTimeoutMs = 5000;
|
|
333
372
|
/**
|
|
334
373
|
*
|
|
335
374
|
*/
|
|
@@ -339,17 +378,36 @@ export default class BluetoothTransport extends Transport {
|
|
|
339
378
|
/**
|
|
340
379
|
*
|
|
341
380
|
*/
|
|
342
|
-
static
|
|
343
|
-
|
|
381
|
+
static list = (): Promise<void[]> => {
|
|
382
|
+
throw new Error("not implemented");
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Exposed method from the ble-plx library
|
|
387
|
+
* Sets new log level for native module's logging mechanism.
|
|
388
|
+
* @param string logLevel New log level to be set.
|
|
389
|
+
*/
|
|
390
|
+
static setLogLevel = (logLevel: string): void => {
|
|
391
|
+
if (Object.values<string>(LogLevel).includes(logLevel)) {
|
|
392
|
+
bleManagerInstance().setLogLevel(logLevel as LogLevel);
|
|
393
|
+
} else {
|
|
394
|
+
throw new Error(`${logLevel} is not a valid LogLevel`);
|
|
395
|
+
}
|
|
344
396
|
};
|
|
345
397
|
|
|
346
398
|
/**
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
399
|
+
* Listen to state changes on the bleManagerInstance and notify the
|
|
400
|
+
* specified observer.
|
|
401
|
+
* @param observer
|
|
402
|
+
* @returns TransportSubscription
|
|
350
403
|
*/
|
|
351
|
-
static observeState(
|
|
352
|
-
|
|
404
|
+
static observeState(
|
|
405
|
+
observer: Observer<{
|
|
406
|
+
type: string;
|
|
407
|
+
available: boolean;
|
|
408
|
+
}>
|
|
409
|
+
): TransportSubscription {
|
|
410
|
+
const emitFromState = (type: string) => {
|
|
353
411
|
observer.next({
|
|
354
412
|
type,
|
|
355
413
|
available: type === "PoweredOn",
|
|
@@ -357,24 +415,23 @@ export default class BluetoothTransport extends Transport {
|
|
|
357
415
|
};
|
|
358
416
|
|
|
359
417
|
bleManagerInstance().onStateChange(emitFromState, true);
|
|
418
|
+
|
|
360
419
|
return {
|
|
361
420
|
unsubscribe: () => {},
|
|
362
421
|
};
|
|
363
422
|
}
|
|
364
423
|
|
|
365
|
-
static list = (): any => {
|
|
366
|
-
throw new Error("not implemented");
|
|
367
|
-
};
|
|
368
|
-
|
|
369
424
|
/**
|
|
370
425
|
* Scan for bluetooth Ledger devices
|
|
426
|
+
* @param observer Device is partial in order to avoid the live-common/this dep
|
|
427
|
+
* @returns TransportSubscription
|
|
371
428
|
*/
|
|
372
429
|
static listen(
|
|
373
|
-
observer: TransportObserver<
|
|
430
|
+
observer: TransportObserver<any, HwTransportError>
|
|
374
431
|
): TransportSubscription {
|
|
375
|
-
log(
|
|
432
|
+
log(TAG, "listening for devices");
|
|
376
433
|
|
|
377
|
-
let unsubscribed;
|
|
434
|
+
let unsubscribed: boolean;
|
|
378
435
|
|
|
379
436
|
const stateSub = bleManagerInstance().onStateChange(async (state) => {
|
|
380
437
|
if (state === "PoweredOn") {
|
|
@@ -383,29 +440,32 @@ export default class BluetoothTransport extends Transport {
|
|
|
383
440
|
getBluetoothServiceUuids()
|
|
384
441
|
);
|
|
385
442
|
if (unsubscribed) return;
|
|
386
|
-
|
|
387
|
-
devices
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
443
|
+
if (devices.length) {
|
|
444
|
+
log(TAG, "disconnecting from devices");
|
|
445
|
+
|
|
446
|
+
await Promise.all(
|
|
447
|
+
devices.map((d) => BleTransport.disconnect(d.id).catch(() => {}))
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
391
451
|
if (unsubscribed) return;
|
|
392
452
|
bleManagerInstance().startDeviceScan(
|
|
393
453
|
getBluetoothServiceUuids(),
|
|
394
454
|
null,
|
|
395
|
-
(bleError,
|
|
455
|
+
(bleError: BleError | null, scannedDevice: Device | null) => {
|
|
396
456
|
if (bleError) {
|
|
397
457
|
observer.error(mapBleErrorToHwTransportError(bleError));
|
|
398
458
|
unsubscribe();
|
|
399
459
|
return;
|
|
400
460
|
}
|
|
401
461
|
|
|
402
|
-
const res = retrieveInfos(
|
|
462
|
+
const res = retrieveInfos(scannedDevice);
|
|
403
463
|
const deviceModel = res && res.deviceModel;
|
|
404
464
|
|
|
405
|
-
if (
|
|
465
|
+
if (scannedDevice) {
|
|
406
466
|
observer.next({
|
|
407
467
|
type: "add",
|
|
408
|
-
descriptor:
|
|
468
|
+
descriptor: scannedDevice,
|
|
409
469
|
deviceModel,
|
|
410
470
|
});
|
|
411
471
|
}
|
|
@@ -418,7 +478,8 @@ export default class BluetoothTransport extends Transport {
|
|
|
418
478
|
unsubscribed = true;
|
|
419
479
|
bleManagerInstance().stopDeviceScan();
|
|
420
480
|
stateSub.remove();
|
|
421
|
-
|
|
481
|
+
|
|
482
|
+
log(TAG, "done listening.");
|
|
422
483
|
};
|
|
423
484
|
|
|
424
485
|
return {
|
|
@@ -428,32 +489,37 @@ export default class BluetoothTransport extends Transport {
|
|
|
428
489
|
|
|
429
490
|
/**
|
|
430
491
|
* Open a BLE transport
|
|
431
|
-
* @param {
|
|
492
|
+
* @param {Device | string} deviceOrId
|
|
432
493
|
*/
|
|
433
|
-
static async open(deviceOrId: Device | string) {
|
|
494
|
+
static async open(deviceOrId: Device | string): Promise<BleTransport> {
|
|
434
495
|
return open(deviceOrId, true);
|
|
435
496
|
}
|
|
436
497
|
|
|
437
498
|
/**
|
|
438
|
-
*
|
|
499
|
+
* Exposed method from the ble-plx library
|
|
500
|
+
* Disconnects from {@link Device} if it's connected or cancels pending connection.
|
|
439
501
|
*/
|
|
440
|
-
static disconnect = async (id:
|
|
441
|
-
log(
|
|
502
|
+
static disconnect = async (id: DeviceId): Promise<void> => {
|
|
503
|
+
log(TAG, `user disconnect(${id})`);
|
|
442
504
|
await bleManagerInstance().cancelDeviceConnection(id);
|
|
505
|
+
log(TAG, "disconnected");
|
|
443
506
|
};
|
|
444
|
-
|
|
507
|
+
|
|
445
508
|
device: Device;
|
|
509
|
+
deviceModel: DeviceModel;
|
|
510
|
+
disconnectTimeout: null | ReturnType<typeof setTimeout> = null;
|
|
511
|
+
id: string;
|
|
512
|
+
isConnected = true;
|
|
446
513
|
mtuSize = 20;
|
|
447
|
-
writeCharacteristic: Characteristic;
|
|
448
|
-
writeCmdCharacteristic: Characteristic;
|
|
449
514
|
notifyObservable: Observable<any>;
|
|
450
|
-
deviceModel: DeviceModel;
|
|
451
515
|
notYetDisconnected = true;
|
|
516
|
+
writeCharacteristic: Characteristic;
|
|
517
|
+
writeCmdCharacteristic: Characteristic | undefined;
|
|
452
518
|
|
|
453
519
|
constructor(
|
|
454
520
|
device: Device,
|
|
455
521
|
writeCharacteristic: Characteristic,
|
|
456
|
-
writeCmdCharacteristic: Characteristic,
|
|
522
|
+
writeCmdCharacteristic: Characteristic | undefined,
|
|
457
523
|
notifyObservable: Observable<any>,
|
|
458
524
|
deviceModel: DeviceModel
|
|
459
525
|
) {
|
|
@@ -464,24 +530,31 @@ export default class BluetoothTransport extends Transport {
|
|
|
464
530
|
this.writeCmdCharacteristic = writeCmdCharacteristic;
|
|
465
531
|
this.notifyObservable = notifyObservable;
|
|
466
532
|
this.deviceModel = deviceModel;
|
|
467
|
-
|
|
533
|
+
|
|
534
|
+
log(TAG, `BleTransport(${String(this.id)}) new instance`);
|
|
535
|
+
clearDisconnectTimeout(this.id);
|
|
468
536
|
}
|
|
469
537
|
|
|
470
538
|
/**
|
|
471
|
-
*
|
|
539
|
+
* Send data to the device using a low level API.
|
|
540
|
+
* It's recommended to use the "send" method for a higher level API.
|
|
541
|
+
* @param {Buffer} apdu - The data to send.
|
|
542
|
+
* @returns {Promise<Buffer>} A promise that resolves with the response data from the device.
|
|
472
543
|
*/
|
|
473
544
|
exchange = (apdu: Buffer): Promise<any> =>
|
|
474
545
|
this.exchangeAtomicImpl(async () => {
|
|
475
546
|
try {
|
|
476
547
|
const msgIn = apdu.toString("hex");
|
|
477
548
|
log("apdu", `=> ${msgIn}`);
|
|
549
|
+
|
|
478
550
|
const data = await merge(
|
|
479
|
-
// $FlowFixMe
|
|
480
551
|
this.notifyObservable.pipe(receiveAPDU),
|
|
481
552
|
sendAPDU(this.write, apdu, this.mtuSize)
|
|
482
553
|
).toPromise();
|
|
554
|
+
|
|
483
555
|
const msgOut = data.toString("hex");
|
|
484
556
|
log("apdu", `<= ${msgOut}`);
|
|
557
|
+
|
|
485
558
|
return data;
|
|
486
559
|
} catch (e: any) {
|
|
487
560
|
log("ble-error", "exchange got " + String(e));
|
|
@@ -497,26 +570,30 @@ export default class BluetoothTransport extends Transport {
|
|
|
497
570
|
}
|
|
498
571
|
});
|
|
499
572
|
|
|
500
|
-
|
|
501
|
-
|
|
573
|
+
/**
|
|
574
|
+
* Negotiate with the device the maximum transfer unit for the ble frames
|
|
575
|
+
* @returns Promise<number>
|
|
576
|
+
*/
|
|
577
|
+
async inferMTU(): Promise<number> {
|
|
502
578
|
let { mtu } = this.device;
|
|
579
|
+
|
|
503
580
|
await this.exchangeAtomicImpl(async () => {
|
|
504
581
|
try {
|
|
505
|
-
mtu =
|
|
506
|
-
(
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
).toPromise()) + 3;
|
|
582
|
+
mtu = await merge(
|
|
583
|
+
this.notifyObservable.pipe(
|
|
584
|
+
tap((maybeError) => {
|
|
585
|
+
if (maybeError instanceof Error) throw maybeError;
|
|
586
|
+
}),
|
|
587
|
+
first((buffer) => buffer.readUInt8(0) === 0x08),
|
|
588
|
+
map((buffer) => buffer.readUInt8(5))
|
|
589
|
+
),
|
|
590
|
+
defer(() => from(this.write(Buffer.from([0x08, 0, 0, 0, 0])))).pipe(
|
|
591
|
+
ignoreElements()
|
|
592
|
+
)
|
|
593
|
+
).toPromise();
|
|
518
594
|
} catch (e: any) {
|
|
519
|
-
log("ble-error", "inferMTU got " +
|
|
595
|
+
log("ble-error", "inferMTU got " + JSON.stringify(e));
|
|
596
|
+
|
|
520
597
|
await bleManagerInstance()
|
|
521
598
|
.cancelDeviceConnection(this.id)
|
|
522
599
|
.catch(() => {}); // but we ignore if disconnect worked.
|
|
@@ -525,33 +602,38 @@ export default class BluetoothTransport extends Transport {
|
|
|
525
602
|
}
|
|
526
603
|
});
|
|
527
604
|
|
|
528
|
-
if (mtu >
|
|
529
|
-
|
|
530
|
-
log(
|
|
531
|
-
"ble-verbose",
|
|
532
|
-
`BleTransport(${String(this.id)}) mtu set to ${String(mtuSize)}`
|
|
533
|
-
);
|
|
534
|
-
this.mtuSize = mtuSize;
|
|
605
|
+
if (mtu > 20) {
|
|
606
|
+
this.mtuSize = mtu;
|
|
607
|
+
log(TAG, `BleTransport(${this.id}) mtu set to ${this.mtuSize}`);
|
|
535
608
|
}
|
|
536
609
|
|
|
537
610
|
return this.mtuSize;
|
|
538
611
|
}
|
|
539
612
|
|
|
613
|
+
/**
|
|
614
|
+
* Exposed method from the ble-plx library
|
|
615
|
+
* Request the connection priority for the given device.
|
|
616
|
+
* @param {"Balanced" | "High" | "LowPower"} connectionPriority: Connection priority.
|
|
617
|
+
* @returns {Promise<Device>} Connected device.
|
|
618
|
+
*/
|
|
540
619
|
async requestConnectionPriority(
|
|
541
620
|
connectionPriority: "Balanced" | "High" | "LowPower"
|
|
542
|
-
) {
|
|
543
|
-
await decoratePromiseErrors(
|
|
621
|
+
): Promise<Device> {
|
|
622
|
+
return await decoratePromiseErrors(
|
|
544
623
|
this.device.requestConnectionPriority(
|
|
545
|
-
ConnectionPriority[connectionPriority]
|
|
624
|
+
ConnectionPriority[connectionPriority as keyof ConnectionPriority]
|
|
546
625
|
)
|
|
547
626
|
);
|
|
548
627
|
}
|
|
549
628
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
629
|
+
/**
|
|
630
|
+
* Do not call this directly unless you know what you're doing. Communication
|
|
631
|
+
* with a Ledger device should be through the {@link exchange} method.
|
|
632
|
+
* @param buffer
|
|
633
|
+
* @param txid
|
|
634
|
+
*/
|
|
635
|
+
write = async (buffer: Buffer, txid?: string | undefined): Promise<void> => {
|
|
553
636
|
log("ble-frame", "=> " + buffer.toString("hex"));
|
|
554
|
-
|
|
555
637
|
if (!this.writeCmdCharacteristic) {
|
|
556
638
|
try {
|
|
557
639
|
await this.writeCharacteristic.writeWithResponse(
|
|
@@ -573,32 +655,42 @@ export default class BluetoothTransport extends Transport {
|
|
|
573
655
|
}
|
|
574
656
|
};
|
|
575
657
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
658
|
+
/**
|
|
659
|
+
* We intentionally do not immediately close a transport connection.
|
|
660
|
+
* Instead, we queue the disconnect and wait for a future connection to dismiss the event.
|
|
661
|
+
* This approach prevents unnecessary disconnects and reconnects. We use the isConnected
|
|
662
|
+
* flag to ensure that we do not trigger a disconnect if the current cached transport has
|
|
663
|
+
* already been disconnected.
|
|
664
|
+
* @returns {Promise<void>}
|
|
665
|
+
*/
|
|
666
|
+
async close(): Promise<void> {
|
|
667
|
+
let resolve: (value: void | PromiseLike<void>) => void;
|
|
668
|
+
const disconnectPromise = new Promise<void>((innerResolve) => {
|
|
669
|
+
resolve = innerResolve;
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
clearDisconnectTimeout(this.id);
|
|
673
|
+
|
|
674
|
+
log(TAG, "Queuing a disconnect");
|
|
675
|
+
|
|
676
|
+
this.disconnectTimeout = setTimeout(() => {
|
|
677
|
+
log(TAG, `Triggering a disconnect from ${this.id}`);
|
|
678
|
+
if (this.isConnected) {
|
|
679
|
+
BleTransport.disconnect(this.id)
|
|
680
|
+
.catch(() => {})
|
|
681
|
+
.finally(resolve);
|
|
682
|
+
} else {
|
|
683
|
+
resolve();
|
|
684
|
+
}
|
|
685
|
+
}, BleTransport.disconnectTimeoutMs);
|
|
686
|
+
|
|
687
|
+
// The closure will occur no later than 5s, triggered either by disconnection
|
|
688
|
+
// or the actual response of the apdu.
|
|
689
|
+
await Promise.race([
|
|
690
|
+
this.exchangeBusyPromise || Promise.resolve(),
|
|
691
|
+
disconnectPromise,
|
|
692
|
+
]);
|
|
693
|
+
|
|
694
|
+
return;
|
|
580
695
|
}
|
|
581
696
|
}
|
|
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
|
-
};
|