@onekeyfe/hd-transport-react-native 1.2.2-alpha.115 → 1.2.2-alpha.116
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 +48 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +212 -14
- package/package.json +5 -5
- package/src/__tests__/connectTimeout.test.ts +104 -0
- package/src/__tests__/protocolV2Link.test.ts +450 -24
- package/src/index.ts +341 -22
|
@@ -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,
|
|
@@ -110,10 +111,18 @@ const createHarness = ({
|
|
|
110
111
|
deviceName = 'OneKey Pro 2',
|
|
111
112
|
isWritableWithResponse = true,
|
|
112
113
|
monitorError,
|
|
114
|
+
pro2BleReplies = false,
|
|
113
115
|
}: {
|
|
114
116
|
deviceName?: string;
|
|
115
117
|
isWritableWithResponse?: boolean;
|
|
116
118
|
monitorError?: (Error & { reason?: string; attErrorCode?: number; iosErrorCode?: number }) | null;
|
|
119
|
+
/**
|
|
120
|
+
* Answer like a real Pro 2 over BLE: reassemble a frame split across ATT writes, echo the
|
|
121
|
+
* 17-character probe message so the reply is 29 bytes, and send it as one notification of
|
|
122
|
+
* at most ATT_MTU-3 bytes. A reply that does not fit is cut, never continued (btsnoop:
|
|
123
|
+
* 20 bytes declaring 29 at the default MTU).
|
|
124
|
+
*/
|
|
125
|
+
pro2BleReplies?: boolean;
|
|
117
126
|
} = {}) => {
|
|
118
127
|
const uuid = 'rn-pro2-id';
|
|
119
128
|
const sentSeqs: number[] = [];
|
|
@@ -138,18 +147,31 @@ const createHarness = ({
|
|
|
138
147
|
return { remove: jest.fn() };
|
|
139
148
|
}),
|
|
140
149
|
};
|
|
150
|
+
let pendingPro2Frame: Buffer | undefined;
|
|
151
|
+
let negotiatedMtu = () => 23;
|
|
141
152
|
const handleWrite = (base64: string) => {
|
|
142
|
-
|
|
153
|
+
let frame = Buffer.from(base64, 'base64');
|
|
154
|
+
if (pro2BleReplies) {
|
|
155
|
+
if (frame[0] !== 0x5a && !pendingPro2Frame) return Promise.resolve();
|
|
156
|
+
pendingPro2Frame =
|
|
157
|
+
pendingPro2Frame && frame[0] !== 0x5a ? Buffer.concat([pendingPro2Frame, frame]) : frame;
|
|
158
|
+
const declaredLength =
|
|
159
|
+
pendingPro2Frame.length >= 3 ? pendingPro2Frame[1] + pendingPro2Frame[2] * 256 : Infinity;
|
|
160
|
+
if (pendingPro2Frame.length < declaredLength) return Promise.resolve();
|
|
161
|
+
frame = pendingPro2Frame;
|
|
162
|
+
pendingPro2Frame = undefined;
|
|
163
|
+
}
|
|
143
164
|
sentSeqs.push(frame[6]);
|
|
144
165
|
if (shouldRespond) {
|
|
145
166
|
responseSeq += 1;
|
|
146
167
|
const response = ProtocolV2.encodeFrame(
|
|
147
168
|
schemas,
|
|
148
169
|
'Success',
|
|
149
|
-
{ message: 'ok' },
|
|
170
|
+
{ message: pro2BleReplies ? 'protocol-v2-probe' : 'ok' },
|
|
150
171
|
{ router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
|
|
151
172
|
);
|
|
152
|
-
|
|
173
|
+
const notification = pro2BleReplies ? response.subarray(0, negotiatedMtu() - 3) : response;
|
|
174
|
+
notifyCallback?.(null, { value: Buffer.from(notification).toString('base64') });
|
|
153
175
|
}
|
|
154
176
|
return Promise.resolve();
|
|
155
177
|
};
|
|
@@ -193,6 +215,14 @@ const createHarness = ({
|
|
|
193
215
|
transport.init(logger, emitter);
|
|
194
216
|
transport.configure(protocolV1Schema);
|
|
195
217
|
transport.configureProtocolV2(protocolV2Schema);
|
|
218
|
+
// The Pro 2 sizes replies to the MTU of the link, i.e. the one the transport adopted.
|
|
219
|
+
negotiatedMtu = () => {
|
|
220
|
+
try {
|
|
221
|
+
return (transport as any).getCachedTransport(uuid).mtuSize ?? device.mtu;
|
|
222
|
+
} catch {
|
|
223
|
+
return device.mtu;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
196
226
|
|
|
197
227
|
return {
|
|
198
228
|
transport,
|
|
@@ -203,6 +233,7 @@ const createHarness = ({
|
|
|
203
233
|
bleManager,
|
|
204
234
|
sentSeqs,
|
|
205
235
|
writeCharacteristic,
|
|
236
|
+
notifyCharacteristic,
|
|
206
237
|
setShouldRespond(value: boolean) {
|
|
207
238
|
shouldRespond = value;
|
|
208
239
|
},
|
|
@@ -1145,32 +1176,427 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
1145
1176
|
expect(resolveCharacteristics).not.toHaveBeenCalled();
|
|
1146
1177
|
}, 10_000);
|
|
1147
1178
|
|
|
1148
|
-
|
|
1149
|
-
const
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
.mockImplementation(() =>
|
|
1158
|
-
|
|
1179
|
+
describe('Android MTU before service discovery', () => {
|
|
1180
|
+
const connectedPeripherals = () => jest.requireMock('../BleManager').getConnectedDeviceIds;
|
|
1181
|
+
|
|
1182
|
+
beforeEach(() => {
|
|
1183
|
+
setPlatformOS('android');
|
|
1184
|
+
connectedPeripherals().mockClear();
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
afterEach(() => {
|
|
1188
|
+
connectedPeripherals().mockImplementation(() => Promise.resolve([]));
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
/** Production bounds are seconds long; behaviour tests shorten the real-time waits. */
|
|
1192
|
+
const fastAndroidWaits = (transport: ReactNativeBleTransport) => {
|
|
1193
|
+
transport.androidMtuExchangeTimeoutMs = 200;
|
|
1194
|
+
transport.androidLinkDropQuietMs = 120;
|
|
1195
|
+
transport.androidLinkDropTimeoutMs = 600;
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* A device whose LE link is only up between connect() and cancelConnection(), so the
|
|
1200
|
+
* transport has to reconnect after every drop instead of the test flipping the link.
|
|
1201
|
+
*/
|
|
1202
|
+
const reconnectingDevice = (device: any) => {
|
|
1203
|
+
const link = { connected: false };
|
|
1204
|
+
device.isConnected.mockImplementation(() => Promise.resolve(link.connected));
|
|
1205
|
+
device.connect = jest.fn(() => {
|
|
1206
|
+
link.connected = true;
|
|
1159
1207
|
return Promise.resolve(device);
|
|
1160
1208
|
});
|
|
1209
|
+
device.cancelConnection.mockImplementation(() => {
|
|
1210
|
+
link.connected = false;
|
|
1211
|
+
return Promise.resolve();
|
|
1212
|
+
});
|
|
1213
|
+
return link;
|
|
1214
|
+
};
|
|
1161
1215
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1216
|
+
/** requestMTU resolves a fresh Device snapshot, as react-native-ble-plx does. */
|
|
1217
|
+
const negotiatedSnapshot = (device: any, mtu: number) => {
|
|
1218
|
+
const negotiated = { ...device, mtu };
|
|
1219
|
+
device.requestMTU.mockImplementation(() => Promise.resolve(negotiated));
|
|
1220
|
+
return negotiated;
|
|
1221
|
+
};
|
|
1222
|
+
|
|
1223
|
+
const defaultMtuError = {
|
|
1224
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
1225
|
+
message: expect.stringContaining('BLE link stayed at the default MTU'),
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
test('negotiates the MTU after a bare connect and before service discovery, adopting the returned device', async () => {
|
|
1229
|
+
const { transport, uuid, device } = createHarness();
|
|
1230
|
+
reconnectingDevice(device);
|
|
1231
|
+
device.mtu = 23;
|
|
1232
|
+
const negotiated = negotiatedSnapshot(device, 247);
|
|
1233
|
+
|
|
1234
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).resolves.toEqual({
|
|
1235
|
+
uuid,
|
|
1236
|
+
protocolType: 'V2',
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
// No MTU request or GATT refresh inside the native connect budget: refreshGatt makes
|
|
1240
|
+
// the stack rediscover first, and a budget that expires mid-rediscovery closes the
|
|
1241
|
+
// client with the MTU request still unsent.
|
|
1242
|
+
expect(device.connect).toHaveBeenCalledTimes(1);
|
|
1243
|
+
expect(device.connect).toHaveBeenCalledWith({ timeout: expect.any(Number) });
|
|
1244
|
+
const resolveCharacteristics = (transport as any).resolveCharacteristics as jest.Mock;
|
|
1245
|
+
const [connectOrder] = device.connect.mock.invocationCallOrder;
|
|
1246
|
+
const [mtuOrder] = device.requestMTU.mock.invocationCallOrder;
|
|
1247
|
+
const [discoveryOrder] = resolveCharacteristics.mock.invocationCallOrder;
|
|
1248
|
+
expect(connectOrder).toBeLessThan(mtuOrder);
|
|
1249
|
+
expect(mtuOrder).toBeLessThan(discoveryOrder);
|
|
1250
|
+
const cached = (transport as any).getCachedTransport(uuid);
|
|
1251
|
+
expect(cached.device).toBe(negotiated);
|
|
1252
|
+
expect(cached.mtuSize).toBe(247);
|
|
1253
|
+
await transport.release(uuid, true);
|
|
1165
1254
|
});
|
|
1166
1255
|
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1256
|
+
test('keeps the iOS connect chain unchanged', async () => {
|
|
1257
|
+
setPlatformOS('ios');
|
|
1258
|
+
const { transport, uuid, device } = createHarness();
|
|
1259
|
+
reconnectingDevice(device);
|
|
1260
|
+
|
|
1261
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).resolves.toEqual({
|
|
1262
|
+
uuid,
|
|
1263
|
+
protocolType: 'V2',
|
|
1264
|
+
});
|
|
1265
|
+
expect(device.connect).toHaveBeenCalledWith({
|
|
1266
|
+
requestMTU: 247,
|
|
1267
|
+
timeout: expect.any(Number),
|
|
1268
|
+
refreshGatt: 'OnConnected',
|
|
1269
|
+
});
|
|
1270
|
+
expect(device.requestMTU).not.toHaveBeenCalled();
|
|
1271
|
+
await transport.release(uuid, true);
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
test.each([
|
|
1275
|
+
{ expectedProtocol: 'V2' as const, label: 'an expected Protocol V2 device' },
|
|
1276
|
+
{ expectedProtocol: 'V1' as const, label: 'an expected Protocol V1 device' },
|
|
1277
|
+
{ expectedProtocol: undefined, label: 'a device of unknown protocol' },
|
|
1278
|
+
])('drops a link that stays at the default MTU for $label', async ({ expectedProtocol }) => {
|
|
1279
|
+
const { transport, uuid, device, bleManager, writeCharacteristic, notifyCharacteristic } =
|
|
1280
|
+
createHarness({ pro2BleReplies: true });
|
|
1281
|
+
fastAndroidWaits(transport);
|
|
1282
|
+
device.mtu = 23;
|
|
1283
|
+
// The exchange completes without raising the MTU.
|
|
1284
|
+
device.requestMTU.mockImplementation(() => Promise.resolve(device));
|
|
1285
|
+
|
|
1286
|
+
// Protocol V1 writes 192-byte packets whatever the MTU, and the fake Pro 2 would
|
|
1287
|
+
// answer the 29-byte probe with 20 bytes and stop, so the link is useless to either.
|
|
1288
|
+
await expect(transport.acquire({ uuid, expectedProtocol })).rejects.toMatchObject(
|
|
1289
|
+
defaultMtuError
|
|
1290
|
+
);
|
|
1291
|
+
// Refused before a transport exists, i.e. by acquire itself: no discovery, no
|
|
1292
|
+
// notification subscription, no write of any protocol.
|
|
1293
|
+
expect((transport as any).resolveCharacteristics).not.toHaveBeenCalled();
|
|
1294
|
+
expect(notifyCharacteristic.monitor).not.toHaveBeenCalled();
|
|
1295
|
+
expect(writeCharacteristic.writeWithoutResponse).not.toHaveBeenCalled();
|
|
1296
|
+
expect(writeCharacteristic.writeWithResponse).not.toHaveBeenCalled();
|
|
1297
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
1298
|
+
expect(device.cancelConnection).toHaveBeenCalled();
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
test('treats a fast native MTU rejection as a default-MTU link, not as a timeout', async () => {
|
|
1302
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1303
|
+
fastAndroidWaits(transport);
|
|
1304
|
+
device.mtu = 23;
|
|
1305
|
+
device.requestMTU.mockImplementation(() => Promise.reject(new Error('MTU request failed')));
|
|
1306
|
+
|
|
1307
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject(
|
|
1308
|
+
defaultMtuError
|
|
1309
|
+
);
|
|
1310
|
+
// No bounded wait ran, so nothing was abandoned mid-flight.
|
|
1311
|
+
expect(bleManager.cancelTransaction).not.toHaveBeenCalled();
|
|
1312
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
test('drops a link whose MTU exchange never completes and holds it past the quiet period', async () => {
|
|
1316
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1317
|
+
fastAndroidWaits(transport);
|
|
1318
|
+
device.mtu = 23;
|
|
1319
|
+
device.requestMTU.mockImplementation(() => new Promise(() => {}));
|
|
1320
|
+
// The GATT registry empties as soon as this client closes; that alone must not
|
|
1321
|
+
// release the wait, because the LE link outlives the client by the idle timer.
|
|
1322
|
+
connectedPeripherals().mockImplementation(() => Promise.resolve([]));
|
|
1323
|
+
|
|
1324
|
+
const startedAt = Date.now();
|
|
1325
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
1326
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
1327
|
+
message: expect.stringContaining('BLE MTU exchange did not complete'),
|
|
1328
|
+
});
|
|
1329
|
+
expect(Date.now() - startedAt).toBeGreaterThanOrEqual(
|
|
1330
|
+
transport.androidMtuExchangeTimeoutMs + transport.androidLinkDropQuietMs
|
|
1331
|
+
);
|
|
1332
|
+
expect(bleManager.cancelTransaction).toHaveBeenCalledWith(
|
|
1333
|
+
expect.stringContaining(`${uuid}:mtu:connected:0:`)
|
|
1334
|
+
);
|
|
1335
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(uuid);
|
|
1336
|
+
expect(device.cancelConnection).toHaveBeenCalled();
|
|
1337
|
+
expect((transport as any).resolveCharacteristics).not.toHaveBeenCalled();
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
test('a second consecutive MTU timeout trips the wedged-link guard', async () => {
|
|
1341
|
+
const { transport, uuid, device, bleManager } = createHarness();
|
|
1342
|
+
fastAndroidWaits(transport);
|
|
1343
|
+
device.mtu = 23;
|
|
1344
|
+
device.requestMTU.mockImplementation(() => new Promise(() => {}));
|
|
1345
|
+
|
|
1346
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
1347
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
1348
|
+
});
|
|
1349
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toMatchObject({
|
|
1350
|
+
errorCode: HardwareErrorCode.PollingTimeout,
|
|
1351
|
+
message: expect.stringContaining(BLE_SETUP_WEDGED_MESSAGE),
|
|
1352
|
+
});
|
|
1353
|
+
expect(bleManager.destroy).toHaveBeenCalled();
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
test('stop() during the link-drop wait releases it promptly', async () => {
|
|
1357
|
+
const { transport, uuid, device } = createHarness();
|
|
1358
|
+
fastAndroidWaits(transport);
|
|
1359
|
+
transport.androidLinkDropQuietMs = 3000;
|
|
1360
|
+
transport.androidLinkDropTimeoutMs = 6000;
|
|
1361
|
+
device.mtu = 23;
|
|
1362
|
+
device.requestMTU.mockImplementation(() => new Promise(() => {}));
|
|
1363
|
+
|
|
1364
|
+
const acquiring = transport.acquire({ uuid, expectedProtocol: 'V2' }).catch(error => error);
|
|
1365
|
+
await new Promise(resolve => {
|
|
1366
|
+
setTimeout(resolve, transport.androidMtuExchangeTimeoutMs + 100);
|
|
1367
|
+
});
|
|
1368
|
+
const stoppedAt = Date.now();
|
|
1369
|
+
await transport.stop();
|
|
1370
|
+
await expect(acquiring).resolves.toBeInstanceOf(Error);
|
|
1371
|
+
expect(Date.now() - stoppedAt).toBeLessThan(1500);
|
|
1372
|
+
}, 10_000);
|
|
1373
|
+
|
|
1374
|
+
test('does not reuse a cached transport that reports the default MTU', async () => {
|
|
1375
|
+
const { transport, uuid, device } = createHarness();
|
|
1376
|
+
fastAndroidWaits(transport);
|
|
1377
|
+
device.mtu = 23;
|
|
1378
|
+
negotiatedSnapshot(device, 247);
|
|
1379
|
+
|
|
1380
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
1381
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(1);
|
|
1382
|
+
(transport as any).getCachedTransport(uuid).mtuSize = 23;
|
|
1383
|
+
|
|
1384
|
+
await expect(transport.call(uuid, 'Ping', { message: 'x' })).rejects.toMatchObject({
|
|
1385
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
1386
|
+
message: expect.stringContaining('Protocol V2 needs a negotiated BLE MTU'),
|
|
1387
|
+
});
|
|
1388
|
+
// The next acquire goes through the full path and negotiates again instead of
|
|
1389
|
+
// handing the same unusable link back to every retry.
|
|
1390
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).resolves.toEqual({
|
|
1391
|
+
uuid,
|
|
1392
|
+
protocolType: 'V2',
|
|
1393
|
+
});
|
|
1394
|
+
expect(device.requestMTU).toHaveBeenCalledTimes(2);
|
|
1395
|
+
expect((transport as any).getCachedTransport(uuid).mtuSize).toBe(247);
|
|
1396
|
+
await transport.release(uuid, true);
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
test('reassembles a Protocol V2 frame split across ATT writes at a negotiated MTU', async () => {
|
|
1400
|
+
const { transport, uuid, device, writeCharacteristic } = createHarness({
|
|
1401
|
+
pro2BleReplies: true,
|
|
1402
|
+
});
|
|
1403
|
+
device.mtu = 23;
|
|
1404
|
+
negotiatedSnapshot(device, 247);
|
|
1405
|
+
|
|
1406
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).resolves.toEqual({
|
|
1407
|
+
uuid,
|
|
1408
|
+
protocolType: 'V2',
|
|
1409
|
+
});
|
|
1410
|
+
const writesBefore = writeCharacteristic.writeWithoutResponse.mock.calls.length;
|
|
1411
|
+
// Longer than one 244-byte packet, so the fake Pro 2 only answers once it has
|
|
1412
|
+
// reassembled both writes by the declared frame length.
|
|
1413
|
+
await expect(
|
|
1414
|
+
transport.call(uuid, 'Ping', { message: 'x'.repeat(300) })
|
|
1415
|
+
).resolves.toMatchObject({ type: 'Success' });
|
|
1416
|
+
expect(writeCharacteristic.writeWithoutResponse.mock.calls.length - writesBefore).toBe(2);
|
|
1417
|
+
await transport.release(uuid, true);
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
test('a reply larger than ATT_MTU-3 is cut by the fake Pro 2 and the call never completes', async () => {
|
|
1421
|
+
const { transport, uuid, device } = createHarness({ pro2BleReplies: true });
|
|
1422
|
+
fastAndroidWaits(transport);
|
|
1423
|
+
(transport as any).sessionProtocols.set(uuid, 'V2');
|
|
1424
|
+
reconnectingDevice(device);
|
|
1425
|
+
device.mtu = 23;
|
|
1426
|
+
// 30 passes the default-MTU check but only carries 27-byte notifications, short of
|
|
1427
|
+
// the 29-byte reply — the truncation seen in the capture at MTU 23.
|
|
1428
|
+
negotiatedSnapshot(device, 30);
|
|
1429
|
+
|
|
1430
|
+
await expect(
|
|
1431
|
+
transport.acquire({ uuid, expectedProtocol: 'V2', skipProtocolProbe: true })
|
|
1432
|
+
).resolves.toEqual({ uuid, protocolType: 'V2' });
|
|
1433
|
+
await expect(
|
|
1434
|
+
transport.call(uuid, 'Ping', { message: 'protocol-v2-probe' }, { timeoutMs: 300 })
|
|
1435
|
+
).rejects.toBeDefined();
|
|
1436
|
+
await transport.release(uuid, true);
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
test.each([
|
|
1440
|
+
{
|
|
1441
|
+
label: 'a missing OneKey service',
|
|
1442
|
+
error: () => ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound),
|
|
1443
|
+
},
|
|
1444
|
+
{
|
|
1445
|
+
label: 'a missing characteristic',
|
|
1446
|
+
error: () => ERRORS.TypedError(HardwareErrorCode.BleCharacteristicNotFound),
|
|
1447
|
+
},
|
|
1448
|
+
{
|
|
1449
|
+
label: 'a mis-typed characteristic',
|
|
1450
|
+
error: () =>
|
|
1451
|
+
ERRORS.TypedError('BLECharacteristicNotWritable: write characteristic not writable'),
|
|
1452
|
+
},
|
|
1453
|
+
])(
|
|
1454
|
+
'drops the link on $label and rediscovers the GATT table on the next connect, before the MTU exchange',
|
|
1455
|
+
async ({ error }) => {
|
|
1456
|
+
const { transport, uuid, device } = createHarness();
|
|
1457
|
+
fastAndroidWaits(transport);
|
|
1458
|
+
const link = reconnectingDevice(device);
|
|
1459
|
+
device.mtu = 23;
|
|
1460
|
+
negotiatedSnapshot(device, 247);
|
|
1461
|
+
const resolveCharacteristics = (transport as any).resolveCharacteristics as jest.Mock;
|
|
1462
|
+
resolveCharacteristics.mockImplementationOnce(() => Promise.reject(error()));
|
|
1463
|
+
|
|
1464
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toBeDefined();
|
|
1465
|
+
expect(device.connect).toHaveBeenLastCalledWith({ timeout: expect.any(Number) });
|
|
1466
|
+
// The stale table is only refreshed through a connect, so the link must be down.
|
|
1467
|
+
expect(device.cancelConnection).toHaveBeenCalled();
|
|
1468
|
+
expect(link.connected).toBe(false);
|
|
1469
|
+
|
|
1470
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).resolves.toEqual({
|
|
1471
|
+
uuid,
|
|
1472
|
+
protocolType: 'V2',
|
|
1473
|
+
});
|
|
1474
|
+
expect(device.connect).toHaveBeenLastCalledWith({
|
|
1475
|
+
timeout: expect.any(Number),
|
|
1476
|
+
refreshGatt: 'OnConnected',
|
|
1477
|
+
});
|
|
1478
|
+
const discoveryOrders = resolveCharacteristics.mock.invocationCallOrder;
|
|
1479
|
+
const mtuOrders: number[] = device.requestMTU.mock.invocationCallOrder;
|
|
1480
|
+
expect(discoveryOrders[discoveryOrders.length - 1]).toBeLessThan(
|
|
1481
|
+
mtuOrders[mtuOrders.length - 1]
|
|
1482
|
+
);
|
|
1483
|
+
|
|
1484
|
+
// The refresh is spent once the table resolved through a refreshed connect.
|
|
1485
|
+
await transport.release(uuid, true);
|
|
1486
|
+
link.connected = false;
|
|
1487
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
1488
|
+
expect(device.connect).toHaveBeenLastCalledWith({ timeout: expect.any(Number) });
|
|
1489
|
+
await transport.release(uuid, true);
|
|
1490
|
+
}
|
|
1491
|
+
);
|
|
1492
|
+
|
|
1493
|
+
test('keeps the refresh marker when the refresh connect fell back without refreshGatt', async () => {
|
|
1494
|
+
const { transport, uuid, device } = createHarness();
|
|
1495
|
+
fastAndroidWaits(transport);
|
|
1496
|
+
const link = reconnectingDevice(device);
|
|
1497
|
+
device.mtu = 23;
|
|
1498
|
+
negotiatedSnapshot(device, 247);
|
|
1499
|
+
const resolveCharacteristics = (transport as any).resolveCharacteristics as jest.Mock;
|
|
1500
|
+
resolveCharacteristics.mockImplementationOnce(() =>
|
|
1501
|
+
Promise.reject(ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound))
|
|
1502
|
+
);
|
|
1503
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).rejects.toBeDefined();
|
|
1504
|
+
|
|
1505
|
+
// The refresh connect is cancelled by the native budget; the fallback connect that
|
|
1506
|
+
// succeeds carries no refreshGatt, so the cache was never cleared.
|
|
1507
|
+
const cancelled = Object.assign(new Error('Operation was cancelled'), { errorCode: 2 });
|
|
1508
|
+
device.connect.mockImplementationOnce(() => Promise.reject(cancelled));
|
|
1509
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).resolves.toEqual({
|
|
1510
|
+
uuid,
|
|
1511
|
+
protocolType: 'V2',
|
|
1512
|
+
});
|
|
1513
|
+
expect(device.connect).toHaveBeenLastCalledWith({ timeout: expect.any(Number) });
|
|
1514
|
+
|
|
1515
|
+
await transport.release(uuid, true);
|
|
1516
|
+
link.connected = false;
|
|
1517
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
1518
|
+
expect(device.connect).toHaveBeenLastCalledWith({
|
|
1519
|
+
timeout: expect.any(Number),
|
|
1520
|
+
refreshGatt: 'OnConnected',
|
|
1521
|
+
});
|
|
1522
|
+
await transport.release(uuid, true);
|
|
1523
|
+
});
|
|
1524
|
+
|
|
1525
|
+
test('arms a GATT refresh from a stale-table notify failure', async () => {
|
|
1526
|
+
const harness = createHarness();
|
|
1527
|
+
const { transport, uuid, device } = harness;
|
|
1528
|
+
fastAndroidWaits(transport);
|
|
1529
|
+
const link = reconnectingDevice(device);
|
|
1530
|
+
device.mtu = 23;
|
|
1531
|
+
negotiatedSnapshot(device, 247);
|
|
1532
|
+
|
|
1533
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
1534
|
+
expect(device.connect).toHaveBeenLastCalledWith({ timeout: expect.any(Number) });
|
|
1535
|
+
// The symptom recorded after Pro firmware upgrades: the UUIDs still resolve from the
|
|
1536
|
+
// cached table, the stale CCCD handle fails when notifications are enabled.
|
|
1537
|
+
harness.emitMonitorError(
|
|
1538
|
+
Object.assign(new Error('notify failed'), {
|
|
1539
|
+
reason: 'Cannot find client characteristic config descriptor',
|
|
1540
|
+
})
|
|
1541
|
+
);
|
|
1542
|
+
|
|
1543
|
+
await expect(transport.acquire({ uuid, expectedProtocol: 'V2' })).resolves.toEqual({
|
|
1544
|
+
uuid,
|
|
1545
|
+
protocolType: 'V2',
|
|
1546
|
+
});
|
|
1547
|
+
expect(link.connected).toBe(true);
|
|
1548
|
+
expect(device.connect).toHaveBeenLastCalledWith({
|
|
1549
|
+
timeout: expect.any(Number),
|
|
1550
|
+
refreshGatt: 'OnConnected',
|
|
1551
|
+
});
|
|
1552
|
+
await transport.release(uuid, true);
|
|
1553
|
+
});
|
|
1554
|
+
|
|
1555
|
+
test('keeps the GATT refresh for a firmware-install reconnect', async () => {
|
|
1556
|
+
const { transport, uuid, device } = createHarness();
|
|
1557
|
+
(transport as any).sessionProtocols.set(uuid, 'V2');
|
|
1558
|
+
reconnectingDevice(device);
|
|
1559
|
+
|
|
1560
|
+
await expect(
|
|
1561
|
+
transport.acquire({ uuid, expectedProtocol: 'V2', skipProtocolProbe: true })
|
|
1562
|
+
).resolves.toEqual({ uuid, protocolType: 'V2' });
|
|
1563
|
+
expect(device.connect).toHaveBeenCalledWith({
|
|
1564
|
+
timeout: expect.any(Number),
|
|
1565
|
+
refreshGatt: 'OnConnected',
|
|
1566
|
+
});
|
|
1567
|
+
await transport.release(uuid, true);
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
test('a firmware upload reconnect refreshes the table, discovers, then negotiates the MTU', async () => {
|
|
1571
|
+
const { transport, uuid, device } = createHarness();
|
|
1572
|
+
fastAndroidWaits(transport);
|
|
1573
|
+
const link = reconnectingDevice(device);
|
|
1574
|
+
device.mtu = 23;
|
|
1575
|
+
const negotiated = negotiatedSnapshot(device, 247);
|
|
1576
|
+
await transport.acquire({ uuid, expectedProtocol: 'V2' });
|
|
1577
|
+
const cached = (transport as any).getCachedTransport(uuid);
|
|
1578
|
+
const resolveCharacteristics = (transport as any).resolveCharacteristics as jest.Mock;
|
|
1579
|
+
device.connect.mockClear();
|
|
1580
|
+
device.requestMTU.mockClear();
|
|
1581
|
+
resolveCharacteristics.mockClear();
|
|
1582
|
+
|
|
1583
|
+
link.connected = false;
|
|
1584
|
+
cached.mtuSize = 23;
|
|
1585
|
+
await transport.reconnectFirmwareUploadTransport(uuid, cached);
|
|
1586
|
+
|
|
1587
|
+
expect(device.connect).toHaveBeenCalledWith({
|
|
1588
|
+
timeout: expect.any(Number),
|
|
1589
|
+
refreshGatt: 'OnConnected',
|
|
1590
|
+
});
|
|
1591
|
+
const [connectOrder] = device.connect.mock.invocationCallOrder;
|
|
1592
|
+
const [discoveryOrder] = resolveCharacteristics.mock.invocationCallOrder;
|
|
1593
|
+
const [mtuOrder] = device.requestMTU.mock.invocationCallOrder;
|
|
1594
|
+
expect(connectOrder).toBeLessThan(discoveryOrder);
|
|
1595
|
+
expect(discoveryOrder).toBeLessThan(mtuOrder);
|
|
1596
|
+
expect(cached.device).toBe(negotiated);
|
|
1597
|
+
expect(cached.mtuSize).toBe(247);
|
|
1598
|
+
await transport.release(uuid, true);
|
|
1599
|
+
});
|
|
1174
1600
|
});
|
|
1175
1601
|
|
|
1176
1602
|
test('cleans up a cancelled native GATT setup, not only a timed-out one', async () => {
|