@onekeyfe/hd-transport-react-native 1.2.0-alpha.52 → 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 +133 -14
- package/package.json +5 -5
- package/src/__tests__/protocolV2Link.test.ts +8 -1
- package/src/__tests__/writePacketTimeout.test.ts +306 -0
- package/src/index.ts +205 -11
package/dist/index.d.ts
CHANGED
|
@@ -35,6 +35,17 @@ type ResolvedBleCharacteristics = {
|
|
|
35
35
|
notifyCharacteristic: Characteristic;
|
|
36
36
|
};
|
|
37
37
|
declare const getFirmwareUploadWriteRetryType: (error: unknown) => FirmwareUploadWriteRetryType | null;
|
|
38
|
+
/**
|
|
39
|
+
* Per-packet write budget. iOS only resolves writeWithoutResponse once CoreBluetooth
|
|
40
|
+
* reports the peripheral ready again; a peripheral wedged by its own firmware reboot
|
|
41
|
+
* stops reporting ready while staying connected, so the write promise never settles.
|
|
42
|
+
* Response timeouts cannot cover that — they are armed after the writes complete —
|
|
43
|
+
* and an unbounded write leaves the whole transport unusable until the process dies.
|
|
44
|
+
* A healthy packet completes in milliseconds, so this only fires on a dead link.
|
|
45
|
+
*/
|
|
46
|
+
declare const BLE_WRITE_PACKET_TIMEOUT_MS = 10000;
|
|
47
|
+
/** Consecutive wedged writes on one device before the BLE manager itself is recreated. */
|
|
48
|
+
declare const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
38
49
|
type ProtocolV2BleTuning = {
|
|
39
50
|
iosPacketLength?: number;
|
|
40
51
|
androidPacketLength?: number;
|
|
@@ -61,6 +72,15 @@ declare class ReactNativeBleTransport {
|
|
|
61
72
|
firmwareUploadWriteRecoveryIds: Set<string>;
|
|
62
73
|
/** Per-device protocol type detected by active wire-level probe after connect. */
|
|
63
74
|
private deviceProtocol;
|
|
75
|
+
/**
|
|
76
|
+
* Protocol a probe is currently trying, before the device has confirmed it. Calls
|
|
77
|
+
* must route with it, but acquire() must not treat it as a detected protocol: a
|
|
78
|
+
* probe that never answers would otherwise leave the reuse fast path handing out a
|
|
79
|
+
* transport that was never validated.
|
|
80
|
+
*/
|
|
81
|
+
private probingProtocols;
|
|
82
|
+
/** Consecutive write timeouts per device; reset by any write that completes. */
|
|
83
|
+
private writeTimeoutCounts;
|
|
64
84
|
private deviceProtocolHints;
|
|
65
85
|
private protocolV2Assemblers;
|
|
66
86
|
private protocolV2FrameQueues;
|
|
@@ -100,9 +120,26 @@ declare class ReactNativeBleTransport {
|
|
|
100
120
|
disconnect(session: string): Promise<void>;
|
|
101
121
|
cancel(): void;
|
|
102
122
|
private getCachedTransport;
|
|
123
|
+
/**
|
|
124
|
+
* Write one packet under a bounded budget. A write that never settles means the
|
|
125
|
+
* peripheral is wedged even though the GATT link still reports connected, so the
|
|
126
|
+
* link is torn down: releasing JS state alone would leave the poisoned peripheral
|
|
127
|
+
* cached and every later call would hang on it again.
|
|
128
|
+
*/
|
|
129
|
+
private writeBlePacket;
|
|
130
|
+
/**
|
|
131
|
+
* Drop a link whose writes stopped completing. The JS state is purged synchronously
|
|
132
|
+
* so the next acquire() cannot reuse the dead transport, while the native teardown is
|
|
133
|
+
* intentionally NOT awaited: it talks to the very layer that just stopped settling
|
|
134
|
+
* promises, so awaiting it could hang exactly like the write it is recovering from.
|
|
135
|
+
*/
|
|
136
|
+
private tearDownWedgedLink;
|
|
137
|
+
private resetPlxManager;
|
|
103
138
|
private createProtocolMismatchError;
|
|
104
139
|
private createProtocolDetectionError;
|
|
105
140
|
private clearProbeProtocol;
|
|
141
|
+
/** Protocol to route a call with: confirmed if known, otherwise the one being probed. */
|
|
142
|
+
private getActiveProtocol;
|
|
106
143
|
private detectProtocol;
|
|
107
144
|
private resetProbeStateAfterProtocolProbe;
|
|
108
145
|
private probeProtocolV1;
|
|
@@ -120,4 +157,4 @@ declare class ReactNativeBleTransport {
|
|
|
120
157
|
getProtocolType(path: string): ProtocolType | undefined;
|
|
121
158
|
}
|
|
122
159
|
|
|
123
|
-
export { IOneKeyDevice, ProtocolV2BleTuning, configureProtocolV2BleTuning, ReactNativeBleTransport as default, getFirmwareUploadWriteRetryType, getProtocolV2BleTuning, resetProtocolV2BleTuning };
|
|
160
|
+
export { BLE_WRITE_PACKET_TIMEOUT_MS, BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD, IOneKeyDevice, ProtocolV2BleTuning, configureProtocolV2BleTuning, ReactNativeBleTransport as default, getFirmwareUploadWriteRetryType, getProtocolV2BleTuning, resetProtocolV2BleTuning };
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAIL,UAAU,IAAI,aAAa,EAE5B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,SAAS,EAAE,EAEhB,KAAK,oBAAoB,EAIzB,KAAK,YAAY,EAKjB,KAAK,oBAAoB,EAG1B,MAAM,wBAAwB,CAAC;AAmBhC,OAAO,YAAY,MAAM,gBAAgB,CAAC;AAK1C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACjF,OAAO,KAAK,YAAY,MAAM,QAAQ,CAAC;AACvC,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAgBjE,KAAK,4BAA4B,GAAG,WAAW,CAAC;AAChD,KAAK,0BAA0B,GAAG;IAChC,mBAAmB,EAAE,cAAc,CAAC;IACpC,oBAAoB,EAAE,cAAc,CAAC;CACtC,CAAC;AAOF,eAAO,MAAM,+BAA+B,UACnC,OAAO,KACb,4BAA4B,GAAG,IAsBjC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -244,6 +244,12 @@ const getFirmwareUploadWriteRetryType = (error) => {
|
|
|
244
244
|
const resolveFirmwareUploadRetryDelay = (attempt, baseDelayMs = 200, maxDelayMs = 1200) => Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
|
|
245
245
|
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
246
246
|
const PROTOCOL_V2_PROBE_TIMEOUT_MS = 10000;
|
|
247
|
+
const BLE_WRITE_PACKET_TIMEOUT_MS = 10000;
|
|
248
|
+
const WEDGED_WRITE_MESSAGE = 'BLE write timeout after';
|
|
249
|
+
const isWedgedWriteError = (error) => (error === null || error === void 0 ? void 0 : error.errorCode) === hdShared.HardwareErrorCode.BleWriteCharacteristicError &&
|
|
250
|
+
typeof (error === null || error === void 0 ? void 0 : error.message) === 'string' &&
|
|
251
|
+
error.message.startsWith(WEDGED_WRITE_MESSAGE);
|
|
252
|
+
const BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = 2;
|
|
247
253
|
const DEVICE_SCAN_TIMEOUT_MS = 3000;
|
|
248
254
|
const IOS_NOTIFY_READY_DELAY_MS = 150;
|
|
249
255
|
const ANDROID_NOTIFY_READY_DELAY_MS = 300;
|
|
@@ -341,6 +347,8 @@ class ReactNativeBleTransport {
|
|
|
341
347
|
this.runPromiseDeviceId = null;
|
|
342
348
|
this.firmwareUploadWriteRecoveryIds = new Set();
|
|
343
349
|
this.deviceProtocol = new Map();
|
|
350
|
+
this.probingProtocols = new Map();
|
|
351
|
+
this.writeTimeoutCounts = new Map();
|
|
344
352
|
this.deviceProtocolHints = new Map();
|
|
345
353
|
this.protocolV2Assemblers = new Map();
|
|
346
354
|
this.protocolV2FrameQueues = new Map();
|
|
@@ -852,7 +860,7 @@ class ReactNativeBleTransport {
|
|
|
852
860
|
Log === null || Log === void 0 ? void 0 : Log.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
|
|
853
861
|
return;
|
|
854
862
|
}
|
|
855
|
-
if (this.
|
|
863
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
856
864
|
let errorCode = hdShared.HardwareErrorCode.BleCharacteristicNotifyError;
|
|
857
865
|
if ((_a = error.reason) === null || _a === void 0 ? void 0 : _a.includes('The connection has timed out unexpectedly')) {
|
|
858
866
|
errorCode = hdShared.HardwareErrorCode.BleTimeoutError;
|
|
@@ -903,7 +911,7 @@ class ReactNativeBleTransport {
|
|
|
903
911
|
}
|
|
904
912
|
try {
|
|
905
913
|
const data = buffer.Buffer.from(c.value, 'base64');
|
|
906
|
-
const protocol = this.
|
|
914
|
+
const protocol = this.getActiveProtocol(uuid);
|
|
907
915
|
if (!protocol) {
|
|
908
916
|
Log === null || Log === void 0 ? void 0 : Log.debug('monitor data ignored before protocol detection: ', uuid);
|
|
909
917
|
return;
|
|
@@ -931,7 +939,7 @@ class ReactNativeBleTransport {
|
|
|
931
939
|
catch (error) {
|
|
932
940
|
Log === null || Log === void 0 ? void 0 : Log.debug('monitor data error: ', error);
|
|
933
941
|
const notifyError = hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleWriteCharacteristicError);
|
|
934
|
-
if (this.
|
|
942
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
935
943
|
this.rejectProtocolV2Frames(uuid, notifyError);
|
|
936
944
|
}
|
|
937
945
|
else if (this.runPromiseDeviceId === uuid) {
|
|
@@ -987,6 +995,7 @@ class ReactNativeBleTransport {
|
|
|
987
995
|
delete transportCache[uuid];
|
|
988
996
|
}
|
|
989
997
|
this.deviceProtocol.delete(uuid);
|
|
998
|
+
this.probingProtocols.delete(uuid);
|
|
990
999
|
(_f = this.protocolV2Assemblers.get(uuid)) === null || _f === void 0 ? void 0 : _f.reset();
|
|
991
1000
|
this.protocolV2Assemblers.delete(uuid);
|
|
992
1001
|
this.resetProtocolV2Frames(uuid);
|
|
@@ -1047,6 +1056,7 @@ class ReactNativeBleTransport {
|
|
|
1047
1056
|
this.runPromiseDeviceId = null;
|
|
1048
1057
|
}
|
|
1049
1058
|
};
|
|
1059
|
+
const isCurrentOwner = () => this.runPromise === runPromise;
|
|
1050
1060
|
const messages = this._messages;
|
|
1051
1061
|
const buffers = ProtocolV1.encodeTransportPackets(messages, name, data);
|
|
1052
1062
|
let timeout;
|
|
@@ -1067,6 +1077,9 @@ class ReactNativeBleTransport {
|
|
|
1067
1077
|
}
|
|
1068
1078
|
catch (e) {
|
|
1069
1079
|
onError(e);
|
|
1080
|
+
if (isWedgedWriteError(e)) {
|
|
1081
|
+
throw e;
|
|
1082
|
+
}
|
|
1070
1083
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleWriteCharacteristicError);
|
|
1071
1084
|
}
|
|
1072
1085
|
}
|
|
@@ -1094,6 +1107,9 @@ class ReactNativeBleTransport {
|
|
|
1094
1107
|
}
|
|
1095
1108
|
catch (e) {
|
|
1096
1109
|
onError(e);
|
|
1110
|
+
if (isWedgedWriteError(e)) {
|
|
1111
|
+
throw e;
|
|
1112
|
+
}
|
|
1097
1113
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleWriteCharacteristicError);
|
|
1098
1114
|
}
|
|
1099
1115
|
}
|
|
@@ -1104,7 +1120,7 @@ class ReactNativeBleTransport {
|
|
|
1104
1120
|
});
|
|
1105
1121
|
}
|
|
1106
1122
|
if (name === 'EmmcFileWrite') {
|
|
1107
|
-
yield writeChunkedData(buffers, data => transport.writeWithRetry(
|
|
1123
|
+
yield writeChunkedData(buffers, data => this.writeBlePacket(uuid, data, payload => transport.writeWithRetry(payload), isCurrentOwner), e => {
|
|
1108
1124
|
releaseOwnershipIfCurrent();
|
|
1109
1125
|
Log === null || Log === void 0 ? void 0 : Log.error('writeCharacteristic write error: ', e);
|
|
1110
1126
|
});
|
|
@@ -1121,7 +1137,7 @@ class ReactNativeBleTransport {
|
|
|
1121
1137
|
let attempt = 0;
|
|
1122
1138
|
while (true) {
|
|
1123
1139
|
try {
|
|
1124
|
-
yield transport.writeCharacteristic.writeWithoutResponse(
|
|
1140
|
+
yield this.writeBlePacket(uuid, data, payload => transport.writeCharacteristic.writeWithoutResponse(payload), isCurrentOwner);
|
|
1125
1141
|
return;
|
|
1126
1142
|
}
|
|
1127
1143
|
catch (error) {
|
|
@@ -1148,11 +1164,14 @@ class ReactNativeBleTransport {
|
|
|
1148
1164
|
for (const o of buffers) {
|
|
1149
1165
|
const outData = o.toString('base64');
|
|
1150
1166
|
try {
|
|
1151
|
-
yield transport.writeCharacteristic.writeWithoutResponse(
|
|
1167
|
+
yield this.writeBlePacket(uuid, outData, payload => transport.writeCharacteristic.writeWithoutResponse(payload), isCurrentOwner);
|
|
1152
1168
|
}
|
|
1153
1169
|
catch (e) {
|
|
1154
1170
|
Log === null || Log === void 0 ? void 0 : Log.debug('writeCharacteristic write error: ', e);
|
|
1155
1171
|
releaseOwnershipIfCurrent();
|
|
1172
|
+
if (isWedgedWriteError(e)) {
|
|
1173
|
+
throw e;
|
|
1174
|
+
}
|
|
1156
1175
|
if (e.errorCode === reactNativeBlePlx.BleErrorCode.DeviceDisconnected) {
|
|
1157
1176
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceNotBonded);
|
|
1158
1177
|
}
|
|
@@ -1265,6 +1284,7 @@ class ReactNativeBleTransport {
|
|
|
1265
1284
|
delete transportCache[session];
|
|
1266
1285
|
}
|
|
1267
1286
|
this.deviceProtocol.delete(session);
|
|
1287
|
+
this.probingProtocols.delete(session);
|
|
1268
1288
|
this.deviceProtocolHints.delete(session);
|
|
1269
1289
|
this.protocolV2Assemblers.delete(session);
|
|
1270
1290
|
this.resetProtocolV2Frames(session);
|
|
@@ -1293,6 +1313,80 @@ class ReactNativeBleTransport {
|
|
|
1293
1313
|
}
|
|
1294
1314
|
return transport;
|
|
1295
1315
|
}
|
|
1316
|
+
writeBlePacket(uuid, data, write, isCurrentOwner) {
|
|
1317
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
1318
|
+
let timer;
|
|
1319
|
+
let timedOut = false;
|
|
1320
|
+
try {
|
|
1321
|
+
yield Promise.race([
|
|
1322
|
+
write(data),
|
|
1323
|
+
new Promise((_, reject) => {
|
|
1324
|
+
timer = setTimeout(() => {
|
|
1325
|
+
timedOut = true;
|
|
1326
|
+
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleWriteCharacteristicError, `BLE write timeout after ${BLE_WRITE_PACKET_TIMEOUT_MS}ms`));
|
|
1327
|
+
}, BLE_WRITE_PACKET_TIMEOUT_MS);
|
|
1328
|
+
}),
|
|
1329
|
+
]);
|
|
1330
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1331
|
+
}
|
|
1332
|
+
catch (error) {
|
|
1333
|
+
if (timedOut) {
|
|
1334
|
+
if (isCurrentOwner && !isCurrentOwner()) {
|
|
1335
|
+
Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] stale BLE write timed out, link kept:', uuid);
|
|
1336
|
+
}
|
|
1337
|
+
else {
|
|
1338
|
+
this.tearDownWedgedLink(uuid);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
throw error;
|
|
1342
|
+
}
|
|
1343
|
+
finally {
|
|
1344
|
+
if (timer)
|
|
1345
|
+
clearTimeout(timer);
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
tearDownWedgedLink(uuid) {
|
|
1350
|
+
var _a;
|
|
1351
|
+
const timeouts = ((_a = this.writeTimeoutCounts.get(uuid)) !== null && _a !== void 0 ? _a : 0) + 1;
|
|
1352
|
+
this.writeTimeoutCounts.set(uuid, timeouts);
|
|
1353
|
+
Log === null || Log === void 0 ? void 0 : Log.error('[ReactNativeBleTransport] BLE write timed out, tearing down link:', uuid, {
|
|
1354
|
+
consecutiveWriteTimeouts: timeouts,
|
|
1355
|
+
});
|
|
1356
|
+
const wedged = transportCache[uuid];
|
|
1357
|
+
this.disconnect(uuid).catch(error => {
|
|
1358
|
+
Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] wedged link teardown failed (ignored):', error);
|
|
1359
|
+
});
|
|
1360
|
+
if (wedged && transportCache[uuid] === wedged) {
|
|
1361
|
+
delete transportCache[uuid];
|
|
1362
|
+
}
|
|
1363
|
+
this.deviceProtocol.delete(uuid);
|
|
1364
|
+
this.probingProtocols.delete(uuid);
|
|
1365
|
+
this.protocolV2Assemblers.delete(uuid);
|
|
1366
|
+
this.resetProtocolV2Frames(uuid);
|
|
1367
|
+
if (timeouts >= BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD) {
|
|
1368
|
+
Log === null || Log === void 0 ? void 0 : Log.error('[ReactNativeBleTransport] BLE writes wedged repeatedly, resetting BLE manager');
|
|
1369
|
+
this.resetPlxManager();
|
|
1370
|
+
this.writeTimeoutCounts.delete(uuid);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
resetPlxManager() {
|
|
1374
|
+
const manager = this.blePlxManager;
|
|
1375
|
+
this.blePlxManager = undefined;
|
|
1376
|
+
Object.keys(transportCache).forEach(key => {
|
|
1377
|
+
delete transportCache[key];
|
|
1378
|
+
});
|
|
1379
|
+
this.deviceProtocol.clear();
|
|
1380
|
+
this.probingProtocols.clear();
|
|
1381
|
+
this.monitorTokens.clear();
|
|
1382
|
+
this.protocolV2Assemblers.clear();
|
|
1383
|
+
try {
|
|
1384
|
+
manager === null || manager === void 0 ? void 0 : manager.destroy();
|
|
1385
|
+
}
|
|
1386
|
+
catch (error) {
|
|
1387
|
+
Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] BLE manager destroy failed (ignored):', error);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1296
1390
|
createProtocolMismatchError(expected) {
|
|
1297
1391
|
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Device protocol mismatch: expected ${expected}, but device did not respond to expected protocol`);
|
|
1298
1392
|
}
|
|
@@ -1300,10 +1394,17 @@ class ReactNativeBleTransport {
|
|
|
1300
1394
|
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleTimeoutError, 'Unable to detect BLE protocol: device did not respond to Protocol V1 GetFeatures or Protocol V2 Ping');
|
|
1301
1395
|
}
|
|
1302
1396
|
clearProbeProtocol(uuid, protocol) {
|
|
1397
|
+
if (this.probingProtocols.get(uuid) === protocol) {
|
|
1398
|
+
this.probingProtocols.delete(uuid);
|
|
1399
|
+
}
|
|
1303
1400
|
if (this.deviceProtocol.get(uuid) === protocol) {
|
|
1304
1401
|
this.deviceProtocol.delete(uuid);
|
|
1305
1402
|
}
|
|
1306
1403
|
}
|
|
1404
|
+
getActiveProtocol(uuid) {
|
|
1405
|
+
var _a;
|
|
1406
|
+
return (_a = this.deviceProtocol.get(uuid)) !== null && _a !== void 0 ? _a : this.probingProtocols.get(uuid);
|
|
1407
|
+
}
|
|
1307
1408
|
detectProtocol(uuid, expectedProtocol, protocolHint, rebuildTransport) {
|
|
1308
1409
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1309
1410
|
if (expectedProtocol === 'V1') {
|
|
@@ -1354,6 +1455,7 @@ class ReactNativeBleTransport {
|
|
|
1354
1455
|
}
|
|
1355
1456
|
}
|
|
1356
1457
|
this.deviceProtocol.delete(uuid);
|
|
1458
|
+
this.probingProtocols.delete(uuid);
|
|
1357
1459
|
throw this.createProtocolDetectionError();
|
|
1358
1460
|
});
|
|
1359
1461
|
}
|
|
@@ -1405,13 +1507,17 @@ class ReactNativeBleTransport {
|
|
|
1405
1507
|
return false;
|
|
1406
1508
|
}
|
|
1407
1509
|
try {
|
|
1408
|
-
this.
|
|
1510
|
+
this.probingProtocols.set(uuid, 'V1');
|
|
1409
1511
|
yield this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
1512
|
+
this.probingProtocols.delete(uuid);
|
|
1410
1513
|
return true;
|
|
1411
1514
|
}
|
|
1412
1515
|
catch (error) {
|
|
1413
1516
|
this.clearProbeProtocol(uuid, 'V1');
|
|
1414
1517
|
Log === null || Log === void 0 ? void 0 : Log.debug('[ReactNativeBleTransport] Protocol V1 GetFeatures probe failed:', error);
|
|
1518
|
+
if (isWedgedWriteError(error)) {
|
|
1519
|
+
throw error;
|
|
1520
|
+
}
|
|
1415
1521
|
return false;
|
|
1416
1522
|
}
|
|
1417
1523
|
});
|
|
@@ -1422,7 +1528,7 @@ class ReactNativeBleTransport {
|
|
|
1422
1528
|
if (!this._messages || !this._messagesV2) {
|
|
1423
1529
|
return false;
|
|
1424
1530
|
}
|
|
1425
|
-
this.
|
|
1531
|
+
this.probingProtocols.set(uuid, 'V2');
|
|
1426
1532
|
(_a = this.protocolV2Assemblers.get(uuid)) === null || _a === void 0 ? void 0 : _a.reset();
|
|
1427
1533
|
const detected = yield transport.probeProtocolV2({
|
|
1428
1534
|
call: (name, data, options) => this.callProtocolV2(uuid, name, data, options),
|
|
@@ -1438,6 +1544,9 @@ class ReactNativeBleTransport {
|
|
|
1438
1544
|
if (!detected) {
|
|
1439
1545
|
this.clearProbeProtocol(uuid, 'V2');
|
|
1440
1546
|
}
|
|
1547
|
+
else {
|
|
1548
|
+
this.probingProtocols.delete(uuid);
|
|
1549
|
+
}
|
|
1441
1550
|
return detected;
|
|
1442
1551
|
});
|
|
1443
1552
|
}
|
|
@@ -1509,7 +1618,7 @@ class ReactNativeBleTransport {
|
|
|
1509
1618
|
}
|
|
1510
1619
|
});
|
|
1511
1620
|
}
|
|
1512
|
-
writeProtocolV2Packet(transport, base64, context, assertCurrentGeneration) {
|
|
1621
|
+
writeProtocolV2Packet(uuid, transport, base64, context, assertCurrentGeneration) {
|
|
1513
1622
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1514
1623
|
let attempt = 0;
|
|
1515
1624
|
for (;;) {
|
|
@@ -1518,7 +1627,15 @@ class ReactNativeBleTransport {
|
|
|
1518
1627
|
throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
|
|
1519
1628
|
}
|
|
1520
1629
|
try {
|
|
1521
|
-
yield transport.writeCharacteristic.writeWithoutResponse(
|
|
1630
|
+
yield this.writeBlePacket(uuid, base64, payload => transport.writeCharacteristic.writeWithoutResponse(payload), () => {
|
|
1631
|
+
try {
|
|
1632
|
+
assertCurrentGeneration();
|
|
1633
|
+
return !context.signal.aborted;
|
|
1634
|
+
}
|
|
1635
|
+
catch (_a) {
|
|
1636
|
+
return false;
|
|
1637
|
+
}
|
|
1638
|
+
});
|
|
1522
1639
|
assertCurrentGeneration();
|
|
1523
1640
|
return;
|
|
1524
1641
|
}
|
|
@@ -1539,7 +1656,7 @@ class ReactNativeBleTransport {
|
|
|
1539
1656
|
}
|
|
1540
1657
|
});
|
|
1541
1658
|
}
|
|
1542
|
-
writeProtocolV2Frame(transport$1, frame, context, assertCurrentGeneration) {
|
|
1659
|
+
writeProtocolV2Frame(uuid, transport$1, frame, context, assertCurrentGeneration) {
|
|
1543
1660
|
return __awaiter(this, void 0, void 0, function* () {
|
|
1544
1661
|
const tuning = getProtocolV2BleTuning();
|
|
1545
1662
|
const packetCapacity = resolveProtocolV2PacketCapacity({
|
|
@@ -1558,7 +1675,7 @@ class ReactNativeBleTransport {
|
|
|
1558
1675
|
burstPauseMs: FIRMWARE_UPLOAD_WRITE_PAUSE_MS,
|
|
1559
1676
|
flushDelayMs: FIRMWARE_UPLOAD_WRITE_FLUSH_DELAY_MS,
|
|
1560
1677
|
wait: delay,
|
|
1561
|
-
writePacket: packet => this.writeProtocolV2Packet(transport$1, buffer.Buffer.from(packet).toString('base64'), context, assertCurrentGeneration),
|
|
1678
|
+
writePacket: packet => this.writeProtocolV2Packet(uuid, transport$1, buffer.Buffer.from(packet).toString('base64'), context, assertCurrentGeneration),
|
|
1562
1679
|
});
|
|
1563
1680
|
});
|
|
1564
1681
|
}
|
|
@@ -1607,7 +1724,7 @@ class ReactNativeBleTransport {
|
|
|
1607
1724
|
writeFrame: (frame, context) => __awaiter(this, void 0, void 0, function* () {
|
|
1608
1725
|
assertCurrentGeneration();
|
|
1609
1726
|
const currentTransport = this.getCachedTransport(uuid);
|
|
1610
|
-
yield this.writeProtocolV2Frame(currentTransport, frame, context, assertCurrentGeneration);
|
|
1727
|
+
yield this.writeProtocolV2Frame(uuid, currentTransport, frame, context, assertCurrentGeneration);
|
|
1611
1728
|
}),
|
|
1612
1729
|
readFrame: () => __awaiter(this, void 0, void 0, function* () {
|
|
1613
1730
|
assertCurrentGeneration();
|
|
@@ -1628,10 +1745,12 @@ class ReactNativeBleTransport {
|
|
|
1628
1745
|
};
|
|
1629
1746
|
}
|
|
1630
1747
|
getProtocolType(path) {
|
|
1631
|
-
return this.
|
|
1748
|
+
return this.getActiveProtocol(path);
|
|
1632
1749
|
}
|
|
1633
1750
|
}
|
|
1634
1751
|
|
|
1752
|
+
exports.BLE_WRITE_PACKET_TIMEOUT_MS = BLE_WRITE_PACKET_TIMEOUT_MS;
|
|
1753
|
+
exports.BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD = BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD;
|
|
1635
1754
|
exports.configureProtocolV2BleTuning = configureProtocolV2BleTuning;
|
|
1636
1755
|
exports["default"] = ReactNativeBleTransport;
|
|
1637
1756
|
exports.getFirmwareUploadWriteRetryType = getFirmwareUploadWriteRetryType;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-transport-react-native",
|
|
3
|
-
"version": "1.2.0-alpha.
|
|
3
|
+
"version": "1.2.0-alpha.54",
|
|
4
4
|
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
"lint:fix": "eslint . --fix"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@onekeyfe/hd-core": "1.2.0-alpha.
|
|
24
|
-
"@onekeyfe/hd-shared": "1.2.0-alpha.
|
|
25
|
-
"@onekeyfe/hd-transport": "1.2.0-alpha.
|
|
23
|
+
"@onekeyfe/hd-core": "1.2.0-alpha.54",
|
|
24
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.54",
|
|
25
|
+
"@onekeyfe/hd-transport": "1.2.0-alpha.54",
|
|
26
26
|
"@onekeyfe/react-native-ble-utils": "^0.1.6",
|
|
27
27
|
"react-native-ble-plx": "3.5.1"
|
|
28
28
|
},
|
|
29
|
-
"gitHead": "
|
|
29
|
+
"gitHead": "a382a4719286e0df04fe706ed13099020eb8e13f"
|
|
30
30
|
}
|
|
@@ -482,6 +482,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
482
482
|
configureProtocolV2BleTuning({ iosPacketLength: 20 });
|
|
483
483
|
|
|
484
484
|
await transport.writeProtocolV2Frame(
|
|
485
|
+
'device-uuid',
|
|
485
486
|
bleTransport,
|
|
486
487
|
new Uint8Array(30),
|
|
487
488
|
context,
|
|
@@ -513,7 +514,13 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
513
514
|
configureProtocolV2BleTuning({ iosPacketLength: 20 });
|
|
514
515
|
|
|
515
516
|
await expect(
|
|
516
|
-
transport.writeProtocolV2Frame(
|
|
517
|
+
transport.writeProtocolV2Frame(
|
|
518
|
+
'device-uuid',
|
|
519
|
+
bleTransport,
|
|
520
|
+
new Uint8Array(30),
|
|
521
|
+
context,
|
|
522
|
+
jest.fn()
|
|
523
|
+
)
|
|
517
524
|
).rejects.toMatchObject({ errorCode: 205 });
|
|
518
525
|
expect(writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
519
526
|
});
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
|
|
3
|
+
import ReactNativeBleTransport, {
|
|
4
|
+
BLE_WRITE_PACKET_TIMEOUT_MS,
|
|
5
|
+
BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD,
|
|
6
|
+
} from '../index';
|
|
7
|
+
|
|
8
|
+
import messages from '@onekeyfe/hd-transport/messages.json';
|
|
9
|
+
|
|
10
|
+
jest.mock(
|
|
11
|
+
'react-native',
|
|
12
|
+
() => ({
|
|
13
|
+
Platform: { OS: 'ios', select: (spec: Record<string, unknown>) => spec.ios },
|
|
14
|
+
PermissionsAndroid: {
|
|
15
|
+
PERMISSIONS: {},
|
|
16
|
+
RESULTS: {},
|
|
17
|
+
request: jest.fn(),
|
|
18
|
+
requestMultiple: jest.fn(),
|
|
19
|
+
},
|
|
20
|
+
}),
|
|
21
|
+
{ virtual: true }
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
jest.mock('react-native-ble-plx', () => ({
|
|
25
|
+
BleATTErrorCode: { InvalidHandle: 1 },
|
|
26
|
+
BleError: Error,
|
|
27
|
+
BleErrorCode: { DeviceDisconnected: 201, OperationStartFailed: 601 },
|
|
28
|
+
BleManager: jest.fn(),
|
|
29
|
+
ScanMode: { LowLatency: 2 },
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
jest.mock('@onekeyfe/react-native-ble-utils', () => ({
|
|
33
|
+
__esModule: true,
|
|
34
|
+
default: {
|
|
35
|
+
getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
36
|
+
getBondedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
37
|
+
pairDevice: jest.fn(() => Promise.resolve()),
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const UUID = 'wedged-write-device';
|
|
42
|
+
|
|
43
|
+
const flush = () =>
|
|
44
|
+
new Promise(resolve => {
|
|
45
|
+
setImmediate(resolve);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/** Drive fake timers forward in slices until `settled()` reports done. */
|
|
49
|
+
async function advanceUntil(settled: () => boolean, totalMs: number, stepMs = 500) {
|
|
50
|
+
for (let elapsed = 0; elapsed < totalMs; elapsed += stepMs) {
|
|
51
|
+
jest.advanceTimersByTime(stepMs);
|
|
52
|
+
// eslint-disable-next-line no-await-in-loop
|
|
53
|
+
await flush();
|
|
54
|
+
if (settled()) return;
|
|
55
|
+
}
|
|
56
|
+
throw new Error(`fake timers exhausted after ${totalMs}ms before the call settled`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Drive fake timers forward without requiring the work to settle. */
|
|
60
|
+
async function drain(totalMs: number, stepMs = 500) {
|
|
61
|
+
for (let elapsed = 0; elapsed < totalMs; elapsed += stepMs) {
|
|
62
|
+
jest.advanceTimersByTime(stepMs);
|
|
63
|
+
// eslint-disable-next-line no-await-in-loop
|
|
64
|
+
await flush();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createHarness(writeImpl: () => Promise<void>) {
|
|
69
|
+
const t = new ReactNativeBleTransport({});
|
|
70
|
+
t.configure(messages);
|
|
71
|
+
(t as any).deviceProtocol.set(UUID, 'V1');
|
|
72
|
+
const writeWithoutResponse = jest.fn(writeImpl);
|
|
73
|
+
const fakeBleTransport = {
|
|
74
|
+
writeCharacteristic: { writeWithoutResponse },
|
|
75
|
+
writeWithRetry: jest.fn(writeImpl),
|
|
76
|
+
};
|
|
77
|
+
(t as any).getCachedTransport = () => fakeBleTransport;
|
|
78
|
+
const disconnectSpy = jest.spyOn(t, 'disconnect').mockResolvedValue(undefined);
|
|
79
|
+
return { t, disconnectSpy, writeWithoutResponse };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe('BLE packet write timeout', () => {
|
|
83
|
+
beforeAll(() => {
|
|
84
|
+
jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
afterAll(() => {
|
|
88
|
+
jest.useRealTimers();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
afterEach(() => {
|
|
92
|
+
jest.clearAllTimers();
|
|
93
|
+
jest.restoreAllMocks();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('a never-settling write rejects instead of hanging the call forever', async () => {
|
|
97
|
+
// iOS never resolves writeWithoutResponse when the peripheral stops reporting
|
|
98
|
+
// "ready to send"; without a bounded write the response timeout is never armed.
|
|
99
|
+
const { t } = createHarness(
|
|
100
|
+
() =>
|
|
101
|
+
new Promise<void>(() => {
|
|
102
|
+
// never settles
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const errors: Array<{ errorCode?: unknown }> = [];
|
|
107
|
+
let settled = false;
|
|
108
|
+
const call = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
|
|
109
|
+
call.catch(e => {
|
|
110
|
+
errors.push(e);
|
|
111
|
+
settled = true;
|
|
112
|
+
});
|
|
113
|
+
await flush();
|
|
114
|
+
|
|
115
|
+
await advanceUntil(() => settled, 30000);
|
|
116
|
+
|
|
117
|
+
expect(errors).toHaveLength(1);
|
|
118
|
+
expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleWriteCharacteristicError);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('a wedged write tears the BLE link down so the next call reconnects', async () => {
|
|
122
|
+
const { t, disconnectSpy } = createHarness(
|
|
123
|
+
() =>
|
|
124
|
+
new Promise<void>(() => {
|
|
125
|
+
// never settles
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
let settled = false;
|
|
130
|
+
const call = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
|
|
131
|
+
call.catch(() => {
|
|
132
|
+
settled = true;
|
|
133
|
+
});
|
|
134
|
+
await flush();
|
|
135
|
+
|
|
136
|
+
await advanceUntil(() => settled, 30000);
|
|
137
|
+
|
|
138
|
+
expect(disconnectSpy).toHaveBeenCalledWith(UUID);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('the write budget is per packet, not per call', async () => {
|
|
142
|
+
// Five packets at 4s each: the call's write phase far outlives one packet budget,
|
|
143
|
+
// but no single packet does, so nothing may be torn down.
|
|
144
|
+
const { t, disconnectSpy, writeWithoutResponse } = createHarness(
|
|
145
|
+
() =>
|
|
146
|
+
new Promise<void>(resolve => {
|
|
147
|
+
setTimeout(resolve, 4000);
|
|
148
|
+
})
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const errors: Array<{ errorCode?: unknown }> = [];
|
|
152
|
+
let settled = false;
|
|
153
|
+
const call = t.call(UUID, 'Ping', { message: 'x'.repeat(300) }, { timeoutMs: 120000 });
|
|
154
|
+
call.catch(e => {
|
|
155
|
+
errors.push(e);
|
|
156
|
+
settled = true;
|
|
157
|
+
});
|
|
158
|
+
await flush();
|
|
159
|
+
|
|
160
|
+
await advanceUntil(() => settled, 200000);
|
|
161
|
+
|
|
162
|
+
// Every packet was written: none was aborted even though the write phase as a
|
|
163
|
+
// whole ran far past a single packet budget.
|
|
164
|
+
expect(writeWithoutResponse.mock.calls.length).toBeGreaterThan(1);
|
|
165
|
+
expect(4000 * writeWithoutResponse.mock.calls.length).toBeGreaterThan(
|
|
166
|
+
BLE_WRITE_PACKET_TIMEOUT_MS
|
|
167
|
+
);
|
|
168
|
+
// Fails on the RESPONSE timeout the device never answered, not on a write timeout.
|
|
169
|
+
expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleTimeoutError);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('a superseded call whose write wedges must not tear down the successor link', async () => {
|
|
173
|
+
// Only the first call's write wedges; the successor writes normally.
|
|
174
|
+
let writes = 0;
|
|
175
|
+
const { t, disconnectSpy } = createHarness(() => {
|
|
176
|
+
writes += 1;
|
|
177
|
+
return writes === 1
|
|
178
|
+
? new Promise<void>(() => {
|
|
179
|
+
// never settles
|
|
180
|
+
})
|
|
181
|
+
: Promise.resolve();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const first = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
|
|
185
|
+
first.catch(() => undefined);
|
|
186
|
+
await flush();
|
|
187
|
+
|
|
188
|
+
// A forceRun call supersedes it and becomes the transport owner.
|
|
189
|
+
const second = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
|
|
190
|
+
second.catch(() => undefined);
|
|
191
|
+
await flush();
|
|
192
|
+
|
|
193
|
+
await drain(BLE_WRITE_PACKET_TIMEOUT_MS + 2000);
|
|
194
|
+
|
|
195
|
+
expect(disconnectSpy).not.toHaveBeenCalled();
|
|
196
|
+
expect(t.runPromise).not.toBeNull();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('repeated wedged writes recreate the BLE manager', async () => {
|
|
200
|
+
const { t, disconnectSpy } = createHarness(
|
|
201
|
+
() =>
|
|
202
|
+
new Promise<void>(() => {
|
|
203
|
+
// never settles
|
|
204
|
+
})
|
|
205
|
+
);
|
|
206
|
+
const destroy = jest.fn();
|
|
207
|
+
(t as any).blePlxManager = { destroy };
|
|
208
|
+
|
|
209
|
+
for (let attempt = 0; attempt < BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD; attempt += 1) {
|
|
210
|
+
(t as any).deviceProtocol.set(UUID, 'V1');
|
|
211
|
+
let settled = false;
|
|
212
|
+
const call = t.call(UUID, 'Initialize', {}, { timeoutMs: 25000 });
|
|
213
|
+
call.catch(() => {
|
|
214
|
+
settled = true;
|
|
215
|
+
});
|
|
216
|
+
// eslint-disable-next-line no-await-in-loop
|
|
217
|
+
await flush();
|
|
218
|
+
// eslint-disable-next-line no-await-in-loop
|
|
219
|
+
await advanceUntil(() => settled, BLE_WRITE_PACKET_TIMEOUT_MS + 2000);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
expect(disconnectSpy).toHaveBeenCalledTimes(BLE_WRITE_TIMEOUT_MANAGER_RESET_THRESHOLD);
|
|
223
|
+
expect(destroy).toHaveBeenCalledTimes(1);
|
|
224
|
+
expect((t as any).blePlxManager).toBeUndefined();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('slow but progressing writes are not aborted', async () => {
|
|
228
|
+
// Each packet takes a while but keeps completing: this must not be mistaken
|
|
229
|
+
// for a wedged link, even when the whole call outlives a single packet budget.
|
|
230
|
+
const { t, writeWithoutResponse } = createHarness(
|
|
231
|
+
() =>
|
|
232
|
+
new Promise<void>(resolve => {
|
|
233
|
+
setTimeout(resolve, 4000);
|
|
234
|
+
})
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const errors: unknown[] = [];
|
|
238
|
+
let settled = false;
|
|
239
|
+
const call = t.call(UUID, 'Initialize', {}, { timeoutMs: 60000 });
|
|
240
|
+
call.catch(e => {
|
|
241
|
+
errors.push(e);
|
|
242
|
+
settled = true;
|
|
243
|
+
});
|
|
244
|
+
await flush();
|
|
245
|
+
|
|
246
|
+
// Let every packet drain; the call then waits for a device response that never
|
|
247
|
+
// comes, and must fail with the RESPONSE timeout, not a write timeout.
|
|
248
|
+
await advanceUntil(() => settled, 90000);
|
|
249
|
+
|
|
250
|
+
expect(writeWithoutResponse).toHaveBeenCalled();
|
|
251
|
+
expect(errors).toHaveLength(1);
|
|
252
|
+
expect((errors[0] as { errorCode?: unknown })?.errorCode).toBe(
|
|
253
|
+
HardwareErrorCode.BleTimeoutError
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('protocol probe state visibility', () => {
|
|
259
|
+
beforeAll(() => {
|
|
260
|
+
jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
afterAll(() => {
|
|
264
|
+
jest.useRealTimers();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
afterEach(() => {
|
|
268
|
+
jest.clearAllTimers();
|
|
269
|
+
jest.restoreAllMocks();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test('an in-flight probe does not publish its protocol as confirmed', async () => {
|
|
273
|
+
const { t } = createHarness(
|
|
274
|
+
() =>
|
|
275
|
+
new Promise<void>(() => {
|
|
276
|
+
// never settles
|
|
277
|
+
})
|
|
278
|
+
);
|
|
279
|
+
(t as any).deviceProtocol.delete(UUID);
|
|
280
|
+
|
|
281
|
+
const probe = (t as any).probeProtocolV1(UUID) as Promise<boolean>;
|
|
282
|
+
probe.catch(() => undefined);
|
|
283
|
+
await flush();
|
|
284
|
+
|
|
285
|
+
// The call itself must still route as V1 while probing...
|
|
286
|
+
expect(t.getProtocolType(UUID)).toBe('V1');
|
|
287
|
+
// ...but acquire's cached-transport reuse gate must not treat it as detected.
|
|
288
|
+
expect((t as any).deviceProtocol.get(UUID)).toBeUndefined();
|
|
289
|
+
|
|
290
|
+
await drain(BLE_WRITE_PACKET_TIMEOUT_MS + 2000);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('a probe that confirms its protocol leaves no probing entry behind', async () => {
|
|
294
|
+
const { t } = createHarness(() => Promise.resolve());
|
|
295
|
+
(t as any).deviceProtocol.delete(UUID);
|
|
296
|
+
|
|
297
|
+
const probe = (t as any).probeProtocolV1(UUID) as Promise<boolean>;
|
|
298
|
+
probe.catch(() => undefined);
|
|
299
|
+
await flush();
|
|
300
|
+
// The device answers the probe.
|
|
301
|
+
t.runPromise?.resolve('deadbeef');
|
|
302
|
+
await flush();
|
|
303
|
+
|
|
304
|
+
expect((t as any).probingProtocols.size).toBe(0);
|
|
305
|
+
});
|
|
306
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -106,6 +106,22 @@ const resolveFirmwareUploadRetryDelay = (attempt: number, baseDelayMs = 200, max
|
|
|
106
106
|
Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
|
|
107
107
|
const PROTOCOL_PROBE_TIMEOUT_MS = 1000;
|
|
108
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;
|
|
109
125
|
const DEVICE_SCAN_TIMEOUT_MS = 3000;
|
|
110
126
|
const IOS_NOTIFY_READY_DELAY_MS = 150;
|
|
111
127
|
const ANDROID_NOTIFY_READY_DELAY_MS = 300;
|
|
@@ -256,6 +272,17 @@ export default class ReactNativeBleTransport {
|
|
|
256
272
|
/** Per-device protocol type detected by active wire-level probe after connect. */
|
|
257
273
|
private deviceProtocol: Map<string, ProtocolType> = new Map();
|
|
258
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
|
+
|
|
259
286
|
private deviceProtocolHints: Map<string, ProtocolType> = new Map();
|
|
260
287
|
|
|
261
288
|
private protocolV2Assemblers: Map<string, ProtocolV2FrameAssembler> = new Map();
|
|
@@ -879,7 +906,7 @@ export default class ReactNativeBleTransport {
|
|
|
879
906
|
Log?.debug('monitor error ignored for stale transport: ', uuid, notifyTransactionId);
|
|
880
907
|
return;
|
|
881
908
|
}
|
|
882
|
-
if (this.
|
|
909
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
883
910
|
let errorCode:
|
|
884
911
|
| typeof HardwareErrorCode.BleDeviceBondError
|
|
885
912
|
| typeof HardwareErrorCode.BleCharacteristicNotifyError
|
|
@@ -949,7 +976,7 @@ export default class ReactNativeBleTransport {
|
|
|
949
976
|
|
|
950
977
|
try {
|
|
951
978
|
const data = Buffer.from(c.value as string, 'base64');
|
|
952
|
-
const protocol = this.
|
|
979
|
+
const protocol = this.getActiveProtocol(uuid);
|
|
953
980
|
if (!protocol) {
|
|
954
981
|
Log?.debug('monitor data ignored before protocol detection: ', uuid);
|
|
955
982
|
return;
|
|
@@ -983,7 +1010,7 @@ export default class ReactNativeBleTransport {
|
|
|
983
1010
|
} catch (error) {
|
|
984
1011
|
Log?.debug('monitor data error: ', error);
|
|
985
1012
|
const notifyError = ERRORS.TypedError(HardwareErrorCode.BleWriteCharacteristicError);
|
|
986
|
-
if (this.
|
|
1013
|
+
if (this.getActiveProtocol(uuid) === 'V2') {
|
|
987
1014
|
this.rejectProtocolV2Frames(uuid, notifyError);
|
|
988
1015
|
} else if (this.runPromiseDeviceId === uuid) {
|
|
989
1016
|
this.runPromise?.reject(notifyError);
|
|
@@ -1047,6 +1074,7 @@ export default class ReactNativeBleTransport {
|
|
|
1047
1074
|
}
|
|
1048
1075
|
|
|
1049
1076
|
this.deviceProtocol.delete(uuid);
|
|
1077
|
+
this.probingProtocols.delete(uuid);
|
|
1050
1078
|
// Preserve a name-derived hint across disconnects so reconnect can probe V2 first.
|
|
1051
1079
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1052
1080
|
this.protocolV2Assemblers.delete(uuid);
|
|
@@ -1131,6 +1159,7 @@ export default class ReactNativeBleTransport {
|
|
|
1131
1159
|
this.runPromiseDeviceId = null;
|
|
1132
1160
|
}
|
|
1133
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,7 +1235,13 @@ 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);
|
|
@@ -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);
|
|
@@ -1251,10 +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
|
-
await
|
|
1300
|
+
await this.writeBlePacket(
|
|
1301
|
+
uuid,
|
|
1302
|
+
outData,
|
|
1303
|
+
payload => transport.writeCharacteristic.writeWithoutResponse(payload),
|
|
1304
|
+
isCurrentOwner
|
|
1305
|
+
);
|
|
1255
1306
|
} catch (e) {
|
|
1256
1307
|
Log?.debug('writeCharacteristic write error: ', e);
|
|
1257
1308
|
releaseOwnershipIfCurrent();
|
|
1309
|
+
if (isWedgedWriteError(e)) {
|
|
1310
|
+
throw e;
|
|
1311
|
+
}
|
|
1258
1312
|
if (e.errorCode === BleErrorCode.DeviceDisconnected) {
|
|
1259
1313
|
throw ERRORS.TypedError(HardwareErrorCode.BleDeviceNotBonded);
|
|
1260
1314
|
} else if (e.errorCode === BleErrorCode.OperationStartFailed) {
|
|
@@ -1383,6 +1437,7 @@ export default class ReactNativeBleTransport {
|
|
|
1383
1437
|
delete transportCache[session];
|
|
1384
1438
|
}
|
|
1385
1439
|
this.deviceProtocol.delete(session);
|
|
1440
|
+
this.probingProtocols.delete(session);
|
|
1386
1441
|
this.deviceProtocolHints.delete(session);
|
|
1387
1442
|
this.protocolV2Assemblers.delete(session);
|
|
1388
1443
|
this.resetProtocolV2Frames(session);
|
|
@@ -1417,6 +1472,105 @@ export default class ReactNativeBleTransport {
|
|
|
1417
1472
|
return transport;
|
|
1418
1473
|
}
|
|
1419
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
|
+
|
|
1420
1574
|
private createProtocolMismatchError(expected: ProtocolType) {
|
|
1421
1575
|
return ERRORS.TypedError(
|
|
1422
1576
|
HardwareErrorCode.RuntimeError,
|
|
@@ -1432,11 +1586,19 @@ export default class ReactNativeBleTransport {
|
|
|
1432
1586
|
}
|
|
1433
1587
|
|
|
1434
1588
|
private clearProbeProtocol(uuid: string, protocol: ProtocolType) {
|
|
1589
|
+
if (this.probingProtocols.get(uuid) === protocol) {
|
|
1590
|
+
this.probingProtocols.delete(uuid);
|
|
1591
|
+
}
|
|
1435
1592
|
if (this.deviceProtocol.get(uuid) === protocol) {
|
|
1436
1593
|
this.deviceProtocol.delete(uuid);
|
|
1437
1594
|
}
|
|
1438
1595
|
}
|
|
1439
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
|
+
|
|
1440
1602
|
private async detectProtocol(
|
|
1441
1603
|
uuid: string,
|
|
1442
1604
|
expectedProtocol?: ProtocolType,
|
|
@@ -1500,6 +1662,7 @@ export default class ReactNativeBleTransport {
|
|
|
1500
1662
|
}
|
|
1501
1663
|
|
|
1502
1664
|
this.deviceProtocol.delete(uuid);
|
|
1665
|
+
this.probingProtocols.delete(uuid);
|
|
1503
1666
|
throw this.createProtocolDetectionError();
|
|
1504
1667
|
}
|
|
1505
1668
|
|
|
@@ -1561,14 +1724,20 @@ export default class ReactNativeBleTransport {
|
|
|
1561
1724
|
}
|
|
1562
1725
|
|
|
1563
1726
|
try {
|
|
1564
|
-
this.
|
|
1727
|
+
this.probingProtocols.set(uuid, 'V1');
|
|
1565
1728
|
// GetFeatures identifies Protocol V1 without resetting an existing wallet
|
|
1566
1729
|
// session before Core has a chance to restore a hidden wallet.
|
|
1567
1730
|
await this.callProtocolV1(uuid, 'GetFeatures', {}, { timeoutMs: PROTOCOL_PROBE_TIMEOUT_MS });
|
|
1731
|
+
this.probingProtocols.delete(uuid);
|
|
1568
1732
|
return true;
|
|
1569
1733
|
} catch (error) {
|
|
1570
1734
|
this.clearProbeProtocol(uuid, 'V1');
|
|
1571
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
|
+
}
|
|
1572
1741
|
return false;
|
|
1573
1742
|
}
|
|
1574
1743
|
}
|
|
@@ -1578,7 +1747,7 @@ export default class ReactNativeBleTransport {
|
|
|
1578
1747
|
return false;
|
|
1579
1748
|
}
|
|
1580
1749
|
|
|
1581
|
-
this.
|
|
1750
|
+
this.probingProtocols.set(uuid, 'V2');
|
|
1582
1751
|
this.protocolV2Assemblers.get(uuid)?.reset();
|
|
1583
1752
|
const detected = await probeProtocolV2Helper({
|
|
1584
1753
|
call: (name: string, data: Record<string, unknown>, options?: TransportCallOptions) =>
|
|
@@ -1593,6 +1762,8 @@ export default class ReactNativeBleTransport {
|
|
|
1593
1762
|
});
|
|
1594
1763
|
if (!detected) {
|
|
1595
1764
|
this.clearProbeProtocol(uuid, 'V2');
|
|
1765
|
+
} else {
|
|
1766
|
+
this.probingProtocols.delete(uuid);
|
|
1596
1767
|
}
|
|
1597
1768
|
return detected;
|
|
1598
1769
|
}
|
|
@@ -1674,6 +1845,7 @@ export default class ReactNativeBleTransport {
|
|
|
1674
1845
|
}
|
|
1675
1846
|
|
|
1676
1847
|
private async writeProtocolV2Packet(
|
|
1848
|
+
uuid: string,
|
|
1677
1849
|
transport: BleTransport,
|
|
1678
1850
|
base64: string,
|
|
1679
1851
|
context: ProtocolV2CallContext,
|
|
@@ -1686,7 +1858,21 @@ export default class ReactNativeBleTransport {
|
|
|
1686
1858
|
throw new Error(`Protocol V2 BLE write aborted for ${context.messageName}`);
|
|
1687
1859
|
}
|
|
1688
1860
|
try {
|
|
1689
|
-
await
|
|
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
|
+
);
|
|
1690
1876
|
assertCurrentGeneration();
|
|
1691
1877
|
return;
|
|
1692
1878
|
} catch (error) {
|
|
@@ -1709,6 +1895,7 @@ export default class ReactNativeBleTransport {
|
|
|
1709
1895
|
}
|
|
1710
1896
|
|
|
1711
1897
|
private async writeProtocolV2Frame(
|
|
1898
|
+
uuid: string,
|
|
1712
1899
|
transport: BleTransport,
|
|
1713
1900
|
frame: Uint8Array,
|
|
1714
1901
|
context: ProtocolV2CallContext,
|
|
@@ -1733,6 +1920,7 @@ export default class ReactNativeBleTransport {
|
|
|
1733
1920
|
wait: delay,
|
|
1734
1921
|
writePacket: packet =>
|
|
1735
1922
|
this.writeProtocolV2Packet(
|
|
1923
|
+
uuid,
|
|
1736
1924
|
transport,
|
|
1737
1925
|
Buffer.from(packet).toString('base64'),
|
|
1738
1926
|
context,
|
|
@@ -1797,7 +1985,13 @@ export default class ReactNativeBleTransport {
|
|
|
1797
1985
|
writeFrame: async (frame: Uint8Array, context: ProtocolV2CallContext) => {
|
|
1798
1986
|
assertCurrentGeneration();
|
|
1799
1987
|
const currentTransport = this.getCachedTransport(uuid);
|
|
1800
|
-
await this.writeProtocolV2Frame(
|
|
1988
|
+
await this.writeProtocolV2Frame(
|
|
1989
|
+
uuid,
|
|
1990
|
+
currentTransport,
|
|
1991
|
+
frame,
|
|
1992
|
+
context,
|
|
1993
|
+
assertCurrentGeneration
|
|
1994
|
+
);
|
|
1801
1995
|
},
|
|
1802
1996
|
readFrame: async () => {
|
|
1803
1997
|
assertCurrentGeneration();
|
|
@@ -1822,6 +2016,6 @@ export default class ReactNativeBleTransport {
|
|
|
1822
2016
|
}
|
|
1823
2017
|
|
|
1824
2018
|
getProtocolType(path: string): ProtocolType | undefined {
|
|
1825
|
-
return this.
|
|
2019
|
+
return this.getActiveProtocol(path);
|
|
1826
2020
|
}
|
|
1827
2021
|
}
|