@onekeyfe/hd-transport-react-native 1.2.2-alpha.115 → 1.2.2-alpha.117
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 +24 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +181 -71
- package/package.json +5 -5
- package/src/__tests__/connectTimeout.test.ts +84 -0
- package/src/__tests__/protocolV2Link.test.ts +413 -30
- package/src/index.ts +261 -137
|
@@ -6,6 +6,8 @@ import { ERRORS, HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
|
|
|
6
6
|
|
|
7
7
|
import { onDeviceBondState } from '../BleManager';
|
|
8
8
|
import ReactNativeBleTransport, {
|
|
9
|
+
ANDROID_LINK_DROP_QUIET_MS,
|
|
10
|
+
ANDROID_MTU_EXCHANGE_TIMEOUT_MS,
|
|
9
11
|
BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD,
|
|
10
12
|
BLE_CONNECT_TIMEOUT_MS,
|
|
11
13
|
BLE_GATT_SETUP_TIMEOUT_MS,
|
|
@@ -608,6 +610,19 @@ describe('BLE connect timeout', () => {
|
|
|
608
610
|
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(UUID);
|
|
609
611
|
});
|
|
610
612
|
|
|
613
|
+
test('a native GATT timeout abandons the native connection', async () => {
|
|
614
|
+
const { transport, device, bleManager } = createHarness(() => Promise.resolve());
|
|
615
|
+
const nativeTimeout = Object.assign(new Error('Operation timed out'), {
|
|
616
|
+
errorCode: BleErrorCode.OperationTimedOut,
|
|
617
|
+
});
|
|
618
|
+
device.discoverAllServicesAndCharacteristics.mockRejectedValueOnce(nativeTimeout);
|
|
619
|
+
|
|
620
|
+
await expect((transport as any).resolveCharacteristicsWithTimeout(UUID, device)).rejects.toBe(
|
|
621
|
+
nativeTimeout
|
|
622
|
+
);
|
|
623
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(UUID);
|
|
624
|
+
});
|
|
625
|
+
|
|
611
626
|
test('a successful GATT retry clears the timeout budget before an abandoned call settles', async () => {
|
|
612
627
|
const { transport, device, bleManager } = createHarness(() => Promise.resolve());
|
|
613
628
|
let resolveAbandonedDiscovery: (() => void) | undefined;
|
|
@@ -640,4 +655,73 @@ describe('BLE connect timeout', () => {
|
|
|
640
655
|
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledTimes(1);
|
|
641
656
|
expect((transport as any).connectionSetupTimeoutCounts.has(UUID)).toBe(false);
|
|
642
657
|
});
|
|
658
|
+
|
|
659
|
+
test('holds an unusable link past the idle timer', async () => {
|
|
660
|
+
Object.assign(Platform, { OS: 'android' });
|
|
661
|
+
const { transport, device, bleManager } = createHarness(() => Promise.resolve(device));
|
|
662
|
+
device.isConnected.mockResolvedValue(true);
|
|
663
|
+
Object.assign(device, { mtu: 23, requestMTU: jest.fn(() => new Promise(() => {})) });
|
|
664
|
+
let settled: Error | undefined;
|
|
665
|
+
const acquire = transport.acquire({ uuid: UUID, expectedProtocol: 'V1' }).catch(error => {
|
|
666
|
+
settled = error;
|
|
667
|
+
});
|
|
668
|
+
await flush();
|
|
669
|
+
await flush();
|
|
670
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(1);
|
|
671
|
+
|
|
672
|
+
jest.advanceTimersByTime(ANDROID_MTU_EXCHANGE_TIMEOUT_MS);
|
|
673
|
+
await flush();
|
|
674
|
+
await flush();
|
|
675
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(UUID);
|
|
676
|
+
|
|
677
|
+
jest.advanceTimersByTime(4000);
|
|
678
|
+
await flush();
|
|
679
|
+
await flush();
|
|
680
|
+
expect(settled).toBeUndefined();
|
|
681
|
+
|
|
682
|
+
await advanceUntil(() => !!settled, ANDROID_LINK_DROP_QUIET_MS);
|
|
683
|
+
await acquire;
|
|
684
|
+
expect(settled).toMatchObject({ errorCode: HardwareErrorCode.BleConnectedError });
|
|
685
|
+
expect(device.discoverAllServicesAndCharacteristics).not.toHaveBeenCalled();
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test('a slow but successful MTU exchange completes inside the bound', async () => {
|
|
689
|
+
Object.assign(Platform, { OS: 'android' });
|
|
690
|
+
const { transport, device } = createHarness(() => Promise.resolve(device));
|
|
691
|
+
device.isConnected.mockResolvedValue(true);
|
|
692
|
+
const negotiated = { ...device, mtu: 247 };
|
|
693
|
+
Object.assign(device, {
|
|
694
|
+
mtu: 23,
|
|
695
|
+
// A cold cache makes the stack discover first; the exchange then completes late.
|
|
696
|
+
requestMTU: jest.fn(
|
|
697
|
+
() =>
|
|
698
|
+
new Promise(resolve => {
|
|
699
|
+
setTimeout(() => resolve(negotiated), 10_000);
|
|
700
|
+
})
|
|
701
|
+
),
|
|
702
|
+
});
|
|
703
|
+
const [, notifyCharacteristic] = await device.characteristicsForService();
|
|
704
|
+
Object.assign(notifyCharacteristic, { monitor: jest.fn(() => ({ remove: jest.fn() })) });
|
|
705
|
+
let result: unknown;
|
|
706
|
+
let failure: Error | undefined;
|
|
707
|
+
const acquire = transport.acquire({ uuid: UUID, expectedProtocol: 'V1' }).then(
|
|
708
|
+
value => {
|
|
709
|
+
result = value;
|
|
710
|
+
},
|
|
711
|
+
error => {
|
|
712
|
+
failure = error;
|
|
713
|
+
}
|
|
714
|
+
);
|
|
715
|
+
await flush();
|
|
716
|
+
await flush();
|
|
717
|
+
expect(device.discoverAllServicesAndCharacteristics).not.toHaveBeenCalled();
|
|
718
|
+
|
|
719
|
+
jest.advanceTimersByTime(10_000);
|
|
720
|
+
await advanceUntil(() => result !== undefined || failure !== undefined, 5000);
|
|
721
|
+
await acquire;
|
|
722
|
+
expect(failure).toBeUndefined();
|
|
723
|
+
expect(result).toEqual({ uuid: UUID, protocolType: 'V1' });
|
|
724
|
+
expect(device.discoverAllServicesAndCharacteristics).toHaveBeenCalledTimes(1);
|
|
725
|
+
await transport.release(UUID, true);
|
|
726
|
+
});
|
|
643
727
|
});
|
|
@@ -8,6 +8,7 @@ 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,
|
|
11
12
|
BLE_WRITE_PACKET_TIMEOUT_MS,
|
|
12
13
|
configureProtocolV2BleTuning,
|
|
13
14
|
getFirmwareUploadWriteRetryType,
|
|
@@ -169,11 +170,13 @@ const createHarness = ({
|
|
|
169
170
|
serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
|
|
170
171
|
isConnected: jest.fn(() => Promise.resolve(true)),
|
|
171
172
|
cancelConnection: jest.fn(() => Promise.resolve()),
|
|
173
|
+
connect: jest.fn(),
|
|
172
174
|
onDisconnected: jest.fn(callback => {
|
|
173
175
|
disconnectCallback = callback;
|
|
174
176
|
return { remove: jest.fn() };
|
|
175
177
|
}),
|
|
176
178
|
} as any;
|
|
179
|
+
device.connect.mockResolvedValue(device);
|
|
177
180
|
device.requestMTU = jest.fn(() => Promise.resolve(device));
|
|
178
181
|
device.requestConnectionPriority = jest.fn(() => Promise.resolve(device));
|
|
179
182
|
const bleManager = {
|
|
@@ -1126,63 +1129,414 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
1126
1129
|
await transport.release(uuid, true);
|
|
1127
1130
|
});
|
|
1128
1131
|
|
|
1129
|
-
test('
|
|
1132
|
+
test('bounds a stalled MTU negotiation and continues protocol probing', async () => {
|
|
1130
1133
|
const { transport, uuid, device, bleManager } = createHarness();
|
|
1131
1134
|
device.mtu = 23;
|
|
1132
1135
|
device.requestMTU.mockImplementationOnce(() => new Promise(() => {}));
|
|
1133
|
-
const { resolveCharacteristics } = transport as any;
|
|
1134
1136
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
await expect(transport.acquire({ uuid })).rejects.toMatchObject({
|
|
1139
|
-
errorCode: HardwareErrorCode.BleConnectedError,
|
|
1137
|
+
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
1138
|
+
uuid,
|
|
1139
|
+
protocolType: 'V2',
|
|
1140
1140
|
});
|
|
1141
1141
|
const transactionId = device.requestMTU.mock.calls[0]?.[1];
|
|
1142
1142
|
expect(transactionId).toEqual(expect.stringContaining(`${uuid}:mtu:connected:0:`));
|
|
1143
1143
|
expect(bleManager.cancelTransaction).toHaveBeenCalledWith(transactionId);
|
|
1144
|
-
expect(
|
|
1145
|
-
expect(
|
|
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);
|
|
1146
1152
|
}, 10_000);
|
|
1147
1153
|
|
|
1148
|
-
test('
|
|
1154
|
+
test('does not request MTU again after connect falls back without requestMTU', async () => {
|
|
1155
|
+
const { BleError: BleErrorMock, BleErrorCode } = jest.requireMock('react-native-ble-plx');
|
|
1149
1156
|
const { transport, uuid, device } = createHarness();
|
|
1150
1157
|
device.mtu = 23;
|
|
1151
|
-
|
|
1152
|
-
device.
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
return Promise.resolve(device);
|
|
1160
|
-
});
|
|
1158
|
+
device.isConnected.mockResolvedValueOnce(false).mockResolvedValue(true);
|
|
1159
|
+
device.connect
|
|
1160
|
+
.mockRejectedValueOnce(
|
|
1161
|
+
Object.assign(new BleErrorMock('Operation was cancelled'), {
|
|
1162
|
+
errorCode: BleErrorCode.OperationCancelled,
|
|
1163
|
+
})
|
|
1164
|
+
)
|
|
1165
|
+
.mockResolvedValue(device);
|
|
1161
1166
|
|
|
1162
1167
|
await expect(transport.acquire({ uuid })).resolves.toEqual({
|
|
1163
1168
|
uuid,
|
|
1164
1169
|
protocolType: 'V2',
|
|
1165
1170
|
});
|
|
1166
|
-
|
|
1167
|
-
// ble-plx applies the connect timeout to establishConnection -> refreshGatt ->
|
|
1168
|
-
// requestMtu as one chain, so the fallback means this peripheral did not finish the
|
|
1169
|
-
// MTU exchange. Asking again would only park a second abandoned exchange.
|
|
1170
1171
|
expect(device.connect).toHaveBeenCalledTimes(2);
|
|
1172
|
+
expect(device.connect.mock.calls[0][0]).toEqual(expect.objectContaining({ requestMTU: 247 }));
|
|
1173
|
+
expect(device.connect.mock.calls[1][0]).not.toHaveProperty('requestMTU');
|
|
1171
1174
|
expect(device.requestMTU).not.toHaveBeenCalled();
|
|
1172
1175
|
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(23);
|
|
1173
1176
|
await transport.release(uuid, true);
|
|
1174
1177
|
});
|
|
1175
1178
|
|
|
1176
|
-
test('
|
|
1177
|
-
const { transport, uuid,
|
|
1178
|
-
|
|
1179
|
-
(transport as any).resolveCharacteristics = jest.fn(() => Promise.reject(cancelled));
|
|
1179
|
+
test('keeps the iOS connect chain unchanged', async () => {
|
|
1180
|
+
const { transport, uuid, device } = createHarness();
|
|
1181
|
+
device.isConnected.mockResolvedValueOnce(false);
|
|
1180
1182
|
|
|
1181
|
-
await expect(transport.acquire({ uuid })).
|
|
1182
|
-
|
|
1183
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).resolves.toEqual({
|
|
1184
|
+
uuid,
|
|
1185
|
+
protocolType: 'V2',
|
|
1186
|
+
});
|
|
1187
|
+
expect(device.connect).toHaveBeenCalledWith({
|
|
1188
|
+
requestMTU: 247,
|
|
1189
|
+
timeout: expect.any(Number),
|
|
1190
|
+
refreshGatt: 'OnConnected',
|
|
1191
|
+
});
|
|
1192
|
+
expect(device.requestMTU).not.toHaveBeenCalled();
|
|
1193
|
+
await transport.release(uuid, true);
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
describe('Android MTU before service discovery', () => {
|
|
1197
|
+
beforeEach(() => {
|
|
1198
|
+
setPlatformOS('android');
|
|
1199
|
+
jest.useFakeTimers({
|
|
1200
|
+
doNotFake: ['setImmediate', 'queueMicrotask', 'nextTick', 'performance'],
|
|
1201
|
+
});
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
afterEach(() => {
|
|
1205
|
+
jest.clearAllTimers();
|
|
1206
|
+
jest.useRealTimers();
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
const advanceUntil = async (condition: () => boolean, budgetMs = 60_000) => {
|
|
1210
|
+
for (let elapsed = 0; !condition(); elapsed += 50) {
|
|
1211
|
+
if (elapsed >= budgetMs) throw new Error(`fake time exhausted after ${budgetMs}ms`);
|
|
1212
|
+
jest.advanceTimersByTime(50);
|
|
1213
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1214
|
+
await new Promise(resolve => {
|
|
1215
|
+
setImmediate(resolve);
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
const settle = async <T>(promise: Promise<T>, budgetMs?: number): Promise<T> => {
|
|
1221
|
+
let done = false;
|
|
1222
|
+
promise.then(
|
|
1223
|
+
() => {
|
|
1224
|
+
done = true;
|
|
1225
|
+
},
|
|
1226
|
+
() => {
|
|
1227
|
+
done = true;
|
|
1228
|
+
}
|
|
1229
|
+
);
|
|
1230
|
+
await advanceUntil(() => done, budgetMs);
|
|
1231
|
+
return promise;
|
|
1232
|
+
};
|
|
1233
|
+
|
|
1234
|
+
/**
|
|
1235
|
+
* A device whose LE link is only up between connect() and cancelConnection(), so the
|
|
1236
|
+
* transport has to reconnect after every drop instead of the test flipping the link.
|
|
1237
|
+
*/
|
|
1238
|
+
const reconnectingDevice = (device: any) => {
|
|
1239
|
+
const link = { connected: false };
|
|
1240
|
+
device.isConnected.mockImplementation(() => Promise.resolve(link.connected));
|
|
1241
|
+
device.connect = jest.fn(() => {
|
|
1242
|
+
link.connected = true;
|
|
1243
|
+
return Promise.resolve(device);
|
|
1244
|
+
});
|
|
1245
|
+
device.cancelConnection.mockImplementation(() => {
|
|
1246
|
+
link.connected = false;
|
|
1247
|
+
return Promise.resolve();
|
|
1248
|
+
});
|
|
1249
|
+
return link;
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
/** requestMTU resolves a fresh Device snapshot, as react-native-ble-plx does. */
|
|
1253
|
+
const negotiatedSnapshot = (device: any, mtu: number) => {
|
|
1254
|
+
const negotiated = { ...device, mtu };
|
|
1255
|
+
device.requestMTU.mockImplementation(() => Promise.resolve(negotiated));
|
|
1256
|
+
return negotiated;
|
|
1257
|
+
};
|
|
1258
|
+
|
|
1259
|
+
test('negotiates the MTU after a bare connect and before service discovery, adopting the returned device', async () => {
|
|
1260
|
+
const { transport, uuid, device } = createHarness();
|
|
1261
|
+
reconnectingDevice(device);
|
|
1262
|
+
device.mtu = 23;
|
|
1263
|
+
const negotiated = negotiatedSnapshot(device, 247);
|
|
1264
|
+
|
|
1265
|
+
await expect(settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))).resolves.toEqual({
|
|
1266
|
+
uuid,
|
|
1267
|
+
protocolType: 'V2',
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
expect(device.connect).toHaveBeenCalledTimes(1);
|
|
1271
|
+
expect(device.connect).toHaveBeenCalledWith({ timeout: expect.any(Number) });
|
|
1272
|
+
const resolveCharacteristics = (transport as any).resolveCharacteristics as jest.Mock;
|
|
1273
|
+
const [connectOrder] = device.connect.mock.invocationCallOrder;
|
|
1274
|
+
const [mtuOrder] = device.requestMTU.mock.invocationCallOrder;
|
|
1275
|
+
const [discoveryOrder] = resolveCharacteristics.mock.invocationCallOrder;
|
|
1276
|
+
expect(connectOrder).toBeLessThan(mtuOrder);
|
|
1277
|
+
expect(mtuOrder).toBeLessThan(discoveryOrder);
|
|
1278
|
+
const cached = (transport as any).getCachedTransport(uuid);
|
|
1279
|
+
expect(cached.device).toBe(negotiated);
|
|
1280
|
+
expect(cached.mtuSize).toBe(247);
|
|
1281
|
+
await settle(transport.release(uuid, true));
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
test.each([
|
|
1285
|
+
{ expectedProtocol: 'V2' as const, label: 'an expected Protocol V2 device' },
|
|
1286
|
+
{ expectedProtocol: 'V1' as const, label: 'an expected Protocol V1 device' },
|
|
1287
|
+
{ expectedProtocol: undefined, label: 'a device of unknown protocol' },
|
|
1288
|
+
])('drops a link that stays at the default MTU for $label', async ({ expectedProtocol }) => {
|
|
1289
|
+
const { transport, uuid, device } = createHarness();
|
|
1290
|
+
device.mtu = 23;
|
|
1291
|
+
|
|
1292
|
+
await expect(settle(transport.acquire({ uuid, expectedProtocol }))).rejects.toMatchObject({
|
|
1293
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
1294
|
+
});
|
|
1295
|
+
expect((transport as any).resolveCharacteristics).not.toHaveBeenCalled();
|
|
1296
|
+
expect(device.cancelConnection).toHaveBeenCalled();
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
test.each([
|
|
1300
|
+
['never completes', () => new Promise(() => {})],
|
|
1301
|
+
['completes at the default MTU', undefined],
|
|
1302
|
+
['is rejected', () => Promise.reject(new Error('MTU request failed'))],
|
|
1303
|
+
])(
|
|
1304
|
+
'a second consecutive MTU failure that %s trips the wedged-link guard',
|
|
1305
|
+
async (_label, requestMTU) => {
|
|
1306
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1307
|
+
device.mtu = 23;
|
|
1308
|
+
if (requestMTU) device.requestMTU.mockImplementation(requestMTU);
|
|
1309
|
+
|
|
1310
|
+
await expect(
|
|
1311
|
+
settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))
|
|
1312
|
+
).rejects.toMatchObject({
|
|
1313
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
1314
|
+
});
|
|
1315
|
+
await expect(
|
|
1316
|
+
settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))
|
|
1317
|
+
).rejects.toMatchObject({
|
|
1318
|
+
errorCode: HardwareErrorCode.PollingTimeout,
|
|
1319
|
+
message: expect.stringContaining(BLE_SETUP_WEDGED_MESSAGE),
|
|
1320
|
+
});
|
|
1321
|
+
expect(bleManager.destroy).toHaveBeenCalled();
|
|
1322
|
+
}
|
|
1323
|
+
);
|
|
1324
|
+
|
|
1325
|
+
test('stop() during the link-drop wait releases it promptly', async () => {
|
|
1326
|
+
const { transport, uuid, device } = createHarness();
|
|
1327
|
+
device.mtu = 23;
|
|
1328
|
+
device.requestMTU.mockImplementation(() => new Promise(() => {}));
|
|
1329
|
+
|
|
1330
|
+
const acquiring = transport.acquire({ uuid, expectedProtocol: 'V2' }).catch(error => error);
|
|
1331
|
+
// The link-drop teardown cancels the device connection before the wait starts.
|
|
1332
|
+
await advanceUntil(() => device.cancelConnection.mock.calls.length > 0);
|
|
1333
|
+
await advanceUntil(() => false, 1000).catch(() => undefined);
|
|
1334
|
+
await settle(transport.stop(), 1000);
|
|
1335
|
+
await expect(settle(acquiring, 1000)).resolves.toBeInstanceOf(Error);
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
test.each([
|
|
1339
|
+
{
|
|
1340
|
+
label: 'a missing OneKey service',
|
|
1341
|
+
error: () => ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound),
|
|
1342
|
+
},
|
|
1343
|
+
{
|
|
1344
|
+
label: 'a missing characteristic',
|
|
1345
|
+
error: () => ERRORS.TypedError(HardwareErrorCode.BleCharacteristicNotFound),
|
|
1346
|
+
},
|
|
1347
|
+
{
|
|
1348
|
+
label: 'a mis-typed characteristic',
|
|
1349
|
+
error: () =>
|
|
1350
|
+
ERRORS.TypedError('BLECharacteristicNotWritable: write characteristic not writable'),
|
|
1351
|
+
},
|
|
1352
|
+
])(
|
|
1353
|
+
'marks the endpoint on $label and refreshes the GATT table on the next connect, before the MTU exchange',
|
|
1354
|
+
async ({ error }) => {
|
|
1355
|
+
const { transport, uuid, device } = createHarness();
|
|
1356
|
+
const link = reconnectingDevice(device);
|
|
1357
|
+
device.mtu = 23;
|
|
1358
|
+
negotiatedSnapshot(device, 247);
|
|
1359
|
+
const resolveCharacteristics = (transport as any).resolveCharacteristics as jest.Mock;
|
|
1360
|
+
resolveCharacteristics.mockImplementationOnce(() => Promise.reject(error()));
|
|
1361
|
+
|
|
1362
|
+
await expect(
|
|
1363
|
+
settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))
|
|
1364
|
+
).rejects.toBeDefined();
|
|
1365
|
+
expect(link.connected).toBe(true);
|
|
1366
|
+
|
|
1367
|
+
await expect(settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))).resolves.toEqual({
|
|
1368
|
+
uuid,
|
|
1369
|
+
protocolType: 'V2',
|
|
1370
|
+
});
|
|
1371
|
+
expect(device.connect).toHaveBeenLastCalledWith({
|
|
1372
|
+
timeout: expect.any(Number),
|
|
1373
|
+
refreshGatt: 'OnConnected',
|
|
1374
|
+
});
|
|
1375
|
+
// The stale table is only refreshed through a connect, so the live link is dropped first.
|
|
1376
|
+
const connectOrders: number[] = device.connect.mock.invocationCallOrder;
|
|
1377
|
+
expect(device.cancelConnection.mock.invocationCallOrder[0]).toBeLessThan(
|
|
1378
|
+
connectOrders[connectOrders.length - 1]
|
|
1379
|
+
);
|
|
1380
|
+
const discoveryOrders = resolveCharacteristics.mock.invocationCallOrder;
|
|
1381
|
+
const mtuOrders: number[] = device.requestMTU.mock.invocationCallOrder;
|
|
1382
|
+
expect(discoveryOrders[discoveryOrders.length - 1]).toBeLessThan(
|
|
1383
|
+
mtuOrders[mtuOrders.length - 1]
|
|
1384
|
+
);
|
|
1385
|
+
|
|
1386
|
+
// The refresh is spent once the table resolved through a refreshed connect.
|
|
1387
|
+
await settle(transport.release(uuid, true));
|
|
1388
|
+
link.connected = false;
|
|
1389
|
+
await settle(transport.acquire({ uuid, expectedProtocol: 'V2' }));
|
|
1390
|
+
expect(device.connect).toHaveBeenLastCalledWith({ timeout: expect.any(Number) });
|
|
1391
|
+
await settle(transport.release(uuid, true));
|
|
1392
|
+
}
|
|
1393
|
+
);
|
|
1394
|
+
|
|
1395
|
+
test('keeps the refresh marker when the refresh connect fell back without refreshGatt', async () => {
|
|
1396
|
+
const { transport, uuid, device } = createHarness();
|
|
1397
|
+
const link = reconnectingDevice(device);
|
|
1398
|
+
device.mtu = 23;
|
|
1399
|
+
negotiatedSnapshot(device, 247);
|
|
1400
|
+
const resolveCharacteristics = (transport as any).resolveCharacteristics as jest.Mock;
|
|
1401
|
+
resolveCharacteristics.mockImplementationOnce(() =>
|
|
1402
|
+
Promise.reject(ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound))
|
|
1403
|
+
);
|
|
1404
|
+
await expect(
|
|
1405
|
+
settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))
|
|
1406
|
+
).rejects.toBeDefined();
|
|
1407
|
+
|
|
1408
|
+
// The fallback connect carries no refreshGatt.
|
|
1409
|
+
const cancelled = Object.assign(new Error('Operation was cancelled'), { errorCode: 2 });
|
|
1410
|
+
device.connect.mockImplementationOnce(() => Promise.reject(cancelled));
|
|
1411
|
+
await expect(settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))).resolves.toEqual({
|
|
1412
|
+
uuid,
|
|
1413
|
+
protocolType: 'V2',
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
await settle(transport.release(uuid, true));
|
|
1417
|
+
link.connected = false;
|
|
1418
|
+
await settle(transport.acquire({ uuid, expectedProtocol: 'V2' }));
|
|
1419
|
+
expect(device.connect).toHaveBeenLastCalledWith({
|
|
1420
|
+
timeout: expect.any(Number),
|
|
1421
|
+
refreshGatt: 'OnConnected',
|
|
1422
|
+
});
|
|
1423
|
+
await settle(transport.release(uuid, true));
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
test('does not drop a link it has just connected with refreshGatt', async () => {
|
|
1427
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1428
|
+
bleManager.devices.mockResolvedValue([]);
|
|
1429
|
+
const connectToDevice = jest.fn(() => Promise.resolve(device));
|
|
1430
|
+
Object.assign(bleManager, { connectToDevice });
|
|
1431
|
+
(transport as any).androidGattCacheRefreshes.add(uuid);
|
|
1432
|
+
|
|
1433
|
+
await expect(settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))).resolves.toEqual({
|
|
1434
|
+
uuid,
|
|
1435
|
+
protocolType: 'V2',
|
|
1436
|
+
});
|
|
1437
|
+
expect(connectToDevice).toHaveBeenCalledTimes(1);
|
|
1438
|
+
expect(connectToDevice).toHaveBeenCalledWith(uuid, {
|
|
1439
|
+
timeout: expect.any(Number),
|
|
1440
|
+
refreshGatt: 'OnConnected',
|
|
1441
|
+
});
|
|
1442
|
+
expect(device.cancelConnection).not.toHaveBeenCalled();
|
|
1443
|
+
expect(device.connect).not.toHaveBeenCalled();
|
|
1444
|
+
await settle(transport.release(uuid, true));
|
|
1445
|
+
});
|
|
1446
|
+
|
|
1447
|
+
test('still refreshes after a connect-by-id refresh fell back without refreshGatt', async () => {
|
|
1448
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1449
|
+
const link = reconnectingDevice(device);
|
|
1450
|
+
bleManager.devices.mockResolvedValue([]);
|
|
1451
|
+
const cancelled = Object.assign(new Error('Operation was cancelled'), { errorCode: 2 });
|
|
1452
|
+
const connectToDevice = jest
|
|
1453
|
+
.fn()
|
|
1454
|
+
.mockImplementationOnce(() => Promise.reject(cancelled))
|
|
1455
|
+
.mockImplementation(() => {
|
|
1456
|
+
link.connected = true;
|
|
1457
|
+
return Promise.resolve(device);
|
|
1458
|
+
});
|
|
1459
|
+
Object.assign(bleManager, { connectToDevice });
|
|
1460
|
+
(transport as any).androidGattCacheRefreshes.add(uuid);
|
|
1461
|
+
|
|
1462
|
+
await expect(settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))).resolves.toEqual({
|
|
1463
|
+
uuid,
|
|
1464
|
+
protocolType: 'V2',
|
|
1465
|
+
});
|
|
1466
|
+
expect(connectToDevice).toHaveBeenLastCalledWith(uuid, { timeout: expect.any(Number) });
|
|
1467
|
+
expect(device.connect).toHaveBeenLastCalledWith({
|
|
1468
|
+
timeout: expect.any(Number),
|
|
1469
|
+
refreshGatt: 'OnConnected',
|
|
1470
|
+
});
|
|
1471
|
+
expect((transport as any).androidGattCacheRefreshes.has(uuid)).toBe(false);
|
|
1472
|
+
await settle(transport.release(uuid, true));
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
test.each([
|
|
1476
|
+
'Cannot write client characteristic config descriptor',
|
|
1477
|
+
'Cannot find client characteristic config descriptor',
|
|
1478
|
+
'The handle is invalid',
|
|
1479
|
+
'Writing is not permitted',
|
|
1480
|
+
])('arms a GATT refresh from the notify failure "%s"', async reason => {
|
|
1481
|
+
const harness = createHarness();
|
|
1482
|
+
const { transport, uuid, device } = harness;
|
|
1483
|
+
reconnectingDevice(device);
|
|
1484
|
+
device.mtu = 23;
|
|
1485
|
+
negotiatedSnapshot(device, 247);
|
|
1486
|
+
|
|
1487
|
+
await settle(transport.acquire({ uuid, expectedProtocol: 'V2' }));
|
|
1488
|
+
harness.emitMonitorError(Object.assign(new Error('notify failed'), { reason }));
|
|
1489
|
+
|
|
1490
|
+
await expect(settle(transport.acquire({ uuid, expectedProtocol: 'V2' }))).resolves.toEqual({
|
|
1491
|
+
uuid,
|
|
1492
|
+
protocolType: 'V2',
|
|
1493
|
+
});
|
|
1494
|
+
expect(device.connect).toHaveBeenLastCalledWith({
|
|
1495
|
+
timeout: expect.any(Number),
|
|
1496
|
+
refreshGatt: 'OnConnected',
|
|
1497
|
+
});
|
|
1498
|
+
await settle(transport.release(uuid, true));
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
test('arms a GATT refresh from a notify failure while notifications are being enabled', async () => {
|
|
1502
|
+
const { transport, uuid, device } = createHarness({
|
|
1503
|
+
monitorError: Object.assign(new Error('notify failed'), {
|
|
1504
|
+
reason: 'Cannot write client characteristic config descriptor',
|
|
1505
|
+
}),
|
|
1506
|
+
});
|
|
1507
|
+
const link = reconnectingDevice(device);
|
|
1508
|
+
device.mtu = 23;
|
|
1509
|
+
negotiatedSnapshot(device, 247);
|
|
1510
|
+
|
|
1511
|
+
await settle(transport.acquire({ uuid, expectedProtocol: 'V2' }).catch(error => error));
|
|
1512
|
+
await settle(transport.release(uuid, true));
|
|
1513
|
+
link.connected = false;
|
|
1514
|
+
await settle(transport.acquire({ uuid, expectedProtocol: 'V2' }).catch(error => error));
|
|
1515
|
+
expect(device.connect).toHaveBeenLastCalledWith({
|
|
1516
|
+
timeout: expect.any(Number),
|
|
1517
|
+
refreshGatt: 'OnConnected',
|
|
1518
|
+
});
|
|
1519
|
+
await settle(transport.release(uuid, true));
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
test('keeps the GATT refresh for a firmware-install reconnect', async () => {
|
|
1523
|
+
const { transport, uuid, device } = createHarness();
|
|
1524
|
+
(transport as any).sessionProtocols.set(uuid, 'V2');
|
|
1525
|
+
reconnectingDevice(device);
|
|
1526
|
+
|
|
1527
|
+
await expect(
|
|
1528
|
+
settle(transport.acquire({ uuid, expectedProtocol: 'V2', skipProtocolProbe: true }))
|
|
1529
|
+
).resolves.toEqual({ uuid, protocolType: 'V2' });
|
|
1530
|
+
expect(device.connect).toHaveBeenCalledWith({
|
|
1531
|
+
timeout: expect.any(Number),
|
|
1532
|
+
refreshGatt: 'OnConnected',
|
|
1533
|
+
});
|
|
1534
|
+
await settle(transport.release(uuid, true));
|
|
1535
|
+
});
|
|
1183
1536
|
});
|
|
1184
1537
|
|
|
1185
1538
|
test('spends one Initialize wake on the detection after a fully silent one', async () => {
|
|
1539
|
+
setPlatformOS('android');
|
|
1186
1540
|
const { transport, uuid } = createHarness();
|
|
1187
1541
|
const probes = transport as any;
|
|
1188
1542
|
jest.spyOn(probes, 'probeProtocolV1').mockResolvedValue(false);
|
|
@@ -1206,6 +1560,35 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
1206
1560
|
expect(callProtocolV1).not.toHaveBeenCalled();
|
|
1207
1561
|
});
|
|
1208
1562
|
|
|
1563
|
+
test('does not send the Initialize wake on iOS', async () => {
|
|
1564
|
+
const { transport, uuid } = createHarness();
|
|
1565
|
+
const probes = transport as any;
|
|
1566
|
+
jest.spyOn(probes, 'probeProtocolV1').mockResolvedValue(false);
|
|
1567
|
+
jest.spyOn(probes, 'probeProtocolV2').mockResolvedValue(false);
|
|
1568
|
+
const callProtocolV1 = jest.spyOn(probes, 'callProtocolV1').mockResolvedValue({});
|
|
1569
|
+
|
|
1570
|
+
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1571
|
+
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1572
|
+
expect(callProtocolV1).not.toHaveBeenCalled();
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1575
|
+
test('an unanswered Initialize wake settles the acquire and frees the lifecycle lock', async () => {
|
|
1576
|
+
setPlatformOS('android');
|
|
1577
|
+
const harness = createHarness();
|
|
1578
|
+
const { transport, uuid } = harness;
|
|
1579
|
+
const probes = transport as any;
|
|
1580
|
+
harness.setShouldRespond(false);
|
|
1581
|
+
jest.spyOn(probes, 'probeProtocolV1').mockResolvedValue(false);
|
|
1582
|
+
jest.spyOn(probes, 'probeProtocolV2').mockResolvedValue(false);
|
|
1583
|
+
|
|
1584
|
+
await expect(transport.acquire({ uuid })).rejects.toBeDefined();
|
|
1585
|
+
await expect(transport.acquire({ uuid })).rejects.toMatchObject({
|
|
1586
|
+
errorCode: HardwareErrorCode.BleTimeoutError,
|
|
1587
|
+
});
|
|
1588
|
+
await transport.disconnect(uuid);
|
|
1589
|
+
await transport.stop();
|
|
1590
|
+
}, 10_000);
|
|
1591
|
+
|
|
1209
1592
|
test('accepts a stable low MTU without the delayed refresh loop', async () => {
|
|
1210
1593
|
const { transport, uuid, device } = createHarness();
|
|
1211
1594
|
device.mtu = 185;
|