@onekeyfe/hd-transport-react-native 1.2.0-alpha.66 → 1.2.0-alpha.68
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 +79 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +298 -40
- package/jest.config.js +5 -0
- package/package.json +5 -5
- package/src/__tests__/connectTimeout.test.ts +281 -0
- package/src/__tests__/protocolReprobe.test.ts +132 -0
- package/src/__tests__/protocolV1SchemaFixture.ts +39 -0
- package/src/__tests__/protocolV2Link.test.ts +10 -1
- package/src/__tests__/staleCallTimeout.test.ts +210 -0
- package/src/__tests__/writePacketTimeout.test.ts +305 -0
- package/src/index.ts +449 -39
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-transport-react-native",
|
|
3
|
-
"version": "1.2.0-alpha.
|
|
3
|
+
"version": "1.2.0-alpha.68",
|
|
4
4
|
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
"lint:fix": "eslint . --fix"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@onekeyfe/hd-core": "1.2.0-alpha.
|
|
24
|
-
"@onekeyfe/hd-shared": "1.2.0-alpha.
|
|
25
|
-
"@onekeyfe/hd-transport": "1.2.0-alpha.
|
|
23
|
+
"@onekeyfe/hd-core": "1.2.0-alpha.68",
|
|
24
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.68",
|
|
25
|
+
"@onekeyfe/hd-transport": "1.2.0-alpha.68",
|
|
26
26
|
"@onekeyfe/react-native-ble-utils": "^0.1.6",
|
|
27
27
|
"react-native-ble-plx": "3.5.1"
|
|
28
28
|
},
|
|
29
|
-
"gitHead": "
|
|
29
|
+
"gitHead": "6e8a931d08768980cfe7fa918bba447ed73e2724"
|
|
30
30
|
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { BleErrorCode } from 'react-native-ble-plx';
|
|
3
|
+
|
|
4
|
+
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
5
|
+
|
|
6
|
+
import ReactNativeBleTransport, {
|
|
7
|
+
BLE_CONNECT_TIMEOUT_MS,
|
|
8
|
+
BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD,
|
|
9
|
+
BLE_GATT_SETUP_TIMEOUT_MS,
|
|
10
|
+
} from '../index';
|
|
11
|
+
import protocolV1Schema from './protocolV1SchemaFixture';
|
|
12
|
+
|
|
13
|
+
jest.mock(
|
|
14
|
+
'react-native',
|
|
15
|
+
() => ({
|
|
16
|
+
Platform: { OS: 'ios', select: (spec: Record<string, unknown>) => spec.ios },
|
|
17
|
+
PermissionsAndroid: {
|
|
18
|
+
PERMISSIONS: {},
|
|
19
|
+
RESULTS: {},
|
|
20
|
+
request: jest.fn(),
|
|
21
|
+
requestMultiple: jest.fn(),
|
|
22
|
+
},
|
|
23
|
+
}),
|
|
24
|
+
{ virtual: true }
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
jest.mock('react-native-ble-plx', () => ({
|
|
28
|
+
BleATTErrorCode: { InvalidHandle: 1, UnlikelyError: 14 },
|
|
29
|
+
BleError: Error,
|
|
30
|
+
BleErrorCode: {
|
|
31
|
+
DeviceDisconnected: 201,
|
|
32
|
+
OperationStartFailed: 601,
|
|
33
|
+
DeviceMTUChangeFailed: 401,
|
|
34
|
+
OperationCancelled: 2,
|
|
35
|
+
OperationTimedOut: 3,
|
|
36
|
+
DeviceAlreadyConnected: 203,
|
|
37
|
+
},
|
|
38
|
+
BleManager: jest.fn(),
|
|
39
|
+
ScanMode: { LowLatency: 2 },
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
jest.mock('@onekeyfe/react-native-ble-utils', () => ({
|
|
43
|
+
__esModule: true,
|
|
44
|
+
default: {
|
|
45
|
+
getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
46
|
+
getBondedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
47
|
+
pairDevice: jest.fn(() => Promise.resolve()),
|
|
48
|
+
},
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
const UUID = 'stalled-connect-device';
|
|
52
|
+
|
|
53
|
+
const flush = () =>
|
|
54
|
+
new Promise(resolve => {
|
|
55
|
+
setImmediate(resolve);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
async function advanceUntil(settled: () => boolean, totalMs: number, stepMs = 250) {
|
|
59
|
+
for (let elapsed = 0; elapsed < totalMs; elapsed += stepMs) {
|
|
60
|
+
jest.advanceTimersByTime(stepMs);
|
|
61
|
+
// eslint-disable-next-line no-await-in-loop
|
|
62
|
+
await flush();
|
|
63
|
+
if (settled()) return;
|
|
64
|
+
}
|
|
65
|
+
throw new Error(`fake timers exhausted after ${totalMs}ms before the connect settled`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A device whose native connect() never settles — the observed iOS failure mode. */
|
|
69
|
+
function createHarness(connectImpl: () => Promise<unknown>) {
|
|
70
|
+
const connect = jest.fn(connectImpl);
|
|
71
|
+
const writeCharacteristic = {
|
|
72
|
+
uuid: '00000002-0000-1000-8000-00805f9b34fb',
|
|
73
|
+
isWritableWithResponse: true,
|
|
74
|
+
};
|
|
75
|
+
const notifyCharacteristic = {
|
|
76
|
+
uuid: '00000003-0000-1000-8000-00805f9b34fb',
|
|
77
|
+
isNotifiable: true,
|
|
78
|
+
};
|
|
79
|
+
const device = {
|
|
80
|
+
id: UUID,
|
|
81
|
+
name: 'OneKey Classic',
|
|
82
|
+
localName: 'OneKey Classic',
|
|
83
|
+
serviceUUIDs: ['00000001-0000-1000-8000-00805f9b34fb'],
|
|
84
|
+
isConnected: jest.fn(() => Promise.resolve(false)),
|
|
85
|
+
cancelConnection: jest.fn(() => Promise.resolve()),
|
|
86
|
+
connect,
|
|
87
|
+
discoverAllServicesAndCharacteristics: jest.fn(() => Promise.resolve()),
|
|
88
|
+
characteristicsForService: jest.fn(() =>
|
|
89
|
+
Promise.resolve([writeCharacteristic, notifyCharacteristic])
|
|
90
|
+
),
|
|
91
|
+
services: jest.fn(() => Promise.resolve([])),
|
|
92
|
+
onDisconnected: jest.fn(() => ({ remove: jest.fn() })),
|
|
93
|
+
};
|
|
94
|
+
const transport = new ReactNativeBleTransport({ scanTimeout: 1 });
|
|
95
|
+
const bleManager = {
|
|
96
|
+
devices: jest.fn(() => Promise.resolve([device])),
|
|
97
|
+
connectedDevices: jest.fn(() => Promise.resolve([])),
|
|
98
|
+
connectToDevice: jest.fn(connectImpl),
|
|
99
|
+
cancelTransaction: jest.fn(() => Promise.resolve()),
|
|
100
|
+
cancelDeviceConnection: jest.fn(() => Promise.resolve()),
|
|
101
|
+
onStateChange: jest.fn((listener: (state: string) => void) => {
|
|
102
|
+
// Dispatch asynchronously: subscribeBleOn wires its own cleanup after
|
|
103
|
+
// registering, so a synchronous callback would run before it is ready.
|
|
104
|
+
setImmediate(() => listener('PoweredOn'));
|
|
105
|
+
return { remove: jest.fn() };
|
|
106
|
+
}),
|
|
107
|
+
state: jest.fn(() => Promise.resolve('PoweredOn')),
|
|
108
|
+
startDeviceScan: jest.fn(),
|
|
109
|
+
stopDeviceScan: jest.fn(),
|
|
110
|
+
};
|
|
111
|
+
(transport as any).blePlxManager = bleManager;
|
|
112
|
+
transport.init(
|
|
113
|
+
{ debug: jest.fn(), error: jest.fn(), warn: jest.fn() } as any,
|
|
114
|
+
new EventEmitter()
|
|
115
|
+
);
|
|
116
|
+
transport.configure(protocolV1Schema);
|
|
117
|
+
return { transport, device, bleManager, connect };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
describe('BLE connect timeout', () => {
|
|
121
|
+
beforeAll(() => {
|
|
122
|
+
jest.useFakeTimers({ doNotFake: ['setImmediate', 'performance'] });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
afterAll(() => {
|
|
126
|
+
jest.useRealTimers();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
afterEach(() => {
|
|
130
|
+
jest.clearAllTimers();
|
|
131
|
+
jest.restoreAllMocks();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('a native connect that never settles is bounded instead of blocking forever', async () => {
|
|
135
|
+
// iOS applies its own connect timeout on a serial queue; when that queue is busy
|
|
136
|
+
// the timeout never fires and acquire() blocks until the app-level 60s timeout.
|
|
137
|
+
const { transport } = createHarness(
|
|
138
|
+
() =>
|
|
139
|
+
new Promise(() => {
|
|
140
|
+
// never settles
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const errors: Array<{ errorCode?: unknown }> = [];
|
|
145
|
+
let settled = false;
|
|
146
|
+
transport.acquire({ uuid: UUID }).catch(e => {
|
|
147
|
+
errors.push(e);
|
|
148
|
+
settled = true;
|
|
149
|
+
});
|
|
150
|
+
await flush();
|
|
151
|
+
|
|
152
|
+
await advanceUntil(() => settled, BLE_CONNECT_TIMEOUT_MS + 5000);
|
|
153
|
+
|
|
154
|
+
expect(errors).toHaveLength(1);
|
|
155
|
+
expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleConnectedError);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('the connect budget leaves generous headroom over a healthy connect', () => {
|
|
159
|
+
// Healthy connects finish in ~2-3s (the native budget is 3s); this backstop only
|
|
160
|
+
// fires when the native timeout itself fails to.
|
|
161
|
+
expect(BLE_CONNECT_TIMEOUT_MS).toBeGreaterThanOrEqual(6000);
|
|
162
|
+
expect(BLE_CONNECT_TIMEOUT_MS).toBeLessThanOrEqual(12000);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('a stalled connect is abandoned natively so the next attempt is not cancelled by it', async () => {
|
|
166
|
+
const { transport, bleManager } = createHarness(
|
|
167
|
+
() =>
|
|
168
|
+
new Promise(() => {
|
|
169
|
+
// never settles
|
|
170
|
+
})
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
let settled = false;
|
|
174
|
+
transport.acquire({ uuid: UUID }).catch(() => {
|
|
175
|
+
settled = true;
|
|
176
|
+
});
|
|
177
|
+
await flush();
|
|
178
|
+
await advanceUntil(() => settled, BLE_CONNECT_TIMEOUT_MS + 5000);
|
|
179
|
+
|
|
180
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(UUID);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('repeated stalled connects recreate the BLE manager', async () => {
|
|
184
|
+
const { transport, bleManager } = createHarness(
|
|
185
|
+
() =>
|
|
186
|
+
new Promise(() => {
|
|
187
|
+
// never settles
|
|
188
|
+
})
|
|
189
|
+
);
|
|
190
|
+
const destroy = jest.fn();
|
|
191
|
+
(bleManager as unknown as { destroy: jest.Mock }).destroy = destroy;
|
|
192
|
+
|
|
193
|
+
for (let attempt = 0; attempt < BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD; attempt += 1) {
|
|
194
|
+
let settled = false;
|
|
195
|
+
transport.acquire({ uuid: UUID }).catch(() => {
|
|
196
|
+
settled = true;
|
|
197
|
+
});
|
|
198
|
+
// eslint-disable-next-line no-await-in-loop
|
|
199
|
+
await flush();
|
|
200
|
+
// eslint-disable-next-line no-await-in-loop
|
|
201
|
+
await advanceUntil(() => settled, BLE_CONNECT_TIMEOUT_MS + 5000);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
expect(destroy).toHaveBeenCalledTimes(1);
|
|
205
|
+
expect((transport as unknown as { blePlxManager?: unknown }).blePlxManager).toBeUndefined();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('native connect timeouts contribute to the same manager reset budget', async () => {
|
|
209
|
+
const { transport, bleManager } = createHarness(() => Promise.resolve());
|
|
210
|
+
const destroy = jest.fn();
|
|
211
|
+
(bleManager as unknown as { destroy: jest.Mock }).destroy = destroy;
|
|
212
|
+
const nativeTimeout = Object.assign(new Error('Operation timed out'), {
|
|
213
|
+
errorCode: BleErrorCode.OperationTimedOut,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
for (let attempt = 0; attempt < BLE_CONNECT_TIMEOUT_MANAGER_RESET_THRESHOLD; attempt += 1) {
|
|
217
|
+
// eslint-disable-next-line no-await-in-loop
|
|
218
|
+
await expect(
|
|
219
|
+
(transport as any).connectWithTimeout(UUID, () => Promise.reject(nativeTimeout))
|
|
220
|
+
).rejects.toBe(nativeTimeout);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
expect(destroy).toHaveBeenCalledTimes(1);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('GATT discovery is bounded and abandons the native connection', async () => {
|
|
227
|
+
const { transport, device, bleManager } = createHarness(() => Promise.resolve());
|
|
228
|
+
device.discoverAllServicesAndCharacteristics.mockImplementation(
|
|
229
|
+
() =>
|
|
230
|
+
new Promise(() => {
|
|
231
|
+
// never settles
|
|
232
|
+
})
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const errors: Array<{ errorCode?: unknown }> = [];
|
|
236
|
+
let settled = false;
|
|
237
|
+
(transport as any).resolveCharacteristicsWithTimeout(UUID, device).catch((error: unknown) => {
|
|
238
|
+
errors.push(error as { errorCode?: unknown });
|
|
239
|
+
settled = true;
|
|
240
|
+
});
|
|
241
|
+
await flush();
|
|
242
|
+
await advanceUntil(() => settled, BLE_GATT_SETUP_TIMEOUT_MS + 5000);
|
|
243
|
+
|
|
244
|
+
expect(errors).toHaveLength(1);
|
|
245
|
+
expect(errors[0]?.errorCode).toBe(HardwareErrorCode.BleConnectedError);
|
|
246
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledWith(UUID);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('a successful GATT retry clears the timeout budget before an abandoned call settles', async () => {
|
|
250
|
+
const { transport, device, bleManager } = createHarness(() => Promise.resolve());
|
|
251
|
+
let resolveAbandonedDiscovery: (() => void) | undefined;
|
|
252
|
+
device.discoverAllServicesAndCharacteristics
|
|
253
|
+
.mockImplementationOnce(
|
|
254
|
+
() =>
|
|
255
|
+
new Promise<void>(resolve => {
|
|
256
|
+
resolveAbandonedDiscovery = resolve;
|
|
257
|
+
})
|
|
258
|
+
)
|
|
259
|
+
.mockResolvedValueOnce(undefined);
|
|
260
|
+
|
|
261
|
+
let firstSettled = false;
|
|
262
|
+
(transport as any).resolveCharacteristicsWithTimeout(UUID, device).catch(() => {
|
|
263
|
+
firstSettled = true;
|
|
264
|
+
});
|
|
265
|
+
await flush();
|
|
266
|
+
await advanceUntil(() => firstSettled, BLE_GATT_SETUP_TIMEOUT_MS + 5000);
|
|
267
|
+
|
|
268
|
+
await expect(
|
|
269
|
+
(transport as any).resolveCharacteristicsWithTimeout(UUID, device)
|
|
270
|
+
).resolves.toMatchObject({
|
|
271
|
+
writeCharacteristic: expect.any(Object),
|
|
272
|
+
notifyCharacteristic: expect.any(Object),
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
resolveAbandonedDiscovery?.();
|
|
276
|
+
await flush();
|
|
277
|
+
|
|
278
|
+
expect(bleManager.cancelDeviceConnection).toHaveBeenCalledTimes(1);
|
|
279
|
+
expect((transport as any).connectionSetupTimeoutCounts.has(UUID)).toBe(false);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import ReactNativeBleTransport, { PROTOCOL_REPROBE_FALLBACK_ATTEMPTS } from '../index';
|
|
2
|
+
import protocolV1Schema from './protocolV1SchemaFixture';
|
|
3
|
+
|
|
4
|
+
jest.mock(
|
|
5
|
+
'react-native',
|
|
6
|
+
() => ({
|
|
7
|
+
Platform: { OS: 'ios', select: (spec: Record<string, unknown>) => spec.ios },
|
|
8
|
+
PermissionsAndroid: {
|
|
9
|
+
PERMISSIONS: {},
|
|
10
|
+
RESULTS: {},
|
|
11
|
+
request: jest.fn(),
|
|
12
|
+
requestMultiple: jest.fn(),
|
|
13
|
+
},
|
|
14
|
+
}),
|
|
15
|
+
{ virtual: true }
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
jest.mock('react-native-ble-plx', () => ({
|
|
19
|
+
BleATTErrorCode: { InvalidHandle: 1, UnlikelyError: 14 },
|
|
20
|
+
BleError: Error,
|
|
21
|
+
BleErrorCode: { DeviceDisconnected: 201, OperationStartFailed: 601 },
|
|
22
|
+
BleManager: jest.fn(),
|
|
23
|
+
ScanMode: { LowLatency: 2 },
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
jest.mock('@onekeyfe/react-native-ble-utils', () => ({
|
|
27
|
+
__esModule: true,
|
|
28
|
+
default: {
|
|
29
|
+
getConnectedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
30
|
+
getBondedPeripherals: jest.fn(() => Promise.resolve([])),
|
|
31
|
+
pairDevice: jest.fn(() => Promise.resolve()),
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
const UUID = 'reprobe-device';
|
|
36
|
+
|
|
37
|
+
/** Drive detectProtocol with stubbed probes so we can observe which are attempted. */
|
|
38
|
+
function createHarness({ v1, v2 }: { v1: boolean; v2: boolean }) {
|
|
39
|
+
const transport = new ReactNativeBleTransport({});
|
|
40
|
+
transport.configure(protocolV1Schema);
|
|
41
|
+
const probeV1 = jest.fn(() => {
|
|
42
|
+
if (v1) {
|
|
43
|
+
(transport as any).deviceProtocol.set(UUID, 'V1');
|
|
44
|
+
}
|
|
45
|
+
return Promise.resolve(v1);
|
|
46
|
+
});
|
|
47
|
+
const probeV2 = jest.fn(() => {
|
|
48
|
+
if (v2) {
|
|
49
|
+
(transport as any).deviceProtocol.set(UUID, 'V2');
|
|
50
|
+
}
|
|
51
|
+
return Promise.resolve(v2);
|
|
52
|
+
});
|
|
53
|
+
(transport as any).probeProtocolV1 = probeV1;
|
|
54
|
+
(transport as any).probeProtocolV2 = probeV2;
|
|
55
|
+
(transport as any).resetProbeStateAfterProtocolProbe = jest.fn(() => Promise.resolve());
|
|
56
|
+
return { transport, probeV1, probeV2 };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const detect = (transport: ReactNativeBleTransport) =>
|
|
60
|
+
(transport as any).detectProtocol(UUID, undefined, undefined, () =>
|
|
61
|
+
Promise.resolve()
|
|
62
|
+
) as Promise<string>;
|
|
63
|
+
|
|
64
|
+
describe('protocol re-probe after a known device goes away', () => {
|
|
65
|
+
test('an unknown device still probes both protocols', async () => {
|
|
66
|
+
const { transport, probeV1, probeV2 } = createHarness({ v1: false, v2: false });
|
|
67
|
+
|
|
68
|
+
await expect(detect(transport)).rejects.toBeDefined();
|
|
69
|
+
|
|
70
|
+
expect(probeV1).toHaveBeenCalledTimes(1);
|
|
71
|
+
expect(probeV2).toHaveBeenCalledTimes(1);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('a device already known to speak V1 does not pay the V2 probe while it is away', async () => {
|
|
75
|
+
const known = createHarness({ v1: true, v2: false });
|
|
76
|
+
// First detection confirms V1 (this is the session that just talked to the device).
|
|
77
|
+
await expect(detect(known.transport)).resolves.toBe('V1');
|
|
78
|
+
|
|
79
|
+
// The device now reboots after a firmware install: V1 stops answering.
|
|
80
|
+
(known.transport as any).deviceProtocol.delete(UUID);
|
|
81
|
+
const probeV1 = jest.fn(() => Promise.resolve(false));
|
|
82
|
+
const probeV2 = jest.fn(() => Promise.resolve(false));
|
|
83
|
+
(known.transport as any).probeProtocolV1 = probeV1;
|
|
84
|
+
(known.transport as any).probeProtocolV2 = probeV2;
|
|
85
|
+
|
|
86
|
+
await expect(detect(known.transport)).rejects.toBeDefined();
|
|
87
|
+
|
|
88
|
+
expect(probeV1).toHaveBeenCalledTimes(1);
|
|
89
|
+
// The 10s Protocol V2 ping is pure waste for a device we just spoke V1 to.
|
|
90
|
+
expect(probeV2).not.toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('after repeated failures it re-probes every protocol again', async () => {
|
|
94
|
+
const known = createHarness({ v1: true, v2: false });
|
|
95
|
+
await expect(detect(known.transport)).resolves.toBe('V1');
|
|
96
|
+
|
|
97
|
+
(known.transport as any).deviceProtocol.delete(UUID);
|
|
98
|
+
const probeV1 = jest.fn(() => Promise.resolve(false));
|
|
99
|
+
const probeV2 = jest.fn(() => Promise.resolve(false));
|
|
100
|
+
(known.transport as any).probeProtocolV1 = probeV1;
|
|
101
|
+
(known.transport as any).probeProtocolV2 = probeV2;
|
|
102
|
+
|
|
103
|
+
for (let attempt = 0; attempt < PROTOCOL_REPROBE_FALLBACK_ATTEMPTS; attempt += 1) {
|
|
104
|
+
// eslint-disable-next-line no-await-in-loop
|
|
105
|
+
await expect(detect(known.transport)).rejects.toBeDefined();
|
|
106
|
+
}
|
|
107
|
+
expect(probeV2).not.toHaveBeenCalled();
|
|
108
|
+
|
|
109
|
+
// The device may genuinely have changed protocol, so the shortcut must expire.
|
|
110
|
+
await expect(detect(known.transport)).rejects.toBeDefined();
|
|
111
|
+
expect(probeV2).toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('a successful detection clears the failure streak', async () => {
|
|
115
|
+
const known = createHarness({ v1: true, v2: false });
|
|
116
|
+
await expect(detect(known.transport)).resolves.toBe('V1');
|
|
117
|
+
(known.transport as any).deviceProtocol.delete(UUID);
|
|
118
|
+
|
|
119
|
+
const failingV1 = jest.fn(() => Promise.resolve(false));
|
|
120
|
+
(known.transport as any).probeProtocolV1 = failingV1;
|
|
121
|
+
await expect(detect(known.transport)).rejects.toBeDefined();
|
|
122
|
+
|
|
123
|
+
// Device comes back.
|
|
124
|
+
(known.transport as any).probeProtocolV1 = jest.fn(() => {
|
|
125
|
+
(known.transport as any).deviceProtocol.set(UUID, 'V1');
|
|
126
|
+
return Promise.resolve(true);
|
|
127
|
+
});
|
|
128
|
+
await expect(detect(known.transport)).resolves.toBe('V1');
|
|
129
|
+
|
|
130
|
+
expect((known.transport as any).protocolReprobeFailures.get(UUID)).toBeUndefined();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const protocolV1Schema = {
|
|
2
|
+
nested: {
|
|
3
|
+
Initialize: {
|
|
4
|
+
fields: {
|
|
5
|
+
session_id: { type: 'bytes', id: 1 },
|
|
6
|
+
derive_cardano: { type: 'bool', id: 3 },
|
|
7
|
+
},
|
|
8
|
+
},
|
|
9
|
+
GetFeatures: { fields: {} },
|
|
10
|
+
Success: {
|
|
11
|
+
fields: {
|
|
12
|
+
message: { type: 'string', id: 1 },
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
Ping: {
|
|
16
|
+
fields: {
|
|
17
|
+
message: { type: 'string', id: 1 },
|
|
18
|
+
button_protection: { type: 'bool', id: 2 },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
FirmwareUpload: {
|
|
22
|
+
fields: {
|
|
23
|
+
payload: { rule: 'required', type: 'bytes', id: 1 },
|
|
24
|
+
hash: { type: 'bytes', id: 2 },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
MessageType: {
|
|
28
|
+
values: {
|
|
29
|
+
MessageType_Initialize: 0,
|
|
30
|
+
MessageType_Ping: 1,
|
|
31
|
+
MessageType_Success: 2,
|
|
32
|
+
MessageType_FirmwareUpload: 7,
|
|
33
|
+
MessageType_GetFeatures: 55,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default protocolV1Schema;
|
|
@@ -613,6 +613,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
613
613
|
|
|
614
614
|
await expect(
|
|
615
615
|
transport.writeProtocolV2Packet(
|
|
616
|
+
'test-device',
|
|
616
617
|
{
|
|
617
618
|
writeCharacteristic: {
|
|
618
619
|
isWritableWithResponse: true,
|
|
@@ -648,6 +649,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
648
649
|
|
|
649
650
|
try {
|
|
650
651
|
const call = transport.writeProtocolV2Frame(
|
|
652
|
+
'test-device',
|
|
651
653
|
bleTransport,
|
|
652
654
|
new Uint8Array(10),
|
|
653
655
|
context,
|
|
@@ -722,6 +724,7 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
722
724
|
configureProtocolV2BleTuning({ iosPacketLength: 20 });
|
|
723
725
|
|
|
724
726
|
await transport.writeProtocolV2Frame(
|
|
727
|
+
'device-uuid',
|
|
725
728
|
bleTransport,
|
|
726
729
|
new Uint8Array(30),
|
|
727
730
|
context,
|
|
@@ -753,7 +756,13 @@ describe('ReactNativeBleTransport Protocol V2 link lifecycle', () => {
|
|
|
753
756
|
configureProtocolV2BleTuning({ iosPacketLength: 20 });
|
|
754
757
|
|
|
755
758
|
await expect(
|
|
756
|
-
transport.writeProtocolV2Frame(
|
|
759
|
+
transport.writeProtocolV2Frame(
|
|
760
|
+
'device-uuid',
|
|
761
|
+
bleTransport,
|
|
762
|
+
new Uint8Array(30),
|
|
763
|
+
context,
|
|
764
|
+
jest.fn()
|
|
765
|
+
)
|
|
757
766
|
).rejects.toMatchObject({ errorCode: 205 });
|
|
758
767
|
expect(writeWithoutResponse).toHaveBeenCalledTimes(1);
|
|
759
768
|
});
|