@onekeyfe/hd-transport-web-device 1.2.0-alpha.1 → 1.2.0-alpha.100
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/__tests__/electron-ble-transport.test.ts +465 -20
- package/__tests__/webusb-protocol-v2-timeout.test.ts +385 -0
- package/dist/ble-packet-capacity.d.ts +2 -0
- package/dist/ble-packet-capacity.d.ts.map +1 -0
- package/dist/electron-ble-transport.d.ts +19 -7
- package/dist/electron-ble-transport.d.ts.map +1 -1
- package/dist/index.d.ts +114 -16
- package/dist/index.js +439 -353
- package/dist/transportLog.d.ts +2 -0
- package/dist/transportLog.d.ts.map +1 -0
- package/dist/webusb.d.ts +16 -10
- package/dist/webusb.d.ts.map +1 -1
- package/jest.config.js +5 -0
- package/package.json +6 -5
- package/src/ble-packet-capacity.ts +13 -0
- package/src/electron-ble-transport.ts +321 -196
- package/src/transportLog.ts +1 -0
- package/src/webusb.ts +198 -227
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import transport, { PROTOCOL_V2_CHANNEL_BLE_UART, bytesToHex } from '@onekeyfe/hd-transport';
|
|
2
|
+
import { HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
|
|
3
|
+
import EventEmitter from 'events';
|
|
2
4
|
|
|
3
5
|
import ElectronBleTransport from '../src/electron-ble-transport';
|
|
4
6
|
|
|
@@ -9,6 +11,9 @@ const protocolV1Schema = {
|
|
|
9
11
|
Initialize: {
|
|
10
12
|
fields: {},
|
|
11
13
|
},
|
|
14
|
+
GetFeatures: {
|
|
15
|
+
fields: {},
|
|
16
|
+
},
|
|
12
17
|
Success: {
|
|
13
18
|
fields: {
|
|
14
19
|
message: {
|
|
@@ -21,6 +26,7 @@ const protocolV1Schema = {
|
|
|
21
26
|
values: {
|
|
22
27
|
MessageType_Initialize: 1,
|
|
23
28
|
MessageType_Success: 2,
|
|
29
|
+
MessageType_GetFeatures: 55,
|
|
24
30
|
},
|
|
25
31
|
},
|
|
26
32
|
},
|
|
@@ -28,21 +34,25 @@ const protocolV1Schema = {
|
|
|
28
34
|
|
|
29
35
|
const protocolV2Schema = {
|
|
30
36
|
nested: {
|
|
31
|
-
|
|
37
|
+
ProtocolInfoRequest: {
|
|
32
38
|
fields: {},
|
|
33
39
|
},
|
|
34
|
-
|
|
40
|
+
ProtocolInfo: {
|
|
35
41
|
fields: {
|
|
36
|
-
|
|
42
|
+
version: {
|
|
37
43
|
type: 'uint32',
|
|
38
44
|
id: 1,
|
|
39
45
|
},
|
|
40
|
-
|
|
46
|
+
supported_messages: {
|
|
47
|
+
rule: 'repeated',
|
|
41
48
|
type: 'uint32',
|
|
42
49
|
id: 2,
|
|
50
|
+
options: {
|
|
51
|
+
packed: false,
|
|
52
|
+
},
|
|
43
53
|
},
|
|
44
|
-
|
|
45
|
-
type: '
|
|
54
|
+
protobuf_definition: {
|
|
55
|
+
type: 'string',
|
|
46
56
|
id: 3,
|
|
47
57
|
},
|
|
48
58
|
},
|
|
@@ -65,8 +75,8 @@ const protocolV2Schema = {
|
|
|
65
75
|
},
|
|
66
76
|
MessageType: {
|
|
67
77
|
values: {
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
MessageType_ProtocolInfoRequest: 60200,
|
|
79
|
+
MessageType_ProtocolInfo: 60201,
|
|
70
80
|
MessageType_Ping: 60206,
|
|
71
81
|
MessageType_Success: 60207,
|
|
72
82
|
},
|
|
@@ -95,6 +105,7 @@ const createNobleBle = (device = { id: 'flaky-pro2-id', name: 'Unknown BLE Devic
|
|
|
95
105
|
unsubscribe: jest.fn(() => Promise.resolve()),
|
|
96
106
|
write: jest.fn(() => Promise.resolve()),
|
|
97
107
|
onNotification: jest.fn(() => jest.fn()),
|
|
108
|
+
onMtuChanged: jest.fn(() => jest.fn()),
|
|
98
109
|
onDeviceDisconnected: jest.fn(() => jest.fn()),
|
|
99
110
|
checkAvailability: jest.fn(() =>
|
|
100
111
|
Promise.resolve({
|
|
@@ -106,7 +117,10 @@ const createNobleBle = (device = { id: 'flaky-pro2-id', name: 'Unknown BLE Devic
|
|
|
106
117
|
),
|
|
107
118
|
});
|
|
108
119
|
|
|
109
|
-
const configureTransport = (
|
|
120
|
+
const configureTransport = (
|
|
121
|
+
nobleBle: ReturnType<typeof createNobleBle>,
|
|
122
|
+
emitter?: EventEmitter
|
|
123
|
+
) => {
|
|
110
124
|
(global as any).window = {
|
|
111
125
|
desktopApi: {
|
|
112
126
|
nobleBle,
|
|
@@ -114,7 +128,7 @@ const configureTransport = (nobleBle: ReturnType<typeof createNobleBle>) => {
|
|
|
114
128
|
};
|
|
115
129
|
|
|
116
130
|
const transport = new ElectronBleTransport();
|
|
117
|
-
transport.init(createLogger());
|
|
131
|
+
transport.init(createLogger(), emitter);
|
|
118
132
|
transport.configure(protocolV1Schema);
|
|
119
133
|
transport.configureProtocolV2(protocolV2Schema);
|
|
120
134
|
return transport;
|
|
@@ -126,6 +140,128 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
126
140
|
jest.clearAllMocks();
|
|
127
141
|
});
|
|
128
142
|
|
|
143
|
+
test('keeps raw BLE lifecycle payloads off the public device event channel', async () => {
|
|
144
|
+
const device = { id: 'lifecycle-pro2-id', name: 'OneKey Pro 2' };
|
|
145
|
+
const nobleBle = createNobleBle(device);
|
|
146
|
+
const emitter = new EventEmitter();
|
|
147
|
+
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
148
|
+
let disconnectHandler: ((device: { id: string; name: string | null }) => void) | undefined;
|
|
149
|
+
let responseSeq = 0;
|
|
150
|
+
|
|
151
|
+
nobleBle.onNotification.mockImplementation(handler => {
|
|
152
|
+
notificationHandler = handler;
|
|
153
|
+
return jest.fn();
|
|
154
|
+
});
|
|
155
|
+
nobleBle.onDeviceDisconnected.mockImplementation(handler => {
|
|
156
|
+
disconnectHandler = handler;
|
|
157
|
+
return jest.fn();
|
|
158
|
+
});
|
|
159
|
+
nobleBle.write.mockImplementation(() => {
|
|
160
|
+
responseSeq += 1;
|
|
161
|
+
const response = ProtocolV2.encodeFrame(
|
|
162
|
+
schemas,
|
|
163
|
+
'Success',
|
|
164
|
+
{ message: 'ok' },
|
|
165
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
|
|
166
|
+
);
|
|
167
|
+
setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0);
|
|
168
|
+
return Promise.resolve();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const publicConnect = jest.fn();
|
|
172
|
+
const publicDisconnect = jest.fn();
|
|
173
|
+
const transportDisconnect = jest.fn();
|
|
174
|
+
emitter.on('device-connect', publicConnect);
|
|
175
|
+
emitter.on('device-disconnect', publicDisconnect);
|
|
176
|
+
emitter.on('transport-device-disconnect', transportDisconnect);
|
|
177
|
+
const bleTransport = configureTransport(nobleBle, emitter);
|
|
178
|
+
|
|
179
|
+
await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
|
|
180
|
+
expect(nobleBle.subscribe.mock.invocationCallOrder[0]).toBeLessThan(
|
|
181
|
+
nobleBle.getDevice.mock.invocationCallOrder[1]
|
|
182
|
+
);
|
|
183
|
+
disconnectHandler?.(device);
|
|
184
|
+
|
|
185
|
+
expect(publicConnect).not.toHaveBeenCalled();
|
|
186
|
+
expect(publicDisconnect).not.toHaveBeenCalled();
|
|
187
|
+
expect(transportDisconnect).toHaveBeenCalledWith({
|
|
188
|
+
id: device.id,
|
|
189
|
+
connectId: device.id,
|
|
190
|
+
name: device.name,
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('uses the Protocol V2 BLE writer with the Electron packet size', async () => {
|
|
195
|
+
const device = { id: 'chunked-pro2-id', name: 'OneKey Pro 2' };
|
|
196
|
+
const nobleBle = createNobleBle(device);
|
|
197
|
+
const bleTransport = configureTransport(nobleBle) as any;
|
|
198
|
+
const context = {
|
|
199
|
+
messageName: 'Ping',
|
|
200
|
+
timeoutMs: 1000,
|
|
201
|
+
highThroughput: false,
|
|
202
|
+
generation: 1,
|
|
203
|
+
signal: new AbortController().signal,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
await bleTransport.writeProtocolV2Frame(device.id, new Uint8Array(193), context, jest.fn());
|
|
207
|
+
|
|
208
|
+
expect(nobleBle.write).toHaveBeenCalledTimes(2);
|
|
209
|
+
expect(nobleBle.write.mock.calls.map(([, hex]) => hex.length / 2)).toEqual([192, 1]);
|
|
210
|
+
expect(nobleBle.write.mock.calls.every(([, , options]) => options?.pacingDelayMs === 0)).toBe(
|
|
211
|
+
true
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('uses the negotiated Noble MTU for Protocol V2 BLE writes', async () => {
|
|
216
|
+
const device = { id: 'mtu-pro2-id', name: 'OneKey Pro 2', mtu: 247 };
|
|
217
|
+
const nobleBle = createNobleBle(device);
|
|
218
|
+
const bleTransport = configureTransport(nobleBle) as any;
|
|
219
|
+
const context = {
|
|
220
|
+
messageName: 'FilesystemFileWrite',
|
|
221
|
+
timeoutMs: 1000,
|
|
222
|
+
highThroughput: true,
|
|
223
|
+
generation: 1,
|
|
224
|
+
signal: new AbortController().signal,
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
|
|
228
|
+
try {
|
|
229
|
+
await bleTransport.refreshBlePacketCapacity(device.id);
|
|
230
|
+
await bleTransport.writeProtocolV2Frame(device.id, new Uint8Array(245), context, jest.fn());
|
|
231
|
+
|
|
232
|
+
expect(setTimeoutSpy).not.toHaveBeenCalled();
|
|
233
|
+
} finally {
|
|
234
|
+
setTimeoutSpy.mockRestore();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
expect(nobleBle.write).toHaveBeenCalledTimes(2);
|
|
238
|
+
expect(nobleBle.write.mock.calls.map(([, hex]) => hex.length / 2)).toEqual([244, 1]);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('updates Protocol V2 packet capacity when Noble reports a new MTU', async () => {
|
|
242
|
+
const device = { id: 'mtu-event-pro2-id', name: 'OneKey Pro 2' };
|
|
243
|
+
const nobleBle = createNobleBle(device);
|
|
244
|
+
let mtuHandler: ((changedDevice: { id: string; mtu: number }) => void) | undefined;
|
|
245
|
+
nobleBle.onMtuChanged.mockImplementation(handler => {
|
|
246
|
+
mtuHandler = handler;
|
|
247
|
+
return jest.fn();
|
|
248
|
+
});
|
|
249
|
+
const bleTransport = configureTransport(nobleBle) as any;
|
|
250
|
+
const context = {
|
|
251
|
+
messageName: 'FilesystemFileWrite',
|
|
252
|
+
timeoutMs: 1000,
|
|
253
|
+
highThroughput: true,
|
|
254
|
+
generation: 1,
|
|
255
|
+
signal: new AbortController().signal,
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
bleTransport.createMtuSubscription(device.id);
|
|
259
|
+
mtuHandler?.({ id: device.id, mtu: 247 });
|
|
260
|
+
await bleTransport.writeProtocolV2Frame(device.id, new Uint8Array(245), context, jest.fn());
|
|
261
|
+
|
|
262
|
+
expect(nobleBle.write.mock.calls.map(([, hex]) => hex.length / 2)).toEqual([244, 1]);
|
|
263
|
+
});
|
|
264
|
+
|
|
129
265
|
test('detects Protocol V2 after Protocol V1 probe timeout', async () => {
|
|
130
266
|
const device = { id: 'unknown-pro2-id', name: 'Unknown BLE Device' };
|
|
131
267
|
const nobleBle = createNobleBle(device);
|
|
@@ -136,7 +272,6 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
136
272
|
{ message: 'ok' },
|
|
137
273
|
{ router: PROTOCOL_V2_CHANNEL_BLE_UART }
|
|
138
274
|
);
|
|
139
|
-
|
|
140
275
|
nobleBle.onNotification.mockImplementation(handler => {
|
|
141
276
|
notificationHandler = handler;
|
|
142
277
|
return jest.fn();
|
|
@@ -164,7 +299,7 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
164
299
|
}
|
|
165
300
|
});
|
|
166
301
|
|
|
167
|
-
test('
|
|
302
|
+
test('reconnects Protocol V1 with a non-destructive GetFeatures probe', async () => {
|
|
168
303
|
const device = { id: 'classic-id', name: 'OneKey Classic' };
|
|
169
304
|
const nobleBle = createNobleBle(device);
|
|
170
305
|
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
@@ -178,11 +313,12 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
178
313
|
return jest.fn();
|
|
179
314
|
});
|
|
180
315
|
nobleBle.write.mockImplementation(() => {
|
|
181
|
-
//
|
|
316
|
+
// The first write is the V1 GetFeatures probe; answer with a V1 Success response.
|
|
182
317
|
setTimeout(() => notificationHandler?.(device.id, v1ResponseHex), 0);
|
|
183
318
|
return Promise.resolve();
|
|
184
319
|
});
|
|
185
320
|
const transport = configureTransport(nobleBle);
|
|
321
|
+
const protocolV2Writer = jest.spyOn(transport as any, 'writeProtocolV2Frame');
|
|
186
322
|
|
|
187
323
|
try {
|
|
188
324
|
await expect(transport.acquire({ uuid: device.id })).resolves.toEqual(
|
|
@@ -191,11 +327,100 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
191
327
|
})
|
|
192
328
|
);
|
|
193
329
|
expect(transport.getProtocolType(device.id)).toBe('V1');
|
|
330
|
+
await expect(transport.acquire({ uuid: device.id, expectedProtocol: 'V1' })).resolves.toEqual(
|
|
331
|
+
expect.objectContaining({
|
|
332
|
+
uuid: device.id,
|
|
333
|
+
})
|
|
334
|
+
);
|
|
335
|
+
expect(nobleBle.write).toHaveBeenCalledTimes(2);
|
|
336
|
+
expect(nobleBle.write.mock.calls.every(([, hex]) => /^3f23230037/.test(hex))).toBe(true);
|
|
337
|
+
expect(protocolV2Writer).not.toHaveBeenCalled();
|
|
194
338
|
} finally {
|
|
195
339
|
await transport.release(device.id);
|
|
196
340
|
}
|
|
197
341
|
});
|
|
198
342
|
|
|
343
|
+
test('invalidates and disconnects a Protocol V1 link after a response timeout', async () => {
|
|
344
|
+
const device = { id: 'classic-timeout-id', name: 'OneKey Classic' };
|
|
345
|
+
const nobleBle = createNobleBle(device);
|
|
346
|
+
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
347
|
+
const v1ResponseHex = '3f23230002000000040a026f6b';
|
|
348
|
+
let writeCount = 0;
|
|
349
|
+
|
|
350
|
+
nobleBle.onNotification.mockImplementation(handler => {
|
|
351
|
+
notificationHandler = handler;
|
|
352
|
+
return jest.fn();
|
|
353
|
+
});
|
|
354
|
+
nobleBle.write.mockImplementation(() => {
|
|
355
|
+
writeCount += 1;
|
|
356
|
+
if (writeCount === 1) {
|
|
357
|
+
setTimeout(() => notificationHandler?.(device.id, v1ResponseHex), 0);
|
|
358
|
+
}
|
|
359
|
+
return Promise.resolve();
|
|
360
|
+
});
|
|
361
|
+
const bleTransport = configureTransport(nobleBle);
|
|
362
|
+
|
|
363
|
+
await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V1' });
|
|
364
|
+
await expect(
|
|
365
|
+
bleTransport.call(device.id, 'Initialize', {}, { timeoutMs: 5 })
|
|
366
|
+
).rejects.toMatchObject({ errorCode: HardwareErrorCode.BleTimeoutError });
|
|
367
|
+
|
|
368
|
+
expect(nobleBle.unsubscribe).toHaveBeenCalledWith(device.id);
|
|
369
|
+
expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
|
|
370
|
+
expect(bleTransport.getProtocolType(device.id)).toBeUndefined();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test('keeps another device V2 reader when force-cleaning a V1 call', async () => {
|
|
374
|
+
const device = { id: 'classic-force-clean-id', name: 'OneKey Classic' };
|
|
375
|
+
const nobleBle = createNobleBle(device);
|
|
376
|
+
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
377
|
+
const v1ResponseHex = '3f23230002000000040a026f6b';
|
|
378
|
+
nobleBle.onNotification.mockImplementation(handler => {
|
|
379
|
+
notificationHandler = handler;
|
|
380
|
+
return jest.fn();
|
|
381
|
+
});
|
|
382
|
+
nobleBle.write.mockImplementation(() => {
|
|
383
|
+
setTimeout(() => notificationHandler?.(device.id, v1ResponseHex), 0);
|
|
384
|
+
return Promise.resolve();
|
|
385
|
+
});
|
|
386
|
+
const bleTransport = configureTransport(nobleBle) as any;
|
|
387
|
+
const activeV1Call = createDeferred<string>();
|
|
388
|
+
const otherDeviceReader = createDeferred<Uint8Array>();
|
|
389
|
+
activeV1Call.promise.catch(() => undefined);
|
|
390
|
+
otherDeviceReader.promise.catch(() => undefined);
|
|
391
|
+
bleTransport.runPromise = activeV1Call;
|
|
392
|
+
bleTransport.v2FramePromises.set('device-b', otherDeviceReader);
|
|
393
|
+
|
|
394
|
+
await bleTransport.acquire({
|
|
395
|
+
uuid: device.id,
|
|
396
|
+
expectedProtocol: 'V1',
|
|
397
|
+
forceCleanRunPromise: true,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
expect(bleTransport.v2FramePromises.get('device-b')).toBe(otherDeviceReader);
|
|
401
|
+
await bleTransport.release(device.id);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test('rejects a pending V2 reader when its device frame state resets', async () => {
|
|
405
|
+
const nobleBle = createNobleBle();
|
|
406
|
+
const bleTransport = configureTransport(nobleBle) as any;
|
|
407
|
+
const reader = createDeferred<Uint8Array>();
|
|
408
|
+
bleTransport.v2FramePromises.set('device-a', reader);
|
|
409
|
+
const result = Promise.race([
|
|
410
|
+
reader.promise.then(
|
|
411
|
+
() => 'resolved',
|
|
412
|
+
() => 'rejected'
|
|
413
|
+
),
|
|
414
|
+
new Promise(resolve => {
|
|
415
|
+
setTimeout(() => resolve('pending'), 20);
|
|
416
|
+
}),
|
|
417
|
+
]);
|
|
418
|
+
|
|
419
|
+
bleTransport.resetProtocolV2Frames('device-a');
|
|
420
|
+
|
|
421
|
+
await expect(result).resolves.toBe('rejected');
|
|
422
|
+
});
|
|
423
|
+
|
|
199
424
|
test('throws when both protocol probes fail', async () => {
|
|
200
425
|
const device = { id: 'dead-device-id', name: 'Unknown Device' };
|
|
201
426
|
const nobleBle = createNobleBle(device);
|
|
@@ -215,19 +440,21 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
215
440
|
const device = { id: 'named-pro2-id', name: 'OneKey Pro 2' };
|
|
216
441
|
const nobleBle = createNobleBle(device);
|
|
217
442
|
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
218
|
-
const probeResponse = ProtocolV2.encodeFrame(
|
|
219
|
-
schemas,
|
|
220
|
-
'Success',
|
|
221
|
-
{ message: 'ok' },
|
|
222
|
-
{ router: PROTOCOL_V2_CHANNEL_BLE_UART }
|
|
223
|
-
);
|
|
224
443
|
|
|
225
444
|
nobleBle.onNotification.mockImplementation(handler => {
|
|
226
445
|
notificationHandler = handler;
|
|
227
446
|
return jest.fn();
|
|
228
447
|
});
|
|
448
|
+
let responseSeq = 0;
|
|
229
449
|
nobleBle.write.mockImplementation(() => {
|
|
230
|
-
|
|
450
|
+
responseSeq += 1;
|
|
451
|
+
const response = ProtocolV2.encodeFrame(
|
|
452
|
+
schemas,
|
|
453
|
+
'Success',
|
|
454
|
+
{ message: 'ok' },
|
|
455
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
|
|
456
|
+
);
|
|
457
|
+
setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0);
|
|
231
458
|
return Promise.resolve();
|
|
232
459
|
});
|
|
233
460
|
const transport = configureTransport(nobleBle);
|
|
@@ -241,8 +468,226 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
241
468
|
);
|
|
242
469
|
expect(nobleBle.write).toHaveBeenCalledTimes(1);
|
|
243
470
|
expect(transport.getProtocolType(device.id)).toBe('V2');
|
|
471
|
+
await expect(transport.call(device.id, 'Ping', { message: 'after-probe' })).resolves.toEqual({
|
|
472
|
+
type: 'Success',
|
|
473
|
+
message: { message: 'ok' },
|
|
474
|
+
});
|
|
475
|
+
const sentSeqs = nobleBle.write.mock.calls.map(([, hex]) =>
|
|
476
|
+
Number.parseInt(hex.slice(12, 14), 16)
|
|
477
|
+
);
|
|
478
|
+
expect(sentSeqs).toEqual([1, 2]);
|
|
244
479
|
} finally {
|
|
245
480
|
await transport.release(device.id);
|
|
246
481
|
}
|
|
247
482
|
});
|
|
483
|
+
|
|
484
|
+
test('rejects the active Protocol V2 reader when pairing is rejected', async () => {
|
|
485
|
+
const device = { id: 'pairing-rejected-pro2-id', name: 'OneKey Pro 2' };
|
|
486
|
+
const nobleBle = createNobleBle(device);
|
|
487
|
+
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
488
|
+
let pairingRejected = false;
|
|
489
|
+
const probeResponse = ProtocolV2.encodeFrame(
|
|
490
|
+
schemas,
|
|
491
|
+
'Success',
|
|
492
|
+
{ message: 'ok' },
|
|
493
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART }
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
nobleBle.onNotification.mockImplementation(handler => {
|
|
497
|
+
notificationHandler = handler;
|
|
498
|
+
return jest.fn();
|
|
499
|
+
});
|
|
500
|
+
nobleBle.write.mockImplementation(() => {
|
|
501
|
+
setTimeout(
|
|
502
|
+
() =>
|
|
503
|
+
notificationHandler?.(
|
|
504
|
+
device.id,
|
|
505
|
+
pairingRejected ? 'PAIRING_REJECTED' : bytesToHex(probeResponse)
|
|
506
|
+
),
|
|
507
|
+
0
|
|
508
|
+
);
|
|
509
|
+
return Promise.resolve();
|
|
510
|
+
});
|
|
511
|
+
const transport = configureTransport(nobleBle);
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
await transport.acquire({ uuid: device.id });
|
|
515
|
+
pairingRejected = true;
|
|
516
|
+
|
|
517
|
+
await expect(
|
|
518
|
+
transport.call(device.id, 'Ping', { message: 'pairing' }, { timeoutMs: 50 })
|
|
519
|
+
).rejects.toMatchObject({ errorCode: HardwareErrorCode.BleDeviceBondedCanceled });
|
|
520
|
+
} finally {
|
|
521
|
+
await transport.release(device.id);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test('rebuilds the active link when Core acquires the same device again', async () => {
|
|
526
|
+
const device = { id: 'repeated-acquire-pro2-id', name: 'OneKey Pro 2' };
|
|
527
|
+
const nobleBle = createNobleBle(device);
|
|
528
|
+
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
529
|
+
nobleBle.onNotification.mockImplementation(handler => {
|
|
530
|
+
notificationHandler = handler;
|
|
531
|
+
return jest.fn();
|
|
532
|
+
});
|
|
533
|
+
let responseSeq = 0;
|
|
534
|
+
nobleBle.write.mockImplementation(() => {
|
|
535
|
+
responseSeq += 1;
|
|
536
|
+
const sequencedResponse = ProtocolV2.encodeFrame(
|
|
537
|
+
schemas,
|
|
538
|
+
'Success',
|
|
539
|
+
{ message: 'ok' },
|
|
540
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
|
|
541
|
+
);
|
|
542
|
+
setTimeout(() => notificationHandler?.(device.id, bytesToHex(sequencedResponse)), 0);
|
|
543
|
+
return Promise.resolve();
|
|
544
|
+
});
|
|
545
|
+
const transport = configureTransport(nobleBle);
|
|
546
|
+
|
|
547
|
+
try {
|
|
548
|
+
await transport.acquire({ uuid: device.id });
|
|
549
|
+
await transport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
|
|
550
|
+
await expect(
|
|
551
|
+
transport.call(device.id, 'Ping', { message: 'after-reacquire' })
|
|
552
|
+
).resolves.toEqual({
|
|
553
|
+
type: 'Success',
|
|
554
|
+
message: { message: 'ok' },
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
const sentSeqs = nobleBle.write.mock.calls.map(([, hex]) =>
|
|
558
|
+
Number.parseInt(hex.slice(12, 14), 16)
|
|
559
|
+
);
|
|
560
|
+
expect(sentSeqs).toEqual([1, 2, 3]);
|
|
561
|
+
} finally {
|
|
562
|
+
await transport.release(device.id);
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test('ignores a delayed disconnect event from the previous BLE connection', async () => {
|
|
567
|
+
const device = { id: 'delayed-disconnect-pro2-id', name: 'OneKey Pro 2' };
|
|
568
|
+
const nobleBle = createNobleBle(device);
|
|
569
|
+
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
570
|
+
const disconnectHandlers: Array<
|
|
571
|
+
(disconnectedDevice: { id: string; name: string | null }) => void
|
|
572
|
+
> = [];
|
|
573
|
+
nobleBle.onNotification.mockImplementation(handler => {
|
|
574
|
+
notificationHandler = handler;
|
|
575
|
+
return jest.fn();
|
|
576
|
+
});
|
|
577
|
+
nobleBle.onDeviceDisconnected.mockImplementation(handler => {
|
|
578
|
+
disconnectHandlers.push(handler);
|
|
579
|
+
return jest.fn();
|
|
580
|
+
});
|
|
581
|
+
let responseSeq = 0;
|
|
582
|
+
nobleBle.write.mockImplementation(() => {
|
|
583
|
+
responseSeq += 1;
|
|
584
|
+
const response = ProtocolV2.encodeFrame(
|
|
585
|
+
schemas,
|
|
586
|
+
'Success',
|
|
587
|
+
{ message: 'ok' },
|
|
588
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
|
|
589
|
+
);
|
|
590
|
+
setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0);
|
|
591
|
+
return Promise.resolve();
|
|
592
|
+
});
|
|
593
|
+
const bleTransport = configureTransport(nobleBle);
|
|
594
|
+
|
|
595
|
+
try {
|
|
596
|
+
await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
|
|
597
|
+
await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
|
|
598
|
+
|
|
599
|
+
disconnectHandlers[0]?.(device);
|
|
600
|
+
|
|
601
|
+
expect(bleTransport.getProtocolType(device.id)).toBe('V2');
|
|
602
|
+
await expect(
|
|
603
|
+
bleTransport.call(device.id, 'Ping', { message: 'after-stale-disconnect' })
|
|
604
|
+
).resolves.toEqual({
|
|
605
|
+
type: 'Success',
|
|
606
|
+
message: { message: 'ok' },
|
|
607
|
+
});
|
|
608
|
+
} finally {
|
|
609
|
+
await bleTransport.release(device.id);
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
test('preserves the active Protocol V2 link when the same schema is configured again', async () => {
|
|
614
|
+
const device = { id: 'stable-schema-pro2-id', name: 'OneKey Pro 2' };
|
|
615
|
+
const nobleBle = createNobleBle(device);
|
|
616
|
+
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
617
|
+
nobleBle.onNotification.mockImplementation(handler => {
|
|
618
|
+
notificationHandler = handler;
|
|
619
|
+
return jest.fn();
|
|
620
|
+
});
|
|
621
|
+
let responseSeq = 0;
|
|
622
|
+
nobleBle.write.mockImplementation(() => {
|
|
623
|
+
responseSeq += 1;
|
|
624
|
+
const response = ProtocolV2.encodeFrame(
|
|
625
|
+
schemas,
|
|
626
|
+
'Success',
|
|
627
|
+
{ message: 'ok' },
|
|
628
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
|
|
629
|
+
);
|
|
630
|
+
setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0);
|
|
631
|
+
return Promise.resolve();
|
|
632
|
+
});
|
|
633
|
+
const bleTransport = configureTransport(nobleBle);
|
|
634
|
+
|
|
635
|
+
try {
|
|
636
|
+
await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
|
|
637
|
+
const invalidateAllLinks = jest.spyOn(
|
|
638
|
+
(bleTransport as any).protocolV2Links,
|
|
639
|
+
'invalidateAllLinks'
|
|
640
|
+
);
|
|
641
|
+
bleTransport.configureProtocolV2(protocolV2Schema);
|
|
642
|
+
await new Promise<void>(resolve => {
|
|
643
|
+
setTimeout(resolve, 0);
|
|
644
|
+
});
|
|
645
|
+
expect(invalidateAllLinks).not.toHaveBeenCalled();
|
|
646
|
+
await expect(
|
|
647
|
+
bleTransport.call(device.id, 'Ping', { message: 'same-schema' })
|
|
648
|
+
).resolves.toEqual({
|
|
649
|
+
type: 'Success',
|
|
650
|
+
message: { message: 'ok' },
|
|
651
|
+
});
|
|
652
|
+
const sentSeqs = nobleBle.write.mock.calls.map(([, hex]) =>
|
|
653
|
+
Number.parseInt(hex.slice(12, 14), 16)
|
|
654
|
+
);
|
|
655
|
+
expect(sentSeqs).toEqual([1, 2]);
|
|
656
|
+
} finally {
|
|
657
|
+
await bleTransport.release(device.id);
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
test('rejects oversized Protocol V2 requests before writing to Electron BLE', async () => {
|
|
662
|
+
const device = { id: 'oversized-frame-pro2-id', name: 'OneKey Pro 2' };
|
|
663
|
+
const nobleBle = createNobleBle(device);
|
|
664
|
+
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
665
|
+
const probeResponse = ProtocolV2.encodeFrame(
|
|
666
|
+
schemas,
|
|
667
|
+
'Success',
|
|
668
|
+
{ message: 'ok' },
|
|
669
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART }
|
|
670
|
+
);
|
|
671
|
+
nobleBle.onNotification.mockImplementation(handler => {
|
|
672
|
+
notificationHandler = handler;
|
|
673
|
+
return jest.fn();
|
|
674
|
+
});
|
|
675
|
+
nobleBle.write.mockImplementation(() => {
|
|
676
|
+
setTimeout(() => notificationHandler?.(device.id, bytesToHex(probeResponse)), 0);
|
|
677
|
+
return Promise.resolve();
|
|
678
|
+
});
|
|
679
|
+
const bleTransport = configureTransport(nobleBle);
|
|
680
|
+
|
|
681
|
+
try {
|
|
682
|
+
await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
|
|
683
|
+
expect(nobleBle.write).toHaveBeenCalledTimes(1);
|
|
684
|
+
|
|
685
|
+
await expect(
|
|
686
|
+
bleTransport.call(device.id, 'Ping', { message: 'x'.repeat(2048) })
|
|
687
|
+
).rejects.toThrow(/Protocol V2 frame too large for transport/);
|
|
688
|
+
expect(nobleBle.write).toHaveBeenCalledTimes(1);
|
|
689
|
+
} finally {
|
|
690
|
+
await bleTransport.release(device.id);
|
|
691
|
+
}
|
|
692
|
+
});
|
|
248
693
|
});
|