@onekeyfe/hd-transport-react-native 1.2.2-alpha.119 → 1.2.2-alpha.120
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 -29
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +59 -265
- package/package.json +5 -5
- package/src/__tests__/connectTimeout.test.ts +1 -85
- package/src/__tests__/protocolV2Link.test.ts +25 -600
- package/src/index.ts +67 -366
|
@@ -8,7 +8,6 @@ import { ERRORS, HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
|
|
|
8
8
|
|
|
9
9
|
import ReactNativeBleTransport, {
|
|
10
10
|
BLE_NATIVE_TEARDOWN_TIMEOUT_MS,
|
|
11
|
-
BLE_SETUP_WEDGED_MESSAGE,
|
|
12
11
|
BLE_WRITE_PACKET_TIMEOUT_MS,
|
|
13
12
|
configureProtocolV2BleTuning,
|
|
14
13
|
getFirmwareUploadWriteRetryType,
|
|
@@ -170,13 +169,11 @@ const createHarness = ({
|
|
|
170
169
|
serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
|
|
171
170
|
isConnected: jest.fn(() => Promise.resolve(true)),
|
|
172
171
|
cancelConnection: jest.fn(() => Promise.resolve()),
|
|
173
|
-
connect: jest.fn(),
|
|
174
172
|
onDisconnected: jest.fn(callback => {
|
|
175
173
|
disconnectCallback = callback;
|
|
176
174
|
return { remove: jest.fn() };
|
|
177
175
|
}),
|
|
178
176
|
} as any;
|
|
179
|
-
device.connect.mockResolvedValue(device);
|
|
180
177
|
device.requestMTU = jest.fn(() => Promise.resolve(device));
|
|
181
178
|
device.requestConnectionPriority = jest.fn(() => Promise.resolve(device));
|
|
182
179
|
const bleManager = {
|
|
@@ -618,7 +615,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
618
615
|
uuid,
|
|
619
616
|
protocolType: 'V2',
|
|
620
617
|
});
|
|
621
|
-
expect(device.requestMTU).
|
|
618
|
+
expect(device.requestMTU).toHaveBeenCalledWith(247);
|
|
622
619
|
expect(writeCharacteristic.writeWithResponse).toHaveBeenCalledTimes(1);
|
|
623
620
|
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalled();
|
|
624
621
|
|
|
@@ -1082,621 +1079,57 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
1082
1079
|
await transport.release(uuid, true);
|
|
1083
1080
|
});
|
|
1084
1081
|
|
|
1085
|
-
test('
|
|
1082
|
+
test('continues with the current MTU when the connected snapshot refresh fails', async () => {
|
|
1086
1083
|
const { transport, uuid, device } = createHarness();
|
|
1084
|
+
const mtuError = new Error('MTU refresh failed');
|
|
1085
|
+
device.requestMTU.mockRejectedValueOnce(mtuError);
|
|
1087
1086
|
|
|
1088
1087
|
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
1089
1088
|
uuid,
|
|
1090
1089
|
protocolType: 'V2',
|
|
1091
1090
|
});
|
|
1092
|
-
expect(device.requestMTU).
|
|
1091
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(1);
|
|
1093
1092
|
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(247);
|
|
1094
1093
|
await transport.release(uuid, true);
|
|
1095
1094
|
});
|
|
1096
1095
|
|
|
1097
|
-
test('
|
|
1096
|
+
test('refreshes a transient bootloader MTU after notifications are ready', async () => {
|
|
1098
1097
|
const { transport, uuid, device } = createHarness();
|
|
1099
1098
|
device.mtu = 23;
|
|
1100
|
-
device.requestMTU
|
|
1101
|
-
device
|
|
1102
|
-
|
|
1103
|
-
|
|
1099
|
+
device.requestMTU
|
|
1100
|
+
.mockResolvedValueOnce(device)
|
|
1101
|
+
.mockResolvedValueOnce(device)
|
|
1102
|
+
.mockImplementationOnce(() => {
|
|
1103
|
+
device.mtu = 247;
|
|
1104
|
+
return Promise.resolve(device);
|
|
1105
|
+
});
|
|
1104
1106
|
|
|
1105
1107
|
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
1106
1108
|
uuid,
|
|
1107
1109
|
protocolType: 'V2',
|
|
1108
1110
|
});
|
|
1109
|
-
expect(device.requestMTU).toHaveBeenCalledTimes(
|
|
1110
|
-
expect(device.requestMTU).toHaveBeenCalledWith(
|
|
1111
|
-
247,
|
|
1112
|
-
expect.stringContaining(`${uuid}:mtu:connected:0:`)
|
|
1113
|
-
);
|
|
1111
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(3);
|
|
1114
1112
|
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(247);
|
|
1115
1113
|
await transport.release(uuid, true);
|
|
1116
1114
|
});
|
|
1117
1115
|
|
|
1118
|
-
test('continues with a low MTU when the
|
|
1116
|
+
test('continues with a low bootloader MTU when the bounded retry fails', async () => {
|
|
1119
1117
|
const { transport, uuid, device } = createHarness();
|
|
1120
1118
|
device.mtu = 23;
|
|
1121
|
-
device.requestMTU
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
protocolType: 'V2',
|
|
1126
|
-
});
|
|
1127
|
-
expect(device.requestMTU).toHaveBeenCalledTimes(1);
|
|
1128
|
-
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(23);
|
|
1129
|
-
await transport.release(uuid, true);
|
|
1130
|
-
});
|
|
1131
|
-
|
|
1132
|
-
test('bounds a stalled MTU negotiation and continues protocol probing', async () => {
|
|
1133
|
-
const { transport, uuid, device, bleManager } = createHarness();
|
|
1134
|
-
device.mtu = 23;
|
|
1135
|
-
device.requestMTU.mockImplementationOnce(() => new Promise(() => {}));
|
|
1136
|
-
|
|
1137
|
-
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
1138
|
-
uuid,
|
|
1139
|
-
protocolType: 'V2',
|
|
1140
|
-
});
|
|
1141
|
-
const transactionId = device.requestMTU.mock.calls[0]?.[1];
|
|
1142
|
-
expect(transactionId).toEqual(expect.stringContaining(`${uuid}:mtu:connected:0:`));
|
|
1143
|
-
expect(bleManager.cancelTransaction).toHaveBeenCalledWith(transactionId);
|
|
1144
|
-
expect(device.cancelConnection).toHaveBeenCalled();
|
|
1145
|
-
expect(device.connect).toHaveBeenCalledWith(
|
|
1146
|
-
expect.objectContaining({ timeout: expect.any(Number) })
|
|
1147
|
-
);
|
|
1148
|
-
expect(device.connect.mock.calls.at(-1)?.[0]).not.toHaveProperty('requestMTU');
|
|
1149
|
-
expect(device.requestMTU).toHaveBeenCalledTimes(1);
|
|
1150
|
-
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(23);
|
|
1151
|
-
await transport.release(uuid, true);
|
|
1152
|
-
}, 10_000);
|
|
1153
|
-
|
|
1154
|
-
test('keeps the iOS link when the MTU-timeout reconnect reports it is still connected', async () => {
|
|
1155
|
-
const { BleError: BleErrorMock, BleErrorCode } = jest.requireMock('react-native-ble-plx');
|
|
1156
|
-
const { transport, uuid, device, bleManager } = createHarness();
|
|
1157
|
-
device.mtu = 23;
|
|
1158
|
-
device.requestMTU.mockImplementationOnce(() => new Promise(() => {}));
|
|
1159
|
-
device.connect.mockRejectedValueOnce(
|
|
1160
|
-
Object.assign(new BleErrorMock('Device already connected'), {
|
|
1161
|
-
errorCode: BleErrorCode.DeviceAlreadyConnected,
|
|
1162
|
-
})
|
|
1163
|
-
);
|
|
1119
|
+
device.requestMTU
|
|
1120
|
+
.mockResolvedValueOnce(device)
|
|
1121
|
+
.mockResolvedValueOnce(device)
|
|
1122
|
+
.mockRejectedValueOnce(new Error('bootloader MTU retry failed'));
|
|
1164
1123
|
|
|
1165
1124
|
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
1166
1125
|
uuid,
|
|
1167
1126
|
protocolType: 'V2',
|
|
1168
1127
|
});
|
|
1169
|
-
expect(device.
|
|
1170
|
-
expect(device.connect).toHaveBeenCalledTimes(1);
|
|
1171
|
-
expect(device.requestMTU).toHaveBeenCalledTimes(1);
|
|
1172
|
-
expect(bleManager.destroy).not.toHaveBeenCalled();
|
|
1128
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(3);
|
|
1173
1129
|
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(23);
|
|
1174
1130
|
await transport.release(uuid, true);
|
|
1175
|
-
}, 10_000);
|
|
1176
|
-
|
|
1177
|
-
test('reconnects after an iOS MTU timeout when cancelling the timed-out link fails', async () => {
|
|
1178
|
-
const { BleError: BleErrorMock, BleErrorCode } = jest.requireMock('react-native-ble-plx');
|
|
1179
|
-
const { transport, uuid, device, bleManager } = createHarness();
|
|
1180
|
-
device.mtu = 23;
|
|
1181
|
-
device.requestMTU.mockImplementationOnce(() => new Promise(() => {}));
|
|
1182
|
-
device.cancelConnection.mockRejectedValueOnce(
|
|
1183
|
-
Object.assign(new BleErrorMock('Operation was cancelled'), {
|
|
1184
|
-
errorCode: BleErrorCode.OperationCancelled,
|
|
1185
|
-
})
|
|
1186
|
-
);
|
|
1187
|
-
|
|
1188
|
-
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
1189
|
-
uuid,
|
|
1190
|
-
protocolType: 'V2',
|
|
1191
|
-
});
|
|
1192
|
-
expect(device.connect).toHaveBeenCalledTimes(1);
|
|
1193
|
-
expect(device.connect.mock.calls[0]?.[0]).not.toHaveProperty('requestMTU');
|
|
1194
|
-
expect(bleManager.destroy).not.toHaveBeenCalled();
|
|
1195
|
-
await transport.release(uuid, true);
|
|
1196
|
-
}, 10_000);
|
|
1197
|
-
|
|
1198
|
-
test('maps a stale bond from the iOS MTU-timeout reconnect', async () => {
|
|
1199
|
-
const { BleError: BleErrorMock } = jest.requireMock('react-native-ble-plx');
|
|
1200
|
-
const { transport, uuid, device } = createHarness();
|
|
1201
|
-
device.mtu = 23;
|
|
1202
|
-
device.requestMTU.mockImplementationOnce(() => new Promise(() => {}));
|
|
1203
|
-
device.connect.mockRejectedValueOnce(
|
|
1204
|
-
Object.assign(new BleErrorMock('Peer removed pairing information'), {
|
|
1205
|
-
errorCode: 200,
|
|
1206
|
-
iosErrorCode: 14,
|
|
1207
|
-
})
|
|
1208
|
-
);
|
|
1209
|
-
|
|
1210
|
-
await expect(transport.acquire({ uuid })).rejects.toMatchObject({
|
|
1211
|
-
errorCode: HardwareErrorCode.BlePeerRemovedPairingInformation,
|
|
1212
|
-
});
|
|
1213
|
-
await transport.release(uuid, true).catch(() => undefined);
|
|
1214
|
-
}, 10_000);
|
|
1215
|
-
|
|
1216
|
-
test('bounds a hung iOS MTU-timeout teardown and resets the BLE manager without reconnecting', async () => {
|
|
1217
|
-
const { transport, uuid, device, bleManager } = createHarness();
|
|
1218
|
-
device.mtu = 23;
|
|
1219
|
-
device.requestMTU.mockImplementationOnce(() => new Promise(() => {}));
|
|
1220
|
-
device.cancelConnection.mockImplementationOnce(() => new Promise(() => {}));
|
|
1221
|
-
|
|
1222
|
-
await expect(transport.acquire({ uuid })).rejects.toMatchObject({
|
|
1223
|
-
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
1224
|
-
message: expect.stringContaining('BLE MTU cleanup timed out'),
|
|
1225
|
-
});
|
|
1226
|
-
expect(bleManager.destroy).toHaveBeenCalledTimes(1);
|
|
1227
|
-
expect(transport.blePlxManager).toBeUndefined();
|
|
1228
|
-
expect(device.connect).not.toHaveBeenCalled();
|
|
1229
|
-
expect((transport as any).lifecycleOperations.has(uuid)).toBe(false);
|
|
1230
|
-
}, 15_000);
|
|
1231
|
-
|
|
1232
|
-
test('does not reconnect after a hung iOS MTU-timeout teardown when the manager reset is already pending', async () => {
|
|
1233
|
-
const { transport, uuid, device, bleManager } = createHarness();
|
|
1234
|
-
device.mtu = 23;
|
|
1235
|
-
device.requestMTU.mockImplementationOnce(() => new Promise(() => {}));
|
|
1236
|
-
device.cancelConnection.mockImplementationOnce(() => new Promise(() => {}));
|
|
1237
|
-
// Another device's reset is still draining, so this teardown timeout cannot swap the manager.
|
|
1238
|
-
jest.spyOn(transport as any, 'resetPlxManager').mockImplementation(() => undefined);
|
|
1239
|
-
|
|
1240
|
-
await expect(transport.acquire({ uuid })).rejects.toMatchObject({
|
|
1241
|
-
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
1242
|
-
});
|
|
1243
|
-
expect(transport.blePlxManager).toBe(bleManager);
|
|
1244
|
-
expect(device.connect).not.toHaveBeenCalled();
|
|
1245
|
-
}, 15_000);
|
|
1246
|
-
|
|
1247
|
-
test('does not request MTU again after connect falls back without requestMTU', async () => {
|
|
1248
|
-
const { BleError: BleErrorMock, BleErrorCode } = jest.requireMock('react-native-ble-plx');
|
|
1249
|
-
const { transport, uuid, device } = createHarness();
|
|
1250
|
-
device.mtu = 23;
|
|
1251
|
-
device.isConnected.mockResolvedValueOnce(false).mockResolvedValue(true);
|
|
1252
|
-
device.connect
|
|
1253
|
-
.mockRejectedValueOnce(
|
|
1254
|
-
Object.assign(new BleErrorMock('Operation was cancelled'), {
|
|
1255
|
-
errorCode: BleErrorCode.OperationCancelled,
|
|
1256
|
-
})
|
|
1257
|
-
)
|
|
1258
|
-
.mockResolvedValue(device);
|
|
1259
|
-
|
|
1260
|
-
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
1261
|
-
uuid,
|
|
1262
|
-
protocolType: 'V2',
|
|
1263
|
-
});
|
|
1264
|
-
expect(device.connect).toHaveBeenCalledTimes(2);
|
|
1265
|
-
expect(device.connect.mock.calls[0][0]).toEqual(expect.objectContaining({ requestMTU: 247 }));
|
|
1266
|
-
expect(device.connect.mock.calls[1][0]).not.toHaveProperty('requestMTU');
|
|
1267
|
-
expect(device.requestMTU).not.toHaveBeenCalled();
|
|
1268
|
-
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(23);
|
|
1269
|
-
await transport.release(uuid, true);
|
|
1270
|
-
});
|
|
1271
|
-
|
|
1272
|
-
test('keeps the iOS connect chain unchanged', async () => {
|
|
1273
|
-
const { transport, uuid, device } = createHarness();
|
|
1274
|
-
device.isConnected.mockResolvedValueOnce(false);
|
|
1275
|
-
|
|
1276
|
-
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).resolves.toEqual({
|
|
1277
|
-
uuid,
|
|
1278
|
-
protocolType: 'V2',
|
|
1279
|
-
});
|
|
1280
|
-
expect(device.connect).toHaveBeenCalledWith({
|
|
1281
|
-
requestMTU: 247,
|
|
1282
|
-
timeout: expect.any(Number),
|
|
1283
|
-
refreshGatt: 'OnConnected',
|
|
1284
|
-
});
|
|
1285
|
-
expect(device.requestMTU).not.toHaveBeenCalled();
|
|
1286
|
-
await transport.release(uuid, true);
|
|
1287
|
-
});
|
|
1288
|
-
|
|
1289
|
-
describe('Android MTU before service discovery', () => {
|
|
1290
|
-
beforeEach(() => {
|
|
1291
|
-
setPlatformOS('android');
|
|
1292
|
-
jest.useFakeTimers({
|
|
1293
|
-
doNotFake: ['setImmediate', 'queueMicrotask', 'nextTick', 'performance'],
|
|
1294
|
-
});
|
|
1295
|
-
});
|
|
1296
|
-
|
|
1297
|
-
afterEach(() => {
|
|
1298
|
-
jest.clearAllTimers();
|
|
1299
|
-
jest.useRealTimers();
|
|
1300
|
-
});
|
|
1301
|
-
|
|
1302
|
-
const advanceUntil = async (condition: () => boolean, budgetMs = 60_000) => {
|
|
1303
|
-
for (let elapsed = 0; !condition(); elapsed += 50) {
|
|
1304
|
-
if (elapsed >= budgetMs) throw new Error(`fake time exhausted after ${budgetMs}ms`);
|
|
1305
|
-
jest.advanceTimersByTime(50);
|
|
1306
|
-
// eslint-disable-next-line no-await-in-loop
|
|
1307
|
-
await new Promise(resolve => {
|
|
1308
|
-
setImmediate(resolve);
|
|
1309
|
-
});
|
|
1310
|
-
}
|
|
1311
|
-
};
|
|
1312
|
-
|
|
1313
|
-
const settle = async <T>(promise: Promise<T>, budgetMs?: number): Promise<T> => {
|
|
1314
|
-
let done = false;
|
|
1315
|
-
promise.then(
|
|
1316
|
-
() => {
|
|
1317
|
-
done = true;
|
|
1318
|
-
},
|
|
1319
|
-
() => {
|
|
1320
|
-
done = true;
|
|
1321
|
-
}
|
|
1322
|
-
);
|
|
1323
|
-
await advanceUntil(() => done, budgetMs);
|
|
1324
|
-
return promise;
|
|
1325
|
-
};
|
|
1326
|
-
|
|
1327
|
-
/**
|
|
1328
|
-
* A device whose LE link is only up between connect() and cancelConnection(), so the
|
|
1329
|
-
* transport has to reconnect after every drop instead of the test flipping the link.
|
|
1330
|
-
*/
|
|
1331
|
-
const reconnectingDevice = (device: any) => {
|
|
1332
|
-
const link = { connected: false };
|
|
1333
|
-
device.isConnected.mockImplementation(() => Promise.resolve(link.connected));
|
|
1334
|
-
device.connect = jest.fn(() => {
|
|
1335
|
-
link.connected = true;
|
|
1336
|
-
return Promise.resolve(device);
|
|
1337
|
-
});
|
|
1338
|
-
device.cancelConnection.mockImplementation(() => {
|
|
1339
|
-
link.connected = false;
|
|
1340
|
-
return Promise.resolve();
|
|
1341
|
-
});
|
|
1342
|
-
return link;
|
|
1343
|
-
};
|
|
1344
|
-
|
|
1345
|
-
/** requestMTU resolves a fresh Device snapshot, as react-native-ble-plx does. */
|
|
1346
|
-
const negotiatedSnapshot = (device: any, mtu: number) => {
|
|
1347
|
-
const negotiated = { ...device, mtu };
|
|
1348
|
-
device.requestMTU.mockImplementation(() => Promise.resolve(negotiated));
|
|
1349
|
-
return negotiated;
|
|
1350
|
-
};
|
|
1351
|
-
|
|
1352
|
-
test('negotiates the MTU after a bare connect and before service discovery, adopting the returned device', async () => {
|
|
1353
|
-
const { transport, uuid, device } = createHarness();
|
|
1354
|
-
reconnectingDevice(device);
|
|
1355
|
-
device.mtu = 23;
|
|
1356
|
-
const negotiated = negotiatedSnapshot(device, 247);
|
|
1357
|
-
|
|
1358
|
-
await expect(settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))).resolves.toEqual({
|
|
1359
|
-
uuid,
|
|
1360
|
-
protocolType: 'V2',
|
|
1361
|
-
});
|
|
1362
|
-
|
|
1363
|
-
expect(device.connect).toHaveBeenCalledTimes(1);
|
|
1364
|
-
expect(device.connect).toHaveBeenCalledWith({ timeout: expect.any(Number) });
|
|
1365
|
-
const resolveCharacteristics = (transport as any).resolveCharacteristics as jest.Mock;
|
|
1366
|
-
const [connectOrder] = device.connect.mock.invocationCallOrder;
|
|
1367
|
-
const [mtuOrder] = device.requestMTU.mock.invocationCallOrder;
|
|
1368
|
-
const [discoveryOrder] = resolveCharacteristics.mock.invocationCallOrder;
|
|
1369
|
-
expect(connectOrder).toBeLessThan(mtuOrder);
|
|
1370
|
-
expect(mtuOrder).toBeLessThan(discoveryOrder);
|
|
1371
|
-
const cached = (transport as any).getCachedTransport(uuid);
|
|
1372
|
-
expect(cached.device).toBe(negotiated);
|
|
1373
|
-
expect(cached.mtuSize).toBe(247);
|
|
1374
|
-
await settle(transport.release(uuid, true));
|
|
1375
|
-
});
|
|
1376
|
-
|
|
1377
|
-
test.each([
|
|
1378
|
-
{ expectedProtocol: 'V2' as const, label: 'an expected Protocol V2 device' },
|
|
1379
|
-
{ expectedProtocol: 'V1' as const, label: 'an expected Protocol V1 device' },
|
|
1380
|
-
{ expectedProtocol: undefined, label: 'a device of unknown protocol' },
|
|
1381
|
-
])('drops a link that stays at the default MTU for $label', async ({ expectedProtocol }) => {
|
|
1382
|
-
const { transport, uuid, device } = createHarness();
|
|
1383
|
-
device.mtu = 23;
|
|
1384
|
-
|
|
1385
|
-
await expect(settle(transport.acquire({ uuid, expectedProtocol }))).rejects.toMatchObject({
|
|
1386
|
-
errorCode: HardwareErrorCode.BleConnectedError,
|
|
1387
|
-
});
|
|
1388
|
-
expect((transport as any).resolveCharacteristics).not.toHaveBeenCalled();
|
|
1389
|
-
expect(device.cancelConnection).toHaveBeenCalled();
|
|
1390
|
-
});
|
|
1391
|
-
|
|
1392
|
-
test.each([
|
|
1393
|
-
['never completes', () => new Promise(() => {})],
|
|
1394
|
-
['completes at the default MTU', undefined],
|
|
1395
|
-
['is rejected', () => Promise.reject(new Error('MTU request failed'))],
|
|
1396
|
-
])(
|
|
1397
|
-
'a second consecutive MTU failure that %s trips the wedged-link guard',
|
|
1398
|
-
async (_label, requestMTU) => {
|
|
1399
|
-
const { transport, uuid, device, bleManager } = createHarness();
|
|
1400
|
-
device.mtu = 23;
|
|
1401
|
-
if (requestMTU) device.requestMTU.mockImplementation(requestMTU);
|
|
1402
|
-
|
|
1403
|
-
await expect(
|
|
1404
|
-
settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))
|
|
1405
|
-
).rejects.toMatchObject({
|
|
1406
|
-
errorCode: HardwareErrorCode.BleConnectedError,
|
|
1407
|
-
});
|
|
1408
|
-
await expect(
|
|
1409
|
-
settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))
|
|
1410
|
-
).rejects.toMatchObject({
|
|
1411
|
-
errorCode: HardwareErrorCode.PollingTimeout,
|
|
1412
|
-
message: expect.stringContaining(BLE_SETUP_WEDGED_MESSAGE),
|
|
1413
|
-
});
|
|
1414
|
-
expect(bleManager.destroy).toHaveBeenCalled();
|
|
1415
|
-
}
|
|
1416
|
-
);
|
|
1417
|
-
|
|
1418
|
-
test('stop() during the link-drop wait releases it promptly', async () => {
|
|
1419
|
-
const { transport, uuid, device } = createHarness();
|
|
1420
|
-
device.mtu = 23;
|
|
1421
|
-
device.requestMTU.mockImplementation(() => new Promise(() => {}));
|
|
1422
|
-
|
|
1423
|
-
const acquiring = transport.acquire({ uuid, expectedProtocol: 'V2' }).catch(error => error);
|
|
1424
|
-
// The link-drop teardown cancels the device connection before the wait starts.
|
|
1425
|
-
await advanceUntil(() => device.cancelConnection.mock.calls.length > 0);
|
|
1426
|
-
await advanceUntil(() => false, 1000).catch(() => undefined);
|
|
1427
|
-
await settle(transport.stop(), 1000);
|
|
1428
|
-
await expect(settle(acquiring, 1000)).resolves.toBeInstanceOf(Error);
|
|
1429
|
-
});
|
|
1430
|
-
|
|
1431
|
-
test.each([
|
|
1432
|
-
{
|
|
1433
|
-
label: 'a missing OneKey service',
|
|
1434
|
-
error: () => ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound),
|
|
1435
|
-
},
|
|
1436
|
-
{
|
|
1437
|
-
label: 'a missing characteristic',
|
|
1438
|
-
error: () => ERRORS.TypedError(HardwareErrorCode.BleCharacteristicNotFound),
|
|
1439
|
-
},
|
|
1440
|
-
{
|
|
1441
|
-
label: 'a mis-typed characteristic',
|
|
1442
|
-
error: () =>
|
|
1443
|
-
ERRORS.TypedError('BLECharacteristicNotWritable: write characteristic not writable'),
|
|
1444
|
-
},
|
|
1445
|
-
])(
|
|
1446
|
-
'marks the endpoint on $label and refreshes the GATT table on the next connect, before the MTU exchange',
|
|
1447
|
-
async ({ error }) => {
|
|
1448
|
-
const { transport, uuid, device } = createHarness();
|
|
1449
|
-
const link = reconnectingDevice(device);
|
|
1450
|
-
device.mtu = 23;
|
|
1451
|
-
negotiatedSnapshot(device, 247);
|
|
1452
|
-
const resolveCharacteristics = (transport as any).resolveCharacteristics as jest.Mock;
|
|
1453
|
-
resolveCharacteristics.mockImplementationOnce(() => Promise.reject(error()));
|
|
1454
|
-
|
|
1455
|
-
await expect(
|
|
1456
|
-
settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))
|
|
1457
|
-
).rejects.toBeDefined();
|
|
1458
|
-
expect(link.connected).toBe(true);
|
|
1459
|
-
|
|
1460
|
-
await expect(settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))).resolves.toEqual({
|
|
1461
|
-
uuid,
|
|
1462
|
-
protocolType: 'V2',
|
|
1463
|
-
});
|
|
1464
|
-
expect(device.connect).toHaveBeenLastCalledWith({
|
|
1465
|
-
timeout: expect.any(Number),
|
|
1466
|
-
refreshGatt: 'OnConnected',
|
|
1467
|
-
});
|
|
1468
|
-
// The stale table is only refreshed through a connect, so the live link is dropped first.
|
|
1469
|
-
const connectOrders: number[] = device.connect.mock.invocationCallOrder;
|
|
1470
|
-
expect(device.cancelConnection.mock.invocationCallOrder[0]).toBeLessThan(
|
|
1471
|
-
connectOrders[connectOrders.length - 1]
|
|
1472
|
-
);
|
|
1473
|
-
const discoveryOrders = resolveCharacteristics.mock.invocationCallOrder;
|
|
1474
|
-
const mtuOrders: number[] = device.requestMTU.mock.invocationCallOrder;
|
|
1475
|
-
expect(discoveryOrders[discoveryOrders.length - 1]).toBeLessThan(
|
|
1476
|
-
mtuOrders[mtuOrders.length - 1]
|
|
1477
|
-
);
|
|
1478
|
-
|
|
1479
|
-
// The refresh is spent once the table resolved through a refreshed connect.
|
|
1480
|
-
await settle(transport.release(uuid, true));
|
|
1481
|
-
link.connected = false;
|
|
1482
|
-
await settle(transport.acquire({ uuid, expectedProtocol: 'V2' }));
|
|
1483
|
-
expect(device.connect).toHaveBeenLastCalledWith({ timeout: expect.any(Number) });
|
|
1484
|
-
await settle(transport.release(uuid, true));
|
|
1485
|
-
}
|
|
1486
|
-
);
|
|
1487
|
-
|
|
1488
|
-
test('keeps the refresh marker when the refresh connect fell back without refreshGatt', async () => {
|
|
1489
|
-
const { transport, uuid, device } = createHarness();
|
|
1490
|
-
const link = reconnectingDevice(device);
|
|
1491
|
-
device.mtu = 23;
|
|
1492
|
-
negotiatedSnapshot(device, 247);
|
|
1493
|
-
const resolveCharacteristics = (transport as any).resolveCharacteristics as jest.Mock;
|
|
1494
|
-
resolveCharacteristics.mockImplementationOnce(() =>
|
|
1495
|
-
Promise.reject(ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound))
|
|
1496
|
-
);
|
|
1497
|
-
await expect(
|
|
1498
|
-
settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))
|
|
1499
|
-
).rejects.toBeDefined();
|
|
1500
|
-
|
|
1501
|
-
// The fallback connect carries no refreshGatt.
|
|
1502
|
-
const cancelled = Object.assign(new Error('Operation was cancelled'), { errorCode: 2 });
|
|
1503
|
-
device.connect.mockImplementationOnce(() => Promise.reject(cancelled));
|
|
1504
|
-
await expect(settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))).resolves.toEqual({
|
|
1505
|
-
uuid,
|
|
1506
|
-
protocolType: 'V2',
|
|
1507
|
-
});
|
|
1508
|
-
|
|
1509
|
-
await settle(transport.release(uuid, true));
|
|
1510
|
-
link.connected = false;
|
|
1511
|
-
await settle(transport.acquire({ uuid, expectedProtocol: 'V2' }));
|
|
1512
|
-
expect(device.connect).toHaveBeenLastCalledWith({
|
|
1513
|
-
timeout: expect.any(Number),
|
|
1514
|
-
refreshGatt: 'OnConnected',
|
|
1515
|
-
});
|
|
1516
|
-
await settle(transport.release(uuid, true));
|
|
1517
|
-
});
|
|
1518
|
-
|
|
1519
|
-
test('does not drop a link it has just connected with refreshGatt', async () => {
|
|
1520
|
-
const { transport, uuid, device, bleManager } = createHarness();
|
|
1521
|
-
bleManager.devices.mockResolvedValue([]);
|
|
1522
|
-
const connectToDevice = jest.fn(() => Promise.resolve(device));
|
|
1523
|
-
Object.assign(bleManager, { connectToDevice });
|
|
1524
|
-
(transport as any).androidGattCacheRefreshes.add(uuid);
|
|
1525
|
-
|
|
1526
|
-
await expect(settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))).resolves.toEqual({
|
|
1527
|
-
uuid,
|
|
1528
|
-
protocolType: 'V2',
|
|
1529
|
-
});
|
|
1530
|
-
expect(connectToDevice).toHaveBeenCalledTimes(1);
|
|
1531
|
-
expect(connectToDevice).toHaveBeenCalledWith(uuid, {
|
|
1532
|
-
timeout: expect.any(Number),
|
|
1533
|
-
refreshGatt: 'OnConnected',
|
|
1534
|
-
});
|
|
1535
|
-
expect(device.cancelConnection).not.toHaveBeenCalled();
|
|
1536
|
-
expect(device.connect).not.toHaveBeenCalled();
|
|
1537
|
-
await settle(transport.release(uuid, true));
|
|
1538
|
-
});
|
|
1539
|
-
|
|
1540
|
-
test('still refreshes after a connect-by-id refresh fell back without refreshGatt', async () => {
|
|
1541
|
-
const { transport, uuid, device, bleManager } = createHarness();
|
|
1542
|
-
const link = reconnectingDevice(device);
|
|
1543
|
-
bleManager.devices.mockResolvedValue([]);
|
|
1544
|
-
const cancelled = Object.assign(new Error('Operation was cancelled'), { errorCode: 2 });
|
|
1545
|
-
const connectToDevice = jest
|
|
1546
|
-
.fn()
|
|
1547
|
-
.mockImplementationOnce(() => Promise.reject(cancelled))
|
|
1548
|
-
.mockImplementation(() => {
|
|
1549
|
-
link.connected = true;
|
|
1550
|
-
return Promise.resolve(device);
|
|
1551
|
-
});
|
|
1552
|
-
Object.assign(bleManager, { connectToDevice });
|
|
1553
|
-
(transport as any).androidGattCacheRefreshes.add(uuid);
|
|
1554
|
-
|
|
1555
|
-
await expect(settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))).resolves.toEqual({
|
|
1556
|
-
uuid,
|
|
1557
|
-
protocolType: 'V2',
|
|
1558
|
-
});
|
|
1559
|
-
expect(connectToDevice).toHaveBeenLastCalledWith(uuid, { timeout: expect.any(Number) });
|
|
1560
|
-
expect(device.connect).toHaveBeenLastCalledWith({
|
|
1561
|
-
timeout: expect.any(Number),
|
|
1562
|
-
refreshGatt: 'OnConnected',
|
|
1563
|
-
});
|
|
1564
|
-
expect((transport as any).androidGattCacheRefreshes.has(uuid)).toBe(false);
|
|
1565
|
-
await settle(transport.release(uuid, true));
|
|
1566
|
-
});
|
|
1567
|
-
|
|
1568
|
-
test.each([
|
|
1569
|
-
'Cannot write client characteristic config descriptor',
|
|
1570
|
-
'Cannot find client characteristic config descriptor',
|
|
1571
|
-
'The handle is invalid',
|
|
1572
|
-
'Writing is not permitted',
|
|
1573
|
-
])('arms a GATT refresh from the notify failure "%s"', async reason => {
|
|
1574
|
-
const harness = createHarness();
|
|
1575
|
-
const { transport, uuid, device } = harness;
|
|
1576
|
-
reconnectingDevice(device);
|
|
1577
|
-
device.mtu = 23;
|
|
1578
|
-
negotiatedSnapshot(device, 247);
|
|
1579
|
-
|
|
1580
|
-
await settle(transport.acquire({ uuid, expectedProtocol: 'V2' }));
|
|
1581
|
-
harness.emitMonitorError(Object.assign(new Error('notify failed'), { reason }));
|
|
1582
|
-
|
|
1583
|
-
await expect(settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))).resolves.toEqual({
|
|
1584
|
-
uuid,
|
|
1585
|
-
protocolType: 'V2',
|
|
1586
|
-
});
|
|
1587
|
-
expect(device.connect).toHaveBeenLastCalledWith({
|
|
1588
|
-
timeout: expect.any(Number),
|
|
1589
|
-
refreshGatt: 'OnConnected',
|
|
1590
|
-
});
|
|
1591
|
-
await settle(transport.release(uuid, true));
|
|
1592
|
-
});
|
|
1593
|
-
|
|
1594
|
-
test('arms a GATT refresh from a notify failure while notifications are being enabled', async () => {
|
|
1595
|
-
const { transport, uuid, device } = createHarness({
|
|
1596
|
-
monitorError: Object.assign(new Error('notify failed'), {
|
|
1597
|
-
reason: 'Cannot write client characteristic config descriptor',
|
|
1598
|
-
}),
|
|
1599
|
-
});
|
|
1600
|
-
const link = reconnectingDevice(device);
|
|
1601
|
-
device.mtu = 23;
|
|
1602
|
-
negotiatedSnapshot(device, 247);
|
|
1603
|
-
|
|
1604
|
-
await settle(transport.acquire({ uuid, expectedProtocol: 'V2' }).catch(error => error));
|
|
1605
|
-
await settle(transport.release(uuid, true));
|
|
1606
|
-
link.connected = false;
|
|
1607
|
-
await settle(transport.acquire({ uuid, expectedProtocol: 'V2' }).catch(error => error));
|
|
1608
|
-
expect(device.connect).toHaveBeenLastCalledWith({
|
|
1609
|
-
timeout: expect.any(Number),
|
|
1610
|
-
refreshGatt: 'OnConnected',
|
|
1611
|
-
});
|
|
1612
|
-
await settle(transport.release(uuid, true));
|
|
1613
|
-
});
|
|
1614
|
-
|
|
1615
|
-
test('keeps the GATT refresh for a firmware-install reconnect', async () => {
|
|
1616
|
-
const { transport, uuid, device } = createHarness();
|
|
1617
|
-
(transport as any).sessionProtocols.set(uuid, 'V2');
|
|
1618
|
-
reconnectingDevice(device);
|
|
1619
|
-
|
|
1620
|
-
await expect(
|
|
1621
|
-
settle(transport.acquire({ uuid, expectedProtocol: 'V2', skipProtocolProbe: true }))
|
|
1622
|
-
).resolves.toEqual({ uuid, protocolType: 'V2' });
|
|
1623
|
-
expect(device.connect).toHaveBeenCalledWith({
|
|
1624
|
-
timeout: expect.any(Number),
|
|
1625
|
-
refreshGatt: 'OnConnected',
|
|
1626
|
-
});
|
|
1627
|
-
await settle(transport.release(uuid, true));
|
|
1628
|
-
});
|
|
1629
|
-
});
|
|
1630
|
-
|
|
1631
|
-
test('spends one Initialize wake on the detection after a fully silent one', async () => {
|
|
1632
|
-
setPlatformOS('android');
|
|
1633
|
-
const { transport, uuid } = createHarness();
|
|
1634
|
-
const probes = transport as any;
|
|
1635
|
-
jest.spyOn(probes, 'probeProtocolV1').mockResolvedValue(false);
|
|
1636
|
-
jest.spyOn(probes, 'probeProtocolV2').mockResolvedValue(false);
|
|
1637
|
-
const callProtocolV1 = jest.spyOn(probes, 'callProtocolV1').mockResolvedValue({});
|
|
1638
|
-
|
|
1639
|
-
// Nothing has gone silent yet, so the device keeps whatever session it has.
|
|
1640
|
-
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1641
|
-
expect(callProtocolV1).not.toHaveBeenCalled();
|
|
1642
|
-
|
|
1643
|
-
// The previous detection answered on no protocol, which is what a sleeping Classic
|
|
1644
|
-
// looks like, so this one wakes on the fresh link before probing.
|
|
1645
|
-
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1646
|
-
expect(callProtocolV1).toHaveBeenCalledTimes(1);
|
|
1647
|
-
expect(callProtocolV1.mock.calls[0]?.[1]).toBe('Initialize');
|
|
1648
|
-
|
|
1649
|
-
// Still silent: the wake is not repeated, so a device that is simply away is not
|
|
1650
|
-
// pushed into a fresh wallet session on every poll.
|
|
1651
|
-
callProtocolV1.mockClear();
|
|
1652
|
-
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1653
|
-
expect(callProtocolV1).not.toHaveBeenCalled();
|
|
1654
1131
|
});
|
|
1655
1132
|
|
|
1656
|
-
test('does not send the Initialize wake before a V2-first probe', async () => {
|
|
1657
|
-
setPlatformOS('android');
|
|
1658
|
-
const { transport, uuid } = createHarness();
|
|
1659
|
-
const probes = transport as any;
|
|
1660
|
-
jest.spyOn(probes, 'probeProtocolV1').mockResolvedValue(false);
|
|
1661
|
-
jest.spyOn(probes, 'probeProtocolV2').mockResolvedValue(false);
|
|
1662
|
-
const callProtocolV1 = jest.spyOn(probes, 'callProtocolV1').mockResolvedValue({});
|
|
1663
|
-
|
|
1664
|
-
await expect(transport.acquire({ uuid, protocolHint: 'V2' })).rejects.toBeDefined();
|
|
1665
|
-
// Silent now, but the next detection probes V2 first: a late V1 reply would land on
|
|
1666
|
-
// the V2 probe, so no Initialize is sent.
|
|
1667
|
-
await expect(transport.acquire({ uuid, protocolHint: 'V2' })).rejects.toBeDefined();
|
|
1668
|
-
expect(callProtocolV1).not.toHaveBeenCalled();
|
|
1669
|
-
});
|
|
1670
|
-
|
|
1671
|
-
test('does not send the Initialize wake on iOS', async () => {
|
|
1672
|
-
const { transport, uuid } = createHarness();
|
|
1673
|
-
const probes = transport as any;
|
|
1674
|
-
jest.spyOn(probes, 'probeProtocolV1').mockResolvedValue(false);
|
|
1675
|
-
jest.spyOn(probes, 'probeProtocolV2').mockResolvedValue(false);
|
|
1676
|
-
const callProtocolV1 = jest.spyOn(probes, 'callProtocolV1').mockResolvedValue({});
|
|
1677
|
-
|
|
1678
|
-
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1679
|
-
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1680
|
-
expect(callProtocolV1).not.toHaveBeenCalled();
|
|
1681
|
-
});
|
|
1682
|
-
|
|
1683
|
-
test('an unanswered Initialize wake settles the acquire and frees the lifecycle lock', async () => {
|
|
1684
|
-
setPlatformOS('android');
|
|
1685
|
-
const harness = createHarness();
|
|
1686
|
-
const { transport, uuid } = harness;
|
|
1687
|
-
const probes = transport as any;
|
|
1688
|
-
harness.setShouldRespond(false);
|
|
1689
|
-
jest.spyOn(probes, 'probeProtocolV1').mockResolvedValue(false);
|
|
1690
|
-
jest.spyOn(probes, 'probeProtocolV2').mockResolvedValue(false);
|
|
1691
|
-
|
|
1692
|
-
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1693
|
-
await expect(transport.acquire({ uuid })).rejects.toMatchObject({
|
|
1694
|
-
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
1695
|
-
});
|
|
1696
|
-
await transport.disconnect(uuid);
|
|
1697
|
-
await transport.stop();
|
|
1698
|
-
}, 10_000);
|
|
1699
|
-
|
|
1700
1133
|
test('accepts a stable low MTU without the delayed refresh loop', async () => {
|
|
1701
1134
|
const { transport, uuid, device } = createHarness();
|
|
1702
1135
|
device.mtu = 185;
|
|
@@ -1705,7 +1138,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
1705
1138
|
uuid,
|
|
1706
1139
|
protocolType: 'V2',
|
|
1707
1140
|
});
|
|
1708
|
-
expect(device.requestMTU).
|
|
1141
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(1);
|
|
1709
1142
|
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(185);
|
|
1710
1143
|
await transport.release(uuid, true);
|
|
1711
1144
|
});
|
|
@@ -1718,7 +1151,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
1718
1151
|
uuid,
|
|
1719
1152
|
protocolType: 'V2',
|
|
1720
1153
|
});
|
|
1721
|
-
expect(device.requestMTU).toHaveBeenCalledTimes(
|
|
1154
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(3);
|
|
1722
1155
|
expect((transport as any).getCachedTransport(uuid).mtuSize).toBeUndefined();
|
|
1723
1156
|
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalled();
|
|
1724
1157
|
await transport.release(uuid, true);
|
|
@@ -1736,7 +1169,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
1736
1169
|
});
|
|
1737
1170
|
|
|
1738
1171
|
await expect(transport.call(uuid, 'FileWrite', {})).resolves.toBeDefined();
|
|
1739
|
-
expect(device.requestMTU).toHaveBeenCalledTimes(
|
|
1172
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(4);
|
|
1740
1173
|
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(
|
|
1741
1174
|
writesBeforeFileWrite + 1
|
|
1742
1175
|
);
|
|
@@ -1753,7 +1186,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
1753
1186
|
await expect(transport.call(uuid, 'FileWrite', {})).rejects.toMatchObject({
|
|
1754
1187
|
errorCode: HardwareErrorCode.BleConnectedError,
|
|
1755
1188
|
});
|
|
1756
|
-
expect(device.requestMTU).toHaveBeenCalledTimes(
|
|
1189
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(4);
|
|
1757
1190
|
expect(writeCharacteristic.writeWithoutResponse).toHaveBeenCalledTimes(writesBeforeFileWrite);
|
|
1758
1191
|
await transport.release(uuid, true);
|
|
1759
1192
|
});
|
|
@@ -2023,17 +1456,9 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
2023
1456
|
test('uses Android 517 MTU and high connection priority during Protocol V2 high-volume calls', async () => {
|
|
2024
1457
|
setPlatformOS('android');
|
|
2025
1458
|
const { transport, uuid, device } = createHarness();
|
|
2026
|
-
device.mtu = 23;
|
|
2027
|
-
device.requestMTU.mockImplementationOnce(() => {
|
|
2028
|
-
device.mtu = 517;
|
|
2029
|
-
return Promise.resolve(device);
|
|
2030
|
-
});
|
|
2031
1459
|
|
|
2032
1460
|
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
2033
|
-
expect(device.requestMTU).toHaveBeenCalledWith(
|
|
2034
|
-
517,
|
|
2035
|
-
expect.stringContaining(`${uuid}:mtu:connected:0:`)
|
|
2036
|
-
);
|
|
1461
|
+
expect(device.requestMTU).toHaveBeenCalledWith(517);
|
|
2037
1462
|
|
|
2038
1463
|
await transport.call(uuid, 'FileWrite', {});
|
|
2039
1464
|
await transport.call(uuid, 'FileWrite', {});
|