@onekeyfe/hd-transport-react-native 1.2.0-alpha.54 → 1.2.0-alpha.55
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 +1 -38
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +74 -160
- package/jest.config.js +0 -5
- package/package.json +5 -5
- package/src/__tests__/enumerate.test.ts +132 -0
- package/src/__tests__/protocolV2Link.test.ts +230 -41
- package/src/index.ts +74 -235
- package/src/__tests__/staleCallTimeout.test.ts +0 -211
- package/src/__tests__/writePacketTimeout.test.ts +0 -306
package/src/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
HardwareErrorCode,
|
|
29
29
|
createDeferred,
|
|
30
30
|
isOnekeyBluetoothDevice,
|
|
31
|
+
isPro2FindMyAdvertisementName,
|
|
31
32
|
} from '@onekeyfe/hd-shared';
|
|
32
33
|
|
|
33
34
|
import { getConnectedDeviceIds, onDeviceBondState, pairDevice } from './BleManager';
|
|
@@ -60,6 +61,7 @@ const FIRMWARE_UPLOAD_WRITE_BURST_SIZE = Platform.OS === 'ios' ? 4 : 5;
|
|
|
60
61
|
const FIRMWARE_UPLOAD_WRITE_PAUSE_MS = Platform.OS === 'ios' ? 8 : 10;
|
|
61
62
|
const FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS = Platform.OS === 'ios' ? 24 : 30;
|
|
62
63
|
const FIRMWARE_UPLOAD_WRITE_MAX_RETRIES = 8;
|
|
64
|
+
const IOS_PROTOCOL_V2_CONTROL_WRITE_DELAY_MS = 5;
|
|
63
65
|
const ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH = 192;
|
|
64
66
|
const FIRMWARE_UPLOAD_WRITE_PACKET_CAPACITY =
|
|
65
67
|
Platform.OS === 'ios' ? IOS_PACKET_LENGTH : ANDROID_FIRMWARE_UPLOAD_PACKET_LENGTH;
|
|
@@ -106,22 +108,6 @@ const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, max
|
|
|
106
108
|
Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
|
|
107
109
|
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
108
110
|
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;
|
|
125
111
|
const DEVICE_SCAN_TIMEOUT_MS = 3000;
|
|
126
112
|
const IOS_NOTIFY_READY_DELAY_MS = 150;
|
|
127
113
|
const ANDROID_NOTIFY_READY_DELAY_MS = 300;
|
|
@@ -272,17 +258,6 @@ export default class ReactNativeBleTransport {
|
|
|
272
258
|
/** Per-device protocol type detected by active wire-level probe after connect. */
|
|
273
259
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
274
260
|
|
|
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
|
-
|
|
286
261
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
287
262
|
|
|
288
263
|
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
@@ -607,12 +582,21 @@ export default class ReactNativeBleTransport {
|
|
|
607
582
|
}
|
|
608
583
|
|
|
609
584
|
const displayName = getDeviceDisplayName(device);
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
585
|
+
// iOS may report a service-only advertisement before the named scan response.
|
|
586
|
+
// Do not cache that incomplete advertisement as an unknown device.
|
|
587
|
+
const isUnnamedIOSPeripheral = Platform.OS === 'ios' && !displayName?.trim();
|
|
588
|
+
const isFindMyPeripheral =
|
|
589
|
+
isPro2FindMyAdvertisementName(device?.name) ||
|
|
590
|
+
isPro2FindMyAdvertisementName(device?.localName);
|
|
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
|
+
});
|
|
616
600
|
if (isOneKey) {
|
|
617
601
|
addDevice(device as unknown as Device);
|
|
618
602
|
} else if (displayName && /\bpro\s*2\b/i.test(displayName)) {
|
|
@@ -633,7 +617,12 @@ export default class ReactNativeBleTransport {
|
|
|
633
617
|
'localName' in device && typeof device.localName === 'string'
|
|
634
618
|
? device.localName
|
|
635
619
|
: null;
|
|
620
|
+
const isFindMyPeripheral =
|
|
621
|
+
isPro2FindMyAdvertisementName(device.name) ||
|
|
622
|
+
isPro2FindMyAdvertisementName(localName);
|
|
623
|
+
|
|
636
624
|
if (
|
|
625
|
+
!isFindMyPeripheral &&
|
|
637
626
|
isOnekeyBluetoothDevice({
|
|
638
627
|
id: device.id,
|
|
639
628
|
name: device.name,
|
|
@@ -906,7 +895,7 @@ export default class ReactNativeBleTransport {
|
|
|
906
895
|
Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
|
|
907
896
|
return;
|
|
908
897
|
}
|
|
909
|
-
if (this.
|
|
898
|
+
if (this.deviceProtocol.get(uuid) === 'V2') {
|
|
910
899
|
let errorCode:
|
|
911
900
|
| typeof HardwareErrorCode.BleDeviceBondError
|
|
912
901
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
@@ -976,7 +965,7 @@ export default class ReactNativeBleTransport {
|
|
|
976
965
|
|
|
977
966
|
try {
|
|
978
967
|
const data = Buffer.from(c.value as string, 'base64');
|
|
979
|
-
const protocol = this.
|
|
968
|
+
const protocol = this.deviceProtocol.get(uuid);
|
|
980
969
|
if (!protocol) {
|
|
981
970
|
Log?.debug('monitor data ignored before protocol detection: ', uuid);
|
|
982
971
|
return;
|
|
@@ -1010,7 +999,7 @@ export default class ReactNativeBleTransport {
|
|
|
1010
999
|
} catch (error) {
|
|
1011
1000
|
Log?.debug('monitor data error: ', error);
|
|
1012
1001
|
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1013
|
-
if (this.
|
|
1002
|
+
if (this.deviceProtocol.get(uuid) === 'V2') {
|
|
1014
1003
|
this.rejectProtocolV2Frames(uuid, notifyError);
|
|
1015
1004
|
} else if (this.runPromiseDeviceId === uuid) {
|
|
1016
1005
|
this.runPromise?.reject(notifyError);
|
|
@@ -1074,7 +1063,6 @@ export default class ReactNativeBleTransport {
|
|
|
1074
1063
|
}
|
|
1075
1064
|
|
|
1076
1065
|
this.deviceProtocol.delete(uuid);
|
|
1077
|
-
this.probingProtocols.delete(uuid);
|
|
1078
1066
|
// Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
|
|
1079
1067
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1080
1068
|
this.protocolV2Assemblers.delete(uuid);
|
|
@@ -1141,25 +1129,8 @@ export default class ReactNativeBleTransport {
|
|
|
1141
1129
|
const transport = this.getCachedTransport(uuid);
|
|
1142
1130
|
const runPromise = createDeferred<string>();
|
|
1143
1131
|
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
|
-
}
|
|
1152
1132
|
this.runPromise = runPromise;
|
|
1153
1133
|
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;
|
|
1163
1134
|
const messages = this._messages;
|
|
1164
1135
|
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
1165
1136
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -1185,9 +1156,6 @@ export default class ReactNativeBleTransport {
|
|
|
1185
1156
|
chunk = ByteBuffer.allocate(packetCapacity);
|
|
1186
1157
|
} catch (e) {
|
|
1187
1158
|
onError(e);
|
|
1188
|
-
if (isWedgedWriteError(e)) {
|
|
1189
|
-
throw e;
|
|
1190
|
-
}
|
|
1191
1159
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1192
1160
|
}
|
|
1193
1161
|
}
|
|
@@ -1219,9 +1187,6 @@ export default class ReactNativeBleTransport {
|
|
|
1219
1187
|
}
|
|
1220
1188
|
} catch (e) {
|
|
1221
1189
|
onError(e);
|
|
1222
|
-
if (isWedgedWriteError(e)) {
|
|
1223
|
-
throw e;
|
|
1224
|
-
}
|
|
1225
1190
|
throw ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
1226
1191
|
}
|
|
1227
1192
|
}
|
|
@@ -1235,15 +1200,9 @@ export default class ReactNativeBleTransport {
|
|
|
1235
1200
|
if (name === 'EmmcFileWrite') {
|
|
1236
1201
|
await writeChunkedData(
|
|
1237
1202
|
buffers,
|
|
1238
|
-
data =>
|
|
1239
|
-
this.writeBlePacket(
|
|
1240
|
-
uuid,
|
|
1241
|
-
data,
|
|
1242
|
-
payload => transport.writeWithRetry(payload),
|
|
1243
|
-
isCurrentOwner
|
|
1244
|
-
),
|
|
1203
|
+
data => transport.writeWithRetry(data),
|
|
1245
1204
|
e => {
|
|
1246
|
-
|
|
1205
|
+
this.runPromise = null;
|
|
1247
1206
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1248
1207
|
}
|
|
1249
1208
|
);
|
|
@@ -1264,12 +1223,7 @@ export default class ReactNativeBleTransport {
|
|
|
1264
1223
|
// eslint-disable-next-line no-constant-condition
|
|
1265
1224
|
while (true) {
|
|
1266
1225
|
try {
|
|
1267
|
-
await
|
|
1268
|
-
uuid,
|
|
1269
|
-
data,
|
|
1270
|
-
payload => transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
1271
|
-
isCurrentOwner
|
|
1272
|
-
);
|
|
1226
|
+
await transport.writeCharacteristic.writeWithoutResponse(data);
|
|
1273
1227
|
return;
|
|
1274
1228
|
} catch (error) {
|
|
1275
1229
|
const retryType = getFirmwareUploadWriteRetryType(error);
|
|
@@ -1288,7 +1242,7 @@ export default class ReactNativeBleTransport {
|
|
|
1288
1242
|
}
|
|
1289
1243
|
},
|
|
1290
1244
|
e => {
|
|
1291
|
-
|
|
1245
|
+
this.runPromise = null;
|
|
1292
1246
|
Log?.error('writeCharacteristic write error: ', e);
|
|
1293
1247
|
}
|
|
1294
1248
|
);
|
|
@@ -1297,18 +1251,16 @@ export default class ReactNativeBleTransport {
|
|
|
1297
1251
|
const outData = o.toString('base64');
|
|
1298
1252
|
// Upload resources on low-end phones may OOM
|
|
1299
1253
|
try {
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1254
|
+
const shouldUseWriteWithResponse =
|
|
1255
|
+
Platform.OS === 'ios' && transport.writeCharacteristic.isWritableWithResponse;
|
|
1256
|
+
if (shouldUseWriteWithResponse) {
|
|
1257
|
+
await transport.writeCharacteristic.writeWithResponse(outData);
|
|
1258
|
+
} else {
|
|
1259
|
+
await transport.writeCharacteristic.writeWithoutResponse(outData);
|
|
1260
|
+
}
|
|
1306
1261
|
} catch (e) {
|
|
1307
1262
|
Log?.debug('writeCharacteristic write error: ', e);
|
|
1308
|
-
|
|
1309
|
-
if (isWedgedWriteError(e)) {
|
|
1310
|
-
throw e;
|
|
1311
|
-
}
|
|
1263
|
+
this.runPromise = null;
|
|
1312
1264
|
if (e.errorCode === BleErrorCode.DeviceDisconnected) {
|
|
1313
1265
|
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded);
|
|
1314
1266
|
} else if (e.errorCode === BleErrorCode.OperationStartFailed) {
|
|
@@ -1351,13 +1303,8 @@ export default class ReactNativeBleTransport {
|
|
|
1351
1303
|
}
|
|
1352
1304
|
const isProbeTimeout =
|
|
1353
1305
|
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;
|
|
1358
1306
|
if (
|
|
1359
1307
|
!isProbeTimeout &&
|
|
1360
|
-
!isStaleCall &&
|
|
1361
1308
|
(e as { errorCode?: unknown })?.errorCode === HardwareErrorCode.BleTimeoutError
|
|
1362
1309
|
) {
|
|
1363
1310
|
await this.disconnect(uuid);
|
|
@@ -1437,7 +1384,6 @@ export default class ReactNativeBleTransport {
|
|
|
1437
1384
|
delete transportCache[session];
|
|
1438
1385
|
}
|
|
1439
1386
|
this.deviceProtocol.delete(session);
|
|
1440
|
-
this.probingProtocols.delete(session);
|
|
1441
1387
|
this.deviceProtocolHints.delete(session);
|
|
1442
1388
|
this.protocolV2Assemblers.delete(session);
|
|
1443
1389
|
this.resetProtocolV2Frames(session);
|
|
@@ -1472,105 +1418,6 @@ export default class ReactNativeBleTransport {
|
|
|
1472
1418
|
return transport;
|
|
1473
1419
|
}
|
|
1474
1420
|
|
|
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
|
-
|
|
1574
1421
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
1575
1422
|
return ERRORS.TypedError(
|
|
1576
1423
|
HardwareErrorCode.RuntimeError,
|
|
@@ -1586,25 +1433,34 @@ export default class ReactNativeBleTransport {
|
|
|
1586
1433
|
}
|
|
1587
1434
|
|
|
1588
1435
|
private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
|
|
1589
|
-
if (this.probingProtocols.get(uuid) === protocol) {
|
|
1590
|
-
this.probingProtocols.delete(uuid);
|
|
1591
|
-
}
|
|
1592
1436
|
if (this.deviceProtocol.get(uuid) === protocol) {
|
|
1593
1437
|
this.deviceProtocol.delete(uuid);
|
|
1594
1438
|
}
|
|
1595
1439
|
}
|
|
1596
1440
|
|
|
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
|
-
|
|
1602
1441
|
private async detectProtocol(
|
|
1603
1442
|
uuid: string,
|
|
1604
1443
|
expectedProtocol?: ProtocolType,
|
|
1605
1444
|
protocolHint?: ProtocolType,
|
|
1606
1445
|
rebuildTransport?: () => Promise<void>
|
|
1607
1446
|
): 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
|
+
|
|
1608
1464
|
if (expectedProtocol === 'V1') {
|
|
1609
1465
|
if (await this.probeProtocolV1(uuid)) {
|
|
1610
1466
|
this.deviceProtocol.set(uuid, 'V1');
|
|
@@ -1662,7 +1518,6 @@ export default class ReactNativeBleTransport {
|
|
|
1662
1518
|
}
|
|
1663
1519
|
|
|
1664
1520
|
this.deviceProtocol.delete(uuid);
|
|
1665
|
-
this.probingProtocols.delete(uuid);
|
|
1666
1521
|
throw this.createProtocolDetectionError();
|
|
1667
1522
|
}
|
|
1668
1523
|
|
|
@@ -1724,20 +1579,14 @@ export default class ReactNativeBleTransport {
|
|
|
1724
1579
|
}
|
|
1725
1580
|
|
|
1726
1581
|
try {
|
|
1727
|
-
this.
|
|
1582
|
+
this.deviceProtocol.set(uuid, 'V1');
|
|
1728
1583
|
// GetFeatures identifies Protocol V1 without resetting an existing wallet
|
|
1729
1584
|
// session before Core has a chance to restore a hidden wallet.
|
|
1730
1585
|
await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
1731
|
-
this.probingProtocols.delete(uuid);
|
|
1732
1586
|
return true;
|
|
1733
1587
|
} catch (error) {
|
|
1734
1588
|
this.clearProbeProtocol(uuid, 'V1');
|
|
1735
1589
|
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
|
-
}
|
|
1741
1590
|
return false;
|
|
1742
1591
|
}
|
|
1743
1592
|
}
|
|
@@ -1747,7 +1596,7 @@ export default class ReactNativeBleTransport {
|
|
|
1747
1596
|
return false;
|
|
1748
1597
|
}
|
|
1749
1598
|
|
|
1750
|
-
this.
|
|
1599
|
+
this.deviceProtocol.set(uuid, 'V2');
|
|
1751
1600
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1752
1601
|
const detected = await probeProtocolV2Helper({
|
|
1753
1602
|
call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
|
|
@@ -1762,8 +1611,6 @@ export default class ReactNativeBleTransport {
|
|
|
1762
1611
|
});
|
|
1763
1612
|
if (!detected) {
|
|
1764
1613
|
this.clearProbeProtocol(uuid, 'V2');
|
|
1765
|
-
} else {
|
|
1766
|
-
this.probingProtocols.delete(uuid);
|
|
1767
1614
|
}
|
|
1768
1615
|
return detected;
|
|
1769
1616
|
}
|
|
@@ -1845,12 +1692,15 @@ export default class ReactNativeBleTransport {
|
|
|
1845
1692
|
}
|
|
1846
1693
|
|
|
1847
1694
|
private async writeProtocolV2Packet(
|
|
1848
|
-
uuid: string,
|
|
1849
1695
|
transport: BleTransport,
|
|
1850
1696
|
base64: string,
|
|
1851
1697
|
context: ProtocolV2CallContext,
|
|
1852
1698
|
assertCurrentGeneration: () => void
|
|
1853
1699
|
) {
|
|
1700
|
+
const shouldUseWriteWithResponse =
|
|
1701
|
+
Platform.OS === 'ios' &&
|
|
1702
|
+
!context.highVolume &&
|
|
1703
|
+
transport.writeCharacteristic.isWritableWithResponse;
|
|
1854
1704
|
let attempt = 0;
|
|
1855
1705
|
for (;;) {
|
|
1856
1706
|
assertCurrentGeneration();
|
|
@@ -1858,21 +1708,11 @@ export default class ReactNativeBleTransport {
|
|
|
1858
1708
|
throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
|
|
1859
1709
|
}
|
|
1860
1710
|
try {
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
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
|
-
);
|
|
1711
|
+
if (shouldUseWriteWithResponse) {
|
|
1712
|
+
await transport.writeCharacteristic.writeWithResponse(base64);
|
|
1713
|
+
} else {
|
|
1714
|
+
await transport.writeCharacteristic.writeWithoutResponse(base64);
|
|
1715
|
+
}
|
|
1876
1716
|
assertCurrentGeneration();
|
|
1877
1717
|
return;
|
|
1878
1718
|
} catch (error) {
|
|
@@ -1895,7 +1735,6 @@ export default class ReactNativeBleTransport {
|
|
|
1895
1735
|
}
|
|
1896
1736
|
|
|
1897
1737
|
private async writeProtocolV2Frame(
|
|
1898
|
-
uuid: string,
|
|
1899
1738
|
transport: BleTransport,
|
|
1900
1739
|
frame: Uint8Array,
|
|
1901
1740
|
context: ProtocolV2CallContext,
|
|
@@ -1908,19 +1747,25 @@ export default class ReactNativeBleTransport {
|
|
|
1908
1747
|
androidPacketLength: tuning.androidPacketLength,
|
|
1909
1748
|
mtu: Platform.OS === 'android' ? transport.mtuSize : undefined,
|
|
1910
1749
|
});
|
|
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;
|
|
1911
1756
|
await writeProtocolV2BleFrame({
|
|
1912
1757
|
frame,
|
|
1913
1758
|
packetCapacity,
|
|
1914
1759
|
assertActive: assertCurrentGeneration,
|
|
1915
1760
|
signal: context.signal,
|
|
1916
1761
|
abortMessage: `Protocol V2 BLE write aborted for ${context.messageName}`,
|
|
1762
|
+
initialDelayMs,
|
|
1917
1763
|
burstSize: FIRMWARE_UPLOAD_WRITE_BURST_SIZE,
|
|
1918
1764
|
burstPauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
|
|
1919
1765
|
flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
|
|
1920
1766
|
wait: delay,
|
|
1921
1767
|
writePacket: packet =>
|
|
1922
1768
|
this.writeProtocolV2Packet(
|
|
1923
|
-
uuid,
|
|
1924
1769
|
transport,
|
|
1925
1770
|
Buffer.from(packet).toString('base64'),
|
|
1926
1771
|
context,
|
|
@@ -1985,13 +1830,7 @@ export default class ReactNativeBleTransport {
|
|
|
1985
1830
|
writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
|
|
1986
1831
|
assertCurrentGeneration();
|
|
1987
1832
|
const currentTransport = this.getCachedTransport(uuid);
|
|
1988
|
-
await this.writeProtocolV2Frame(
|
|
1989
|
-
uuid,
|
|
1990
|
-
currentTransport,
|
|
1991
|
-
frame,
|
|
1992
|
-
context,
|
|
1993
|
-
assertCurrentGeneration
|
|
1994
|
-
);
|
|
1833
|
+
await this.writeProtocolV2Frame(currentTransport, frame, context, assertCurrentGeneration);
|
|
1995
1834
|
},
|
|
1996
1835
|
readFrame: async () => {
|
|
1997
1836
|
assertCurrentGeneration();
|
|
@@ -2016,6 +1855,6 @@ export default class ReactNativeBleTransport {
|
|
|
2016
1855
|
}
|
|
2017
1856
|
|
|
2018
1857
|
getProtocolType(path: string): ProtocolType | undefined {
|
|
2019
|
-
return this.
|
|
1858
|
+
return this.deviceProtocol.get(path);
|
|
2020
1859
|
}
|
|
2021
1860
|
}
|