@onekeyfe/hd-transport-react-native 1.2.0-alpha.53 → 1.2.0-alpha.54
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/index.d.ts +38 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +160 -74
- package/jest.config.js +5 -0
- package/package.json +5 -5
- package/src/__tests__/protocolV2Link.test.ts +41 -230
- package/src/__tests__/staleCallTimeout.test.ts +211 -0
- package/src/__tests__/writePacketTimeout.test.ts +306 -0
- package/src/index.ts +235 -74
- package/src/__tests__/enumerate.test.ts +0 -132
package/src/index.ts
CHANGED
|
@@ -28,7 +28,6 @@ import {
|
|
|
28
28
|
HardwareErrorCode,
|
|
29
29
|
createDeferred,
|
|
30
30
|
isOnekeyBluetoothDevice,
|
|
31
|
-
isPro2FindMyAdvertisementName,
|
|
32
31
|
} from '@onekeyfe/hd-shared';
|
|
33
32
|
|
|
34
33
|
import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
|
|
@@ -61,7 +60,6 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
|
|
|
61
60
|
const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
|
|
62
61
|
const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
|
|
63
62
|
const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
|
|
64
|
-
const IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS = 5;
|
|
65
63
|
const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
|
|
66
64
|
const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY =
|
|
67
65
|
Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
|
|
@@ -108,6 +106,22 @@ const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, max
|
|
|
108
106
|
Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
|
|
109
107
|
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
110
108
|
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10_000;
|
|
109
|
+
/**
|
|
110
|
+
* Per-packet write budget. iOS only resolves writeWithoutResponse once CoreBluetooth
|
|
111
|
+
* reports the peripheral ready again; a peripheral wedged by its own firmware reboot
|
|
112
|
+
* stops reporting ready while staying connected, so the write promise never settles.
|
|
113
|
+
* Response timeouts cannot cover that — they are armed after the writes complete —
|
|
114
|
+
* and an unbounded write leaves the whole transport unusable until the process dies.
|
|
115
|
+
* A healthy packet completes in milliseconds, so this only fires on a dead link.
|
|
116
|
+
*/
|
|
117
|
+
export const BLE_WRITE_PACKET_TIMEOUT_MS = 10_000;
|
|
118
|
+
const WEDGED_WRITE_MESSAGE = 'BLE write timeout after';
|
|
119
|
+
const isWedgedWriteError = (error: unknown): boolean =>
|
|
120
|
+
(error as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleWriteCharacteristicError &&
|
|
121
|
+
typeof (error as { message?: unknown })?.message === 'string' &&
|
|
122
|
+
(error as { message: string }).message.startsWith(WEDGED_WRITE_MESSAGE);
|
|
123
|
+
/** Consecutive wedged writes on one device before the BLE manager itself is recreated. */
|
|
124
|
+
export const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
111
125
|
const DEVICE_SCAN_TIMEOUT_MS = 3000;
|
|
112
126
|
const IOS_NOTIFY_READY_DELAY_MS = 150;
|
|
113
127
|
const ANDROID_NOTIFY_READY_DELAY_MS = 300;
|
|
@@ -258,6 +272,17 @@ export default class ReactNativeBleTransport {
|
|
|
258
272
|
/** Per-device protocol type detected by active wire-level probe after connect. */
|
|
259
273
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
260
274
|
|
|
275
|
+
/**
|
|
276
|
+
* Protocol a probe is currently trying, before the device has confirmed it. Calls
|
|
277
|
+
* must route with it, but acquire() must not treat it as a detected protocol: a
|
|
278
|
+
* probe that never answers would otherwise leave the reuse fast path handing out a
|
|
279
|
+
* transport that was never validated.
|
|
280
|
+
*/
|
|
281
|
+
private probingProtocols: Map<string, ProtocolType> = new Map();
|
|
282
|
+
|
|
283
|
+
/** Consecutive write timeouts per device; reset by any write that completes. */
|
|
284
|
+
private writeTimeoutCounts: Map<string, number> = new Map();
|
|
285
|
+
|
|
261
286
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
262
287
|
|
|
263
288
|
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
@@ -582,21 +607,12 @@ export default class ReactNativeBleTransport {
|
|
|
582
607
|
}
|
|
583
608
|
|
|
584
609
|
const displayName = getDeviceDisplayName(device);
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
const isOneKey =
|
|
592
|
-
!isUnnamedIOSPeripheral &&
|
|
593
|
-
!isFindMyPeripheral &&
|
|
594
|
-
isOnekeyBluetoothDevice({
|
|
595
|
-
id: device?.id,
|
|
596
|
-
name: device?.name,
|
|
597
|
-
localName: device?.localName,
|
|
598
|
-
serviceUuids: device?.serviceUUIDs,
|
|
599
|
-
});
|
|
610
|
+
const isOneKey = isOnekeyBluetoothDevice({
|
|
611
|
+
id: device?.id,
|
|
612
|
+
name: device?.name,
|
|
613
|
+
localName: device?.localName,
|
|
614
|
+
serviceUuids: device?.serviceUUIDs,
|
|
615
|
+
});
|
|
600
616
|
if (isOneKey) {
|
|
601
617
|
addDevice(device as unknown as Device);
|
|
602
618
|
} else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
|
|
@@ -617,12 +633,7 @@ export default class ReactNativeBleTransport {
|
|
|
617
633
|
'localName' in device && typeof device.localName === 'string'
|
|
618
634
|
? device.localName
|
|
619
635
|
: null;
|
|
620
|
-
const isFindMyPeripheral =
|
|
621
|
-
isPro2FindMyAdvertisementName(device.name) ||
|
|
622
|
-
isPro2FindMyAdvertisementName(localName);
|
|
623
|
-
|
|
624
636
|
if (
|
|
625
|
-
!isFindMyPeripheral &&
|
|
626
637
|
isOnekeyBluetoothDevice({
|
|
627
638
|
id: device.id,
|
|
628
639
|
name: device.name,
|
|
@@ -895,7 +906,7 @@ export default class ReactNativeBleTransport {
|
|
|
895
906
|
Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
|
|
896
907
|
return;
|
|
897
908
|
}
|
|
898
|
-
if (this.
|
|
909
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
899
910
|
let errorCode:
|
|
900
911
|
| typeof HardwareErrorCode.BleDeviceBondError
|
|
901
912
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
@@ -965,7 +976,7 @@ export default class ReactNativeBleTransport {
|
|
|
965
976
|
|
|
966
977
|
try {
|
|
967
978
|
const data = Buffer.from(c.value as string, 'base64');
|
|
968
|
-
const protocol = this.
|
|
979
|
+
const protocol = this.getActiveProtocol(uuid);
|
|
969
980
|
if (!protocol) {
|
|
970
981
|
Log?.debug('monitor data ignored before protocol detection: ', uuid);
|
|
971
982
|
return;
|
|
@@ -999,7 +1010,7 @@ export default class ReactNativeBleTransport {
|
|
|
999
1010
|
} catch (error) {
|
|
1000
1011
|
Log?.debug('monitor data error: ', error);
|
|
1001
1012
|
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1002
|
-
if (this.
|
|
1013
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
1003
1014
|
this.rejectProtocolV2Frames(uuid, notifyError);
|
|
1004
1015
|
} else if (this.runPromiseDeviceId === uuid) {
|
|
1005
1016
|
this.runPromise?.reject(notifyError);
|
|
@@ -1063,6 +1074,7 @@ export default class ReactNativeBleTransport {
|
|
|
1063
1074
|
}
|
|
1064
1075
|
|
|
1065
1076
|
this.deviceProtocol.delete(uuid);
|
|
1077
|
+
this.probingProtocols.delete(uuid);
|
|
1066
1078
|
// Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
|
|
1067
1079
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1068
1080
|
this.protocolV2Assemblers.delete(uuid);
|
|
@@ -1129,8 +1141,25 @@ export default class ReactNativeBleTransport {
|
|
|
1129
1141
|
const transport = this.getCachedTransport(uuid);
|
|
1130
1142
|
const runPromise = createDeferred<string>();
|
|
1131
1143
|
runPromise.promise.catch(() => undefined);
|
|
1144
|
+
const supersededRunPromise = this.runPromise;
|
|
1145
|
+
if (supersededRunPromise) {
|
|
1146
|
+
// Only forceRun calls (Initialize/Cancel) reach here with a pending call. Settle
|
|
1147
|
+
// the superseded deferred now so its response race resolves and its finally block
|
|
1148
|
+
// clears its timeout timer; an orphaned timer would otherwise fire much later and
|
|
1149
|
+
// tear down the shared connection while another call is using it.
|
|
1150
|
+
supersededRunPromise.reject(ERRORS.TypedError(HardwareErrorCode.BleForceCleanRunPromise));
|
|
1151
|
+
}
|
|
1132
1152
|
this.runPromise = runPromise;
|
|
1133
1153
|
this.runPromiseDeviceId = uuid;
|
|
1154
|
+
// A superseded call's late write failure must not clear the successor's ownership;
|
|
1155
|
+
// only the call that still owns the slot may release it.
|
|
1156
|
+
const releaseOwnershipIfCurrent = () => {
|
|
1157
|
+
if (this.runPromise === runPromise) {
|
|
1158
|
+
this.runPromise = null;
|
|
1159
|
+
this.runPromiseDeviceId = null;
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
const isCurrentOwner = () => this.runPromise === runPromise;
|
|
1134
1163
|
const messages = this._messages;
|
|
1135
1164
|
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
1136
1165
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -1156,6 +1185,9 @@ export default class ReactNativeBleTransport {
|
|
|
1156
1185
|
chunk = ByteBuffer.allocate(packetCapacity);
|
|
1157
1186
|
} catch (e) {
|
|
1158
1187
|
onError(e);
|
|
1188
|
+
if (isWedgedWriteError(e)) {
|
|
1189
|
+
throw e;
|
|
1190
|
+
}
|
|
1159
1191
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1160
1192
|
}
|
|
1161
1193
|
}
|
|
@@ -1187,6 +1219,9 @@ export default class ReactNativeBleTransport {
|
|
|
1187
1219
|
}
|
|
1188
1220
|
} catch (e) {
|
|
1189
1221
|
onError(e);
|
|
1222
|
+
if (isWedgedWriteError(e)) {
|
|
1223
|
+
throw e;
|
|
1224
|
+
}
|
|
1190
1225
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1191
1226
|
}
|
|
1192
1227
|
}
|
|
@@ -1200,9 +1235,15 @@ export default class ReactNativeBleTransport {
|
|
|
1200
1235
|
if (name === 'EmmcFileWrite') {
|
|
1201
1236
|
await writeChunkedData(
|
|
1202
1237
|
buffers,
|
|
1203
|
-
data =>
|
|
1238
|
+
data =>
|
|
1239
|
+
this.writeBlePacket(
|
|
1240
|
+
uuid,
|
|
1241
|
+
data,
|
|
1242
|
+
payload => transport.writeWithRetry(payload),
|
|
1243
|
+
isCurrentOwner
|
|
1244
|
+
),
|
|
1204
1245
|
e => {
|
|
1205
|
-
|
|
1246
|
+
releaseOwnershipIfCurrent();
|
|
1206
1247
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1207
1248
|
}
|
|
1208
1249
|
);
|
|
@@ -1223,7 +1264,12 @@ export default class ReactNativeBleTransport {
|
|
|
1223
1264
|
// eslint-disable-next-line no-constant-condition
|
|
1224
1265
|
while (true) {
|
|
1225
1266
|
try {
|
|
1226
|
-
await
|
|
1267
|
+
await this.writeBlePacket(
|
|
1268
|
+
uuid,
|
|
1269
|
+
data,
|
|
1270
|
+
payload => transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
1271
|
+
isCurrentOwner
|
|
1272
|
+
);
|
|
1227
1273
|
return;
|
|
1228
1274
|
} catch (error) {
|
|
1229
1275
|
const retryType = getFirmwareUploadWriteRetryType(error);
|
|
@@ -1242,7 +1288,7 @@ export default class ReactNativeBleTransport {
|
|
|
1242
1288
|
}
|
|
1243
1289
|
},
|
|
1244
1290
|
e => {
|
|
1245
|
-
|
|
1291
|
+
releaseOwnershipIfCurrent();
|
|
1246
1292
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1247
1293
|
}
|
|
1248
1294
|
);
|
|
@@ -1251,16 +1297,18 @@ export default class ReactNativeBleTransport {
|
|
|
1251
1297
|
const outData = o.toString('base64');
|
|
1252
1298
|
// Upload resources on low-end phones may OOM
|
|
1253
1299
|
try {
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
}
|
|
1300
|
+
await this.writeBlePacket(
|
|
1301
|
+
uuid,
|
|
1302
|
+
outData,
|
|
1303
|
+
payload => transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
1304
|
+
isCurrentOwner
|
|
1305
|
+
);
|
|
1261
1306
|
} catch (e) {
|
|
1262
1307
|
Log?.debug('writeCharacteristic write error: ', e);
|
|
1263
|
-
|
|
1308
|
+
releaseOwnershipIfCurrent();
|
|
1309
|
+
if (isWedgedWriteError(e)) {
|
|
1310
|
+
throw e;
|
|
1311
|
+
}
|
|
1264
1312
|
if (e.errorCode === BleErrorCode.DeviceDisconnected) {
|
|
1265
1313
|
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded);
|
|
1266
1314
|
} else if (e.errorCode === BleErrorCode.OperationStartFailed) {
|
|
@@ -1303,8 +1351,13 @@ export default class ReactNativeBleTransport {
|
|
|
1303
1351
|
}
|
|
1304
1352
|
const isProbeTimeout =
|
|
1305
1353
|
name === 'GetFeatures' && options?.timeoutMs === PROTOCOL_PROBE_TIMEOUT_MS;
|
|
1354
|
+
// A call that has been superseded (forceRun) or cleaned up no longer owns the
|
|
1355
|
+
// transport; its late timeout must not tear down the connection the current
|
|
1356
|
+
// call is actively using.
|
|
1357
|
+
const isStaleCall = this.runPromise !== runPromise;
|
|
1306
1358
|
if (
|
|
1307
1359
|
!isProbeTimeout &&
|
|
1360
|
+
!isStaleCall &&
|
|
1308
1361
|
(e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError
|
|
1309
1362
|
) {
|
|
1310
1363
|
await this.disconnect(uuid);
|
|
@@ -1384,6 +1437,7 @@ export default class ReactNativeBleTransport {
|
|
|
1384
1437
|
delete transportCache[session];
|
|
1385
1438
|
}
|
|
1386
1439
|
this.deviceProtocol.delete(session);
|
|
1440
|
+
this.probingProtocols.delete(session);
|
|
1387
1441
|
this.deviceProtocolHints.delete(session);
|
|
1388
1442
|
this.protocolV2Assemblers.delete(session);
|
|
1389
1443
|
this.resetProtocolV2Frames(session);
|
|
@@ -1418,6 +1472,105 @@ export default class ReactNativeBleTransport {
|
|
|
1418
1472
|
return transport;
|
|
1419
1473
|
}
|
|
1420
1474
|
|
|
1475
|
+
/**
|
|
1476
|
+
* Write one packet under a bounded budget. A write that never settles means the
|
|
1477
|
+
* peripheral is wedged even though the GATT link still reports connected, so the
|
|
1478
|
+
* link is torn down: releasing JS state alone would leave the poisoned peripheral
|
|
1479
|
+
* cached and every later call would hang on it again.
|
|
1480
|
+
*/
|
|
1481
|
+
private async writeBlePacket(
|
|
1482
|
+
uuid: string,
|
|
1483
|
+
data: string,
|
|
1484
|
+
write: (payload: string) => Promise<unknown>,
|
|
1485
|
+
isCurrentOwner?: () => boolean
|
|
1486
|
+
) {
|
|
1487
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1488
|
+
let timedOut = false;
|
|
1489
|
+
try {
|
|
1490
|
+
await Promise.race([
|
|
1491
|
+
write(data),
|
|
1492
|
+
new Promise<never>((_, reject) => {
|
|
1493
|
+
timer = setTimeout(() => {
|
|
1494
|
+
timedOut = true;
|
|
1495
|
+
reject(
|
|
1496
|
+
ERRORS.TypedError(
|
|
1497
|
+
HardwareErrorCode.BleWriteCharacteristicError,
|
|
1498
|
+
`BLE write timeout after ${BLE_WRITE_PACKET_TIMEOUT_MS}ms`
|
|
1499
|
+
)
|
|
1500
|
+
);
|
|
1501
|
+
}, BLE_WRITE_PACKET_TIMEOUT_MS);
|
|
1502
|
+
}),
|
|
1503
|
+
]);
|
|
1504
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1505
|
+
} catch (error) {
|
|
1506
|
+
if (timedOut) {
|
|
1507
|
+
// A superseded call's late write must not tear down the link the current
|
|
1508
|
+
// call is using; only the owner of the transport may declare it dead.
|
|
1509
|
+
if (isCurrentOwner && !isCurrentOwner()) {
|
|
1510
|
+
Log?.debug('[ReactNativeBleTransport] stale BLE write timed out, link kept:', uuid);
|
|
1511
|
+
} else {
|
|
1512
|
+
this.tearDownWedgedLink(uuid);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
throw error;
|
|
1516
|
+
} finally {
|
|
1517
|
+
if (timer) clearTimeout(timer);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
/**
|
|
1522
|
+
* Drop a link whose writes stopped completing. The JS state is purged synchronously
|
|
1523
|
+
* so the next acquire() cannot reuse the dead transport, while the native teardown is
|
|
1524
|
+
* intentionally NOT awaited: it talks to the very layer that just stopped settling
|
|
1525
|
+
* promises, so awaiting it could hang exactly like the write it is recovering from.
|
|
1526
|
+
*/
|
|
1527
|
+
private tearDownWedgedLink(uuid: string) {
|
|
1528
|
+
const timeouts = (this.writeTimeoutCounts.get(uuid) ?? 0) + 1;
|
|
1529
|
+
this.writeTimeoutCounts.set(uuid, timeouts);
|
|
1530
|
+
Log?.error('[ReactNativeBleTransport] BLE write timed out, tearing down link:', uuid, {
|
|
1531
|
+
consecutiveWriteTimeouts: timeouts,
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
const wedged = transportCache[uuid];
|
|
1535
|
+
this.disconnect(uuid).catch(error => {
|
|
1536
|
+
Log?.debug('[ReactNativeBleTransport] wedged link teardown failed (ignored):', error);
|
|
1537
|
+
});
|
|
1538
|
+
if (wedged && transportCache[uuid] === wedged) {
|
|
1539
|
+
delete transportCache[uuid];
|
|
1540
|
+
}
|
|
1541
|
+
this.deviceProtocol.delete(uuid);
|
|
1542
|
+
this.probingProtocols.delete(uuid);
|
|
1543
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
1544
|
+
this.resetProtocolV2Frames(uuid);
|
|
1545
|
+
|
|
1546
|
+
if (timeouts >= BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1547
|
+
// Reconnecting reuses the same native peripheral object. When it stays wedged
|
|
1548
|
+
// across attempts the poison lives in the BLE manager itself, and only a fresh
|
|
1549
|
+
// manager drops every cached peripheral — the JS equivalent of restarting the app.
|
|
1550
|
+
Log?.error('[ReactNativeBleTransport] BLE writes wedged repeatedly, resetting BLE manager');
|
|
1551
|
+
this.resetPlxManager();
|
|
1552
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
private resetPlxManager() {
|
|
1557
|
+
const manager = this.blePlxManager;
|
|
1558
|
+
this.blePlxManager = undefined;
|
|
1559
|
+
// Every cached transport belongs to the destroyed manager's peripherals.
|
|
1560
|
+
Object.keys(transportCache).forEach(key => {
|
|
1561
|
+
delete transportCache[key];
|
|
1562
|
+
});
|
|
1563
|
+
this.deviceProtocol.clear();
|
|
1564
|
+
this.probingProtocols.clear();
|
|
1565
|
+
this.monitorTokens.clear();
|
|
1566
|
+
this.protocolV2Assemblers.clear();
|
|
1567
|
+
try {
|
|
1568
|
+
manager?.destroy();
|
|
1569
|
+
} catch (error) {
|
|
1570
|
+
Log?.debug('[ReactNativeBleTransport] BLE manager destroy failed (ignored):', error);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1421
1574
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
1422
1575
|
return ERRORS.TypedError(
|
|
1423
1576
|
HardwareErrorCode.RuntimeError,
|
|
@@ -1433,34 +1586,25 @@ export default class ReactNativeBleTransport {
|
|
|
1433
1586
|
}
|
|
1434
1587
|
|
|
1435
1588
|
private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
|
|
1589
|
+
if (this.probingProtocols.get(uuid) === protocol) {
|
|
1590
|
+
this.probingProtocols.delete(uuid);
|
|
1591
|
+
}
|
|
1436
1592
|
if (this.deviceProtocol.get(uuid) === protocol) {
|
|
1437
1593
|
this.deviceProtocol.delete(uuid);
|
|
1438
1594
|
}
|
|
1439
1595
|
}
|
|
1440
1596
|
|
|
1597
|
+
/** Protocol to route a call with: confirmed if known, otherwise the one being probed. */
|
|
1598
|
+
private getActiveProtocol(uuid: string): ProtocolType | undefined {
|
|
1599
|
+
return this.deviceProtocol.get(uuid) ?? this.probingProtocols.get(uuid);
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1441
1602
|
private async detectProtocol(
|
|
1442
1603
|
uuid: string,
|
|
1443
1604
|
expectedProtocol?: ProtocolType,
|
|
1444
1605
|
protocolHint?: ProtocolType,
|
|
1445
1606
|
rebuildTransport?: () => Promise<void>
|
|
1446
1607
|
): Promise<ProtocolType> {
|
|
1447
|
-
if (Platform.OS === 'ios') {
|
|
1448
|
-
const protocol = expectedProtocol ?? protocolHint ?? 'V1';
|
|
1449
|
-
let source = 'ios-legacy-default';
|
|
1450
|
-
if (expectedProtocol) {
|
|
1451
|
-
source = 'expected';
|
|
1452
|
-
} else if (protocolHint) {
|
|
1453
|
-
source = 'hint';
|
|
1454
|
-
}
|
|
1455
|
-
this.deviceProtocol.set(uuid, protocol);
|
|
1456
|
-
Log?.debug('[ReactNativeBleTransport] protocol selected', {
|
|
1457
|
-
deviceId: uuid,
|
|
1458
|
-
protocol,
|
|
1459
|
-
source,
|
|
1460
|
-
});
|
|
1461
|
-
return protocol;
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
1608
|
if (expectedProtocol === 'V1') {
|
|
1465
1609
|
if (await this.probeProtocolV1(uuid)) {
|
|
1466
1610
|
this.deviceProtocol.set(uuid, 'V1');
|
|
@@ -1518,6 +1662,7 @@ export default class ReactNativeBleTransport {
|
|
|
1518
1662
|
}
|
|
1519
1663
|
|
|
1520
1664
|
this.deviceProtocol.delete(uuid);
|
|
1665
|
+
this.probingProtocols.delete(uuid);
|
|
1521
1666
|
throw this.createProtocolDetectionError();
|
|
1522
1667
|
}
|
|
1523
1668
|
|
|
@@ -1579,14 +1724,20 @@ export default class ReactNativeBleTransport {
|
|
|
1579
1724
|
}
|
|
1580
1725
|
|
|
1581
1726
|
try {
|
|
1582
|
-
this.
|
|
1727
|
+
this.probingProtocols.set(uuid, 'V1');
|
|
1583
1728
|
// GetFeatures identifies Protocol V1 without resetting an existing wallet
|
|
1584
1729
|
// session before Core has a chance to restore a hidden wallet.
|
|
1585
1730
|
await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
1731
|
+
this.probingProtocols.delete(uuid);
|
|
1586
1732
|
return true;
|
|
1587
1733
|
} catch (error) {
|
|
1588
1734
|
this.clearProbeProtocol(uuid, 'V1');
|
|
1589
1735
|
Log?.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
|
|
1736
|
+
// A wedged write already dropped the link, so probing another protocol on it
|
|
1737
|
+
// would only fail against a torn-down transport: surface the real cause.
|
|
1738
|
+
if (isWedgedWriteError(error)) {
|
|
1739
|
+
throw error;
|
|
1740
|
+
}
|
|
1590
1741
|
return false;
|
|
1591
1742
|
}
|
|
1592
1743
|
}
|
|
@@ -1596,7 +1747,7 @@ export default class ReactNativeBleTransport {
|
|
|
1596
1747
|
return false;
|
|
1597
1748
|
}
|
|
1598
1749
|
|
|
1599
|
-
this.
|
|
1750
|
+
this.probingProtocols.set(uuid, 'V2');
|
|
1600
1751
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1601
1752
|
const detected = await probeProtocolV2Helper({
|
|
1602
1753
|
call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
|
|
@@ -1611,6 +1762,8 @@ export default class ReactNativeBleTransport {
|
|
|
1611
1762
|
});
|
|
1612
1763
|
if (!detected) {
|
|
1613
1764
|
this.clearProbeProtocol(uuid, 'V2');
|
|
1765
|
+
} else {
|
|
1766
|
+
this.probingProtocols.delete(uuid);
|
|
1614
1767
|
}
|
|
1615
1768
|
return detected;
|
|
1616
1769
|
}
|
|
@@ -1692,15 +1845,12 @@ export default class ReactNativeBleTransport {
|
|
|
1692
1845
|
}
|
|
1693
1846
|
|
|
1694
1847
|
private async writeProtocolV2Packet(
|
|
1848
|
+
uuid: string,
|
|
1695
1849
|
transport: BleTransport,
|
|
1696
1850
|
base64: string,
|
|
1697
1851
|
context: ProtocolV2CallContext,
|
|
1698
1852
|
assertCurrentGeneration: () => void
|
|
1699
1853
|
) {
|
|
1700
|
-
const shouldUseWriteWithResponse =
|
|
1701
|
-
Platform.OS === 'ios' &&
|
|
1702
|
-
!context.highVolume &&
|
|
1703
|
-
transport.writeCharacteristic.isWritableWithResponse;
|
|
1704
1854
|
let attempt = 0;
|
|
1705
1855
|
for (;;) {
|
|
1706
1856
|
assertCurrentGeneration();
|
|
@@ -1708,11 +1858,21 @@ export default class ReactNativeBleTransport {
|
|
|
1708
1858
|
throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
|
|
1709
1859
|
}
|
|
1710
1860
|
try {
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1861
|
+
await this.writeBlePacket(
|
|
1862
|
+
uuid,
|
|
1863
|
+
base64,
|
|
1864
|
+
payload => transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
1865
|
+
// Same rule as Protocol V1: a write from a superseded generation must not
|
|
1866
|
+
// tear down the link that the current generation is using.
|
|
1867
|
+
() => {
|
|
1868
|
+
try {
|
|
1869
|
+
assertCurrentGeneration();
|
|
1870
|
+
return !context.signal.aborted;
|
|
1871
|
+
} catch {
|
|
1872
|
+
return false;
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
);
|
|
1716
1876
|
assertCurrentGeneration();
|
|
1717
1877
|
return;
|
|
1718
1878
|
} catch (error) {
|
|
@@ -1735,6 +1895,7 @@ export default class ReactNativeBleTransport {
|
|
|
1735
1895
|
}
|
|
1736
1896
|
|
|
1737
1897
|
private async writeProtocolV2Frame(
|
|
1898
|
+
uuid: string,
|
|
1738
1899
|
transport: BleTransport,
|
|
1739
1900
|
frame: Uint8Array,
|
|
1740
1901
|
context: ProtocolV2CallContext,
|
|
@@ -1747,25 +1908,19 @@ export default class ReactNativeBleTransport {
|
|
|
1747
1908
|
androidPacketLength: tuning.androidPacketLength,
|
|
1748
1909
|
mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
|
|
1749
1910
|
});
|
|
1750
|
-
// Match Desktop BLE pacing so Pro2 firmware can finish the previous response
|
|
1751
|
-
// before the next single-packet control command is written.
|
|
1752
|
-
const initialDelayMs =
|
|
1753
|
-
Platform.OS === 'ios' && !context.highVolume && frame.length <= packetCapacity
|
|
1754
|
-
? IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS
|
|
1755
|
-
: 0;
|
|
1756
1911
|
await writeProtocolV2BleFrame({
|
|
1757
1912
|
frame,
|
|
1758
1913
|
packetCapacity,
|
|
1759
1914
|
assertActive: assertCurrentGeneration,
|
|
1760
1915
|
signal: context.signal,
|
|
1761
1916
|
abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
|
|
1762
|
-
initialDelayMs,
|
|
1763
1917
|
burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
|
|
1764
1918
|
burstPauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
|
|
1765
1919
|
flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
|
|
1766
1920
|
wait: delay,
|
|
1767
1921
|
writePacket: packet =>
|
|
1768
1922
|
this.writeProtocolV2Packet(
|
|
1923
|
+
uuid,
|
|
1769
1924
|
transport,
|
|
1770
1925
|
Buffer.from(packet).toString('base64'),
|
|
1771
1926
|
context,
|
|
@@ -1830,7 +1985,13 @@ export default class ReactNativeBleTransport {
|
|
|
1830
1985
|
writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
|
|
1831
1986
|
assertCurrentGeneration();
|
|
1832
1987
|
const currentTransport = this.getCachedTransport(uuid);
|
|
1833
|
-
await this.writeProtocolV2Frame(
|
|
1988
|
+
await this.writeProtocolV2Frame(
|
|
1989
|
+
uuid,
|
|
1990
|
+
currentTransport,
|
|
1991
|
+
frame,
|
|
1992
|
+
context,
|
|
1993
|
+
assertCurrentGeneration
|
|
1994
|
+
);
|
|
1834
1995
|
},
|
|
1835
1996
|
readFrame: async () => {
|
|
1836
1997
|
assertCurrentGeneration();
|
|
@@ -1855,6 +2016,6 @@ export default class ReactNativeBleTransport {
|
|
|
1855
2016
|
}
|
|
1856
2017
|
|
|
1857
2018
|
getProtocolType(path: string): ProtocolType | undefined {
|
|
1858
|
-
return this.
|
|
2019
|
+
return this.getActiveProtocol(path);
|
|
1859
2020
|
}
|
|
1860
2021
|
}
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from 'events';
|
|
2
|
-
|
|
3
|
-
import { getConnectedDeviceIds } from '../BleManager';
|
|
4
|
-
import ReactNativeBleTransport from '../index';
|
|
5
|
-
|
|
6
|
-
jest.mock(
|
|
7
|
-
'react-native',
|
|
8
|
-
() => ({
|
|
9
|
-
PermissionsAndroid: {},
|
|
10
|
-
Platform: { OS: 'ios' },
|
|
11
|
-
}),
|
|
12
|
-
{ virtual: true }
|
|
13
|
-
);
|
|
14
|
-
|
|
15
|
-
jest.mock('react-native-ble-plx', () => ({
|
|
16
|
-
BleError: class BleError extends Error {},
|
|
17
|
-
BleErrorCode: {},
|
|
18
|
-
BleManager: jest.fn(),
|
|
19
|
-
ScanMode: { LowLatency: 2 },
|
|
20
|
-
}));
|
|
21
|
-
|
|
22
|
-
jest.mock('../BleManager', () => ({
|
|
23
|
-
getConnectedDeviceIds: jest.fn(),
|
|
24
|
-
onDeviceBondState: jest.fn(),
|
|
25
|
-
pairDevice: jest.fn(),
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
jest.mock('../subscribeBleOn', () => ({
|
|
29
|
-
subscribeBleOn: jest.fn(() => Promise.resolve()),
|
|
30
|
-
}));
|
|
31
|
-
|
|
32
|
-
const ONEKEY_SERVICE_UUID = '00000001-0000-1000-8000-00805f9b34fb';
|
|
33
|
-
|
|
34
|
-
describe('ReactNativeBleTransport iOS discovery', () => {
|
|
35
|
-
test('filters a bonded Pro2 Find My peripheral while keeping the wallet peripheral', async () => {
|
|
36
|
-
jest.mocked(getConnectedDeviceIds).mockResolvedValueOnce([
|
|
37
|
-
{
|
|
38
|
-
id: 'find-my-peripheral',
|
|
39
|
-
name: 'Pro2 6E9E - Find My',
|
|
40
|
-
localName: null,
|
|
41
|
-
serviceUUIDs: [ONEKEY_SERVICE_UUID],
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
id: 'wallet-peripheral',
|
|
45
|
-
name: 'Pro2 6E9E',
|
|
46
|
-
localName: 'Pro2 6E9E',
|
|
47
|
-
serviceUUIDs: [ONEKEY_SERVICE_UUID],
|
|
48
|
-
},
|
|
49
|
-
] as never);
|
|
50
|
-
const blePlxManager = {
|
|
51
|
-
startDeviceScan: jest.fn(),
|
|
52
|
-
stopDeviceScan: jest.fn(),
|
|
53
|
-
};
|
|
54
|
-
const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
|
|
55
|
-
transport.blePlxManager = blePlxManager as never;
|
|
56
|
-
transport.init({ debug: jest.fn(), error: jest.fn() }, new EventEmitter());
|
|
57
|
-
|
|
58
|
-
const devices = await transport.enumerate();
|
|
59
|
-
|
|
60
|
-
expect(devices.map(device => device.id)).toEqual(['wallet-peripheral']);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test('filters a scanned Pro2 Find My peripheral by name when localName is null', async () => {
|
|
64
|
-
jest.mocked(getConnectedDeviceIds).mockResolvedValueOnce([]);
|
|
65
|
-
const blePlxManager = {
|
|
66
|
-
startDeviceScan: jest.fn((_serviceUUIDs, _options, listener) => {
|
|
67
|
-
queueMicrotask(() => {
|
|
68
|
-
listener(null, {
|
|
69
|
-
id: 'find-my-peripheral',
|
|
70
|
-
name: 'Pro2 6E9E - Find My',
|
|
71
|
-
localName: null,
|
|
72
|
-
serviceUUIDs: [ONEKEY_SERVICE_UUID],
|
|
73
|
-
});
|
|
74
|
-
listener(null, {
|
|
75
|
-
id: 'wallet-peripheral',
|
|
76
|
-
name: 'Pro2 6E9E',
|
|
77
|
-
localName: 'Pro2 6E9E',
|
|
78
|
-
serviceUUIDs: [ONEKEY_SERVICE_UUID],
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
}),
|
|
82
|
-
stopDeviceScan: jest.fn(),
|
|
83
|
-
};
|
|
84
|
-
const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
|
|
85
|
-
transport.blePlxManager = blePlxManager as never;
|
|
86
|
-
transport.init({ debug: jest.fn(), error: jest.fn() }, new EventEmitter());
|
|
87
|
-
|
|
88
|
-
const devices = await transport.enumerate();
|
|
89
|
-
|
|
90
|
-
expect(devices.map(device => device.id)).toEqual(['wallet-peripheral']);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test('ignores an unnamed scanned advertisement while keeping the named wallet peripheral', async () => {
|
|
94
|
-
jest.mocked(getConnectedDeviceIds).mockResolvedValueOnce([]);
|
|
95
|
-
const blePlxManager = {
|
|
96
|
-
startDeviceScan: jest.fn((_serviceUUIDs, _options, listener) => {
|
|
97
|
-
queueMicrotask(() => {
|
|
98
|
-
listener(null, {
|
|
99
|
-
id: 'unnamed-peripheral',
|
|
100
|
-
name: null,
|
|
101
|
-
localName: null,
|
|
102
|
-
serviceUUIDs: [
|
|
103
|
-
'0000180a-0000-1000-8000-00805f9b34fb',
|
|
104
|
-
'0000180f-0000-1000-8000-00805f9b34fb',
|
|
105
|
-
'0000fffd-0000-1000-8000-00805f9b34fb',
|
|
106
|
-
ONEKEY_SERVICE_UUID,
|
|
107
|
-
],
|
|
108
|
-
});
|
|
109
|
-
listener(null, {
|
|
110
|
-
id: 'wallet-peripheral',
|
|
111
|
-
name: 'Pro2 769D',
|
|
112
|
-
localName: 'Pro2 769D',
|
|
113
|
-
serviceUUIDs: [
|
|
114
|
-
'0000180a-0000-1000-8000-00805f9b34fb',
|
|
115
|
-
'0000180f-0000-1000-8000-00805f9b34fb',
|
|
116
|
-
'0000fffd-0000-1000-8000-00805f9b34fb',
|
|
117
|
-
ONEKEY_SERVICE_UUID,
|
|
118
|
-
],
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
}),
|
|
122
|
-
stopDeviceScan: jest.fn(),
|
|
123
|
-
};
|
|
124
|
-
const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
|
|
125
|
-
transport.blePlxManager = blePlxManager as never;
|
|
126
|
-
transport.init({ debug: jest.fn(), error: jest.fn() }, new EventEmitter());
|
|
127
|
-
|
|
128
|
-
const devices = await transport.enumerate();
|
|
129
|
-
|
|
130
|
-
expect(devices.map(device => device.id)).toEqual(['wallet-peripheral']);
|
|
131
|
-
});
|
|
132
|
-
});
|