@onekeyfe/hd-transport-web-device 1.2.0-alpha.18 → 1.2.0-alpha.180
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 +548 -27
- package/__tests__/webusb-protocol-cache.test.ts +276 -0
- package/__tests__/webusb-protocol-v2-timeout.test.ts +370 -11
- 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 -4
- package/dist/electron-ble-transport.d.ts.map +1 -1
- package/dist/index.d.ts +171 -15
- package/dist/index.js +472 -301
- package/dist/transportLog.d.ts +1 -6
- package/dist/transportLog.d.ts.map +1 -1
- package/dist/webusb.d.ts +26 -11
- 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 +303 -135
- package/src/transportLog.ts +1 -11
- package/src/webusb.ts +359 -205
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import transport, { PROTOCOL_V2_CHANNEL_BLE_UART, bytesToHex } from '@onekeyfe/hd-transport';
|
|
2
|
-
import { HardwareErrorCode } from '@onekeyfe/hd-shared';
|
|
2
|
+
import { EBleDisconnectReason, HardwareErrorCode, createDeferred } from '@onekeyfe/hd-shared';
|
|
3
|
+
import EventEmitter from 'events';
|
|
3
4
|
|
|
4
5
|
import ElectronBleTransport from '../src/electron-ble-transport';
|
|
5
6
|
|
|
@@ -10,6 +11,9 @@ const protocolV1Schema = {
|
|
|
10
11
|
Initialize: {
|
|
11
12
|
fields: {},
|
|
12
13
|
},
|
|
14
|
+
GetFeatures: {
|
|
15
|
+
fields: {},
|
|
16
|
+
},
|
|
13
17
|
Success: {
|
|
14
18
|
fields: {
|
|
15
19
|
message: {
|
|
@@ -22,6 +26,7 @@ const protocolV1Schema = {
|
|
|
22
26
|
values: {
|
|
23
27
|
MessageType_Initialize: 1,
|
|
24
28
|
MessageType_Success: 2,
|
|
29
|
+
MessageType_GetFeatures: 55,
|
|
25
30
|
},
|
|
26
31
|
},
|
|
27
32
|
},
|
|
@@ -100,6 +105,7 @@ const createNobleBle = (device = { id: 'flaky-pro2-id', name: 'Unknown BLE Devic
|
|
|
100
105
|
unsubscribe: jest.fn(() => Promise.resolve()),
|
|
101
106
|
write: jest.fn(() => Promise.resolve()),
|
|
102
107
|
onNotification: jest.fn(() => jest.fn()),
|
|
108
|
+
onMtuChanged: jest.fn(() => jest.fn()),
|
|
103
109
|
onDeviceDisconnected: jest.fn(() => jest.fn()),
|
|
104
110
|
checkAvailability: jest.fn(() =>
|
|
105
111
|
Promise.resolve({
|
|
@@ -111,7 +117,10 @@ const createNobleBle = (device = { id: 'flaky-pro2-id', name: 'Unknown BLE Devic
|
|
|
111
117
|
),
|
|
112
118
|
});
|
|
113
119
|
|
|
114
|
-
const configureTransport = (
|
|
120
|
+
const configureTransport = (
|
|
121
|
+
nobleBle: ReturnType<typeof createNobleBle>,
|
|
122
|
+
emitter?: EventEmitter
|
|
123
|
+
) => {
|
|
115
124
|
(global as any).window = {
|
|
116
125
|
desktopApi: {
|
|
117
126
|
nobleBle,
|
|
@@ -119,18 +128,164 @@ const configureTransport = (nobleBle: ReturnType<typeof createNobleBle>) => {
|
|
|
119
128
|
};
|
|
120
129
|
|
|
121
130
|
const transport = new ElectronBleTransport();
|
|
122
|
-
transport.init(createLogger());
|
|
131
|
+
transport.init(createLogger(), emitter);
|
|
123
132
|
transport.configure(protocolV1Schema);
|
|
124
133
|
transport.configureProtocolV2(protocolV2Schema);
|
|
125
134
|
return transport;
|
|
126
135
|
};
|
|
127
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Wire the mock so acquire()'s Protocol V2 probe gets an answer. Without it
|
|
139
|
+
* detectProtocol throws and the test never reaches the disconnect behaviour.
|
|
140
|
+
*/
|
|
141
|
+
const echoProtocolV2 = (nobleBle: ReturnType<typeof createNobleBle>, deviceId: string) => {
|
|
142
|
+
let notificationHandler: ((id: string, data: string) => void) | undefined;
|
|
143
|
+
nobleBle.onNotification.mockImplementation(handler => {
|
|
144
|
+
notificationHandler = handler;
|
|
145
|
+
return jest.fn();
|
|
146
|
+
});
|
|
147
|
+
let responseSeq = 0;
|
|
148
|
+
nobleBle.write.mockImplementation(() => {
|
|
149
|
+
responseSeq += 1;
|
|
150
|
+
const response = ProtocolV2.encodeFrame(
|
|
151
|
+
schemas,
|
|
152
|
+
'Success',
|
|
153
|
+
{ message: 'ok' },
|
|
154
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
|
|
155
|
+
);
|
|
156
|
+
setTimeout(() => notificationHandler?.(deviceId, bytesToHex(response)), 0);
|
|
157
|
+
return Promise.resolve();
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
|
|
128
161
|
describe('ElectronBleTransport protocol detection', () => {
|
|
129
162
|
afterEach(() => {
|
|
130
163
|
delete (global as any).window;
|
|
131
164
|
jest.clearAllMocks();
|
|
132
165
|
});
|
|
133
166
|
|
|
167
|
+
test('keeps raw BLE lifecycle payloads off the public device event channel', async () => {
|
|
168
|
+
const device = { id: 'lifecycle-pro2-id', name: 'OneKey Pro 2' };
|
|
169
|
+
const nobleBle = createNobleBle(device);
|
|
170
|
+
const emitter = new EventEmitter();
|
|
171
|
+
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
172
|
+
let disconnectHandler: ((device: { id: string; name: string | null }) => void) | undefined;
|
|
173
|
+
let responseSeq = 0;
|
|
174
|
+
|
|
175
|
+
nobleBle.onNotification.mockImplementation(handler => {
|
|
176
|
+
notificationHandler = handler;
|
|
177
|
+
return jest.fn();
|
|
178
|
+
});
|
|
179
|
+
nobleBle.onDeviceDisconnected.mockImplementation(handler => {
|
|
180
|
+
disconnectHandler = handler;
|
|
181
|
+
return jest.fn();
|
|
182
|
+
});
|
|
183
|
+
nobleBle.write.mockImplementation(() => {
|
|
184
|
+
responseSeq += 1;
|
|
185
|
+
const response = ProtocolV2.encodeFrame(
|
|
186
|
+
schemas,
|
|
187
|
+
'Success',
|
|
188
|
+
{ message: 'ok' },
|
|
189
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
|
|
190
|
+
);
|
|
191
|
+
setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0);
|
|
192
|
+
return Promise.resolve();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const publicConnect = jest.fn();
|
|
196
|
+
const publicDisconnect = jest.fn();
|
|
197
|
+
const transportDisconnect = jest.fn();
|
|
198
|
+
emitter.on('device-connect', publicConnect);
|
|
199
|
+
emitter.on('device-disconnect', publicDisconnect);
|
|
200
|
+
emitter.on('transport-device-disconnect', transportDisconnect);
|
|
201
|
+
const bleTransport = configureTransport(nobleBle, emitter);
|
|
202
|
+
|
|
203
|
+
await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
|
|
204
|
+
expect(nobleBle.subscribe.mock.invocationCallOrder[0]).toBeLessThan(
|
|
205
|
+
nobleBle.getDevice.mock.invocationCallOrder[1]
|
|
206
|
+
);
|
|
207
|
+
disconnectHandler?.(device);
|
|
208
|
+
|
|
209
|
+
expect(publicConnect).not.toHaveBeenCalled();
|
|
210
|
+
expect(publicDisconnect).not.toHaveBeenCalled();
|
|
211
|
+
expect(transportDisconnect).toHaveBeenCalledWith({
|
|
212
|
+
id: device.id,
|
|
213
|
+
connectId: device.id,
|
|
214
|
+
name: device.name,
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('uses the Protocol V2 BLE writer with the Electron packet size', async () => {
|
|
219
|
+
const device = { id: 'chunked-pro2-id', name: 'OneKey Pro 2' };
|
|
220
|
+
const nobleBle = createNobleBle(device);
|
|
221
|
+
const bleTransport = configureTransport(nobleBle) as any;
|
|
222
|
+
const context = {
|
|
223
|
+
messageName: 'Ping',
|
|
224
|
+
timeoutMs: 1000,
|
|
225
|
+
highThroughput: false,
|
|
226
|
+
generation: 1,
|
|
227
|
+
signal: new AbortController().signal,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
await bleTransport.writeProtocolV2Frame(device.id, new Uint8Array(193), context, jest.fn());
|
|
231
|
+
|
|
232
|
+
expect(nobleBle.write).toHaveBeenCalledTimes(2);
|
|
233
|
+
expect(nobleBle.write.mock.calls.map(([, hex]) => hex.length / 2)).toEqual([192, 1]);
|
|
234
|
+
expect(nobleBle.write.mock.calls.every(([, , options]) => options?.pacingDelayMs === 0)).toBe(
|
|
235
|
+
true
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('uses the negotiated Noble MTU for Protocol V2 BLE writes', async () => {
|
|
240
|
+
const device = { id: 'mtu-pro2-id', name: 'OneKey Pro 2', mtu: 247 };
|
|
241
|
+
const nobleBle = createNobleBle(device);
|
|
242
|
+
const bleTransport = configureTransport(nobleBle) as any;
|
|
243
|
+
const context = {
|
|
244
|
+
messageName: 'FilesystemFileWrite',
|
|
245
|
+
timeoutMs: 1000,
|
|
246
|
+
highThroughput: true,
|
|
247
|
+
generation: 1,
|
|
248
|
+
signal: new AbortController().signal,
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
|
|
252
|
+
try {
|
|
253
|
+
await bleTransport.refreshBlePacketCapacity(device.id);
|
|
254
|
+
await bleTransport.writeProtocolV2Frame(device.id, new Uint8Array(245), context, jest.fn());
|
|
255
|
+
|
|
256
|
+
expect(setTimeoutSpy).not.toHaveBeenCalled();
|
|
257
|
+
} finally {
|
|
258
|
+
setTimeoutSpy.mockRestore();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
expect(nobleBle.write).toHaveBeenCalledTimes(2);
|
|
262
|
+
expect(nobleBle.write.mock.calls.map(([, hex]) => hex.length / 2)).toEqual([244, 1]);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('updates Protocol V2 packet capacity when Noble reports a new MTU', async () => {
|
|
266
|
+
const device = { id: 'mtu-event-pro2-id', name: 'OneKey Pro 2' };
|
|
267
|
+
const nobleBle = createNobleBle(device);
|
|
268
|
+
let mtuHandler: ((changedDevice: { id: string; mtu: number }) => void) | undefined;
|
|
269
|
+
nobleBle.onMtuChanged.mockImplementation(handler => {
|
|
270
|
+
mtuHandler = handler;
|
|
271
|
+
return jest.fn();
|
|
272
|
+
});
|
|
273
|
+
const bleTransport = configureTransport(nobleBle) as any;
|
|
274
|
+
const context = {
|
|
275
|
+
messageName: 'FilesystemFileWrite',
|
|
276
|
+
timeoutMs: 1000,
|
|
277
|
+
highThroughput: true,
|
|
278
|
+
generation: 1,
|
|
279
|
+
signal: new AbortController().signal,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
bleTransport.createMtuSubscription(device.id);
|
|
283
|
+
mtuHandler?.({ id: device.id, mtu: 247 });
|
|
284
|
+
await bleTransport.writeProtocolV2Frame(device.id, new Uint8Array(245), context, jest.fn());
|
|
285
|
+
|
|
286
|
+
expect(nobleBle.write.mock.calls.map(([, hex]) => hex.length / 2)).toEqual([244, 1]);
|
|
287
|
+
});
|
|
288
|
+
|
|
134
289
|
test('detects Protocol V2 after Protocol V1 probe timeout', async () => {
|
|
135
290
|
const device = { id: 'unknown-pro2-id', name: 'Unknown BLE Device' };
|
|
136
291
|
const nobleBle = createNobleBle(device);
|
|
@@ -141,7 +296,6 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
141
296
|
{ message: 'ok' },
|
|
142
297
|
{ router: PROTOCOL_V2_CHANNEL_BLE_UART }
|
|
143
298
|
);
|
|
144
|
-
|
|
145
299
|
nobleBle.onNotification.mockImplementation(handler => {
|
|
146
300
|
notificationHandler = handler;
|
|
147
301
|
return jest.fn();
|
|
@@ -164,12 +318,16 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
164
318
|
})
|
|
165
319
|
);
|
|
166
320
|
expect(transport.getProtocolType(device.id)).toBe('V2');
|
|
321
|
+
expect(nobleBle.connect).toHaveBeenCalledTimes(1);
|
|
322
|
+
expect(nobleBle.subscribe).toHaveBeenCalledTimes(1);
|
|
323
|
+
expect(nobleBle.unsubscribe).not.toHaveBeenCalled();
|
|
324
|
+
expect(nobleBle.disconnect).not.toHaveBeenCalled();
|
|
167
325
|
} finally {
|
|
168
326
|
await transport.release(device.id);
|
|
169
327
|
}
|
|
170
328
|
});
|
|
171
329
|
|
|
172
|
-
test('
|
|
330
|
+
test('reconnects a declared Protocol V1 device without probing again', async () => {
|
|
173
331
|
const device = { id: 'classic-id', name: 'OneKey Classic' };
|
|
174
332
|
const nobleBle = createNobleBle(device);
|
|
175
333
|
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
@@ -183,11 +341,12 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
183
341
|
return jest.fn();
|
|
184
342
|
});
|
|
185
343
|
nobleBle.write.mockImplementation(() => {
|
|
186
|
-
//
|
|
344
|
+
// The first write is the V1 GetFeatures probe; answer with a V1 Success response.
|
|
187
345
|
setTimeout(() => notificationHandler?.(device.id, v1ResponseHex), 0);
|
|
188
346
|
return Promise.resolve();
|
|
189
347
|
});
|
|
190
348
|
const transport = configureTransport(nobleBle);
|
|
349
|
+
const protocolV2Writer = jest.spyOn(transport as any, 'writeProtocolV2Frame');
|
|
191
350
|
|
|
192
351
|
try {
|
|
193
352
|
await expect(transport.acquire({ uuid: device.id })).resolves.toEqual(
|
|
@@ -196,11 +355,97 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
196
355
|
})
|
|
197
356
|
);
|
|
198
357
|
expect(transport.getProtocolType(device.id)).toBe('V1');
|
|
358
|
+
await expect(transport.acquire({ uuid: device.id, expectedProtocol: 'V1' })).resolves.toEqual(
|
|
359
|
+
expect.objectContaining({
|
|
360
|
+
uuid: device.id,
|
|
361
|
+
})
|
|
362
|
+
);
|
|
363
|
+
// The first acquire probes because the protocol is unknown; the second
|
|
364
|
+
// declares V1 and must send nothing at all, leaving the first frame on
|
|
365
|
+
// the link to Core — which is what carries the wallet session.
|
|
366
|
+
expect(nobleBle.write).toHaveBeenCalledTimes(1);
|
|
367
|
+
expect(nobleBle.write.mock.calls.every(([, hex]) => /^3f23230037/.test(hex))).toBe(true);
|
|
368
|
+
expect(protocolV2Writer).not.toHaveBeenCalled();
|
|
199
369
|
} finally {
|
|
200
370
|
await transport.release(device.id);
|
|
201
371
|
}
|
|
202
372
|
});
|
|
203
373
|
|
|
374
|
+
test('invalidates and disconnects a Protocol V1 link after a response timeout', async () => {
|
|
375
|
+
const device = { id: 'classic-timeout-id', name: 'OneKey Classic' };
|
|
376
|
+
const nobleBle = createNobleBle(device);
|
|
377
|
+
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
378
|
+
const v1ResponseHex = '3f23230002000000040a026f6b';
|
|
379
|
+
nobleBle.onNotification.mockImplementation(handler => {
|
|
380
|
+
notificationHandler = handler;
|
|
381
|
+
return jest.fn();
|
|
382
|
+
});
|
|
383
|
+
// A declared V1 acquire writes nothing, so every write here belongs to the
|
|
384
|
+
// call under test and none of them is answered.
|
|
385
|
+
nobleBle.write.mockImplementation(() => Promise.resolve());
|
|
386
|
+
const bleTransport = configureTransport(nobleBle);
|
|
387
|
+
|
|
388
|
+
await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V1' });
|
|
389
|
+
await expect(
|
|
390
|
+
bleTransport.call(device.id, 'Initialize', {}, { timeoutMs: 5 })
|
|
391
|
+
).rejects.toMatchObject({ errorCode: HardwareErrorCode.BleTimeoutError });
|
|
392
|
+
|
|
393
|
+
expect(nobleBle.unsubscribe).toHaveBeenCalledWith(device.id);
|
|
394
|
+
expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
|
|
395
|
+
expect(bleTransport.getProtocolType(device.id)).toBeUndefined();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
test('keeps another device V2 reader when force-cleaning a V1 call', async () => {
|
|
399
|
+
const device = { id: 'classic-force-clean-id', name: 'OneKey Classic' };
|
|
400
|
+
const nobleBle = createNobleBle(device);
|
|
401
|
+
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
402
|
+
const v1ResponseHex = '3f23230002000000040a026f6b';
|
|
403
|
+
nobleBle.onNotification.mockImplementation(handler => {
|
|
404
|
+
notificationHandler = handler;
|
|
405
|
+
return jest.fn();
|
|
406
|
+
});
|
|
407
|
+
nobleBle.write.mockImplementation(() => {
|
|
408
|
+
setTimeout(() => notificationHandler?.(device.id, v1ResponseHex), 0);
|
|
409
|
+
return Promise.resolve();
|
|
410
|
+
});
|
|
411
|
+
const bleTransport = configureTransport(nobleBle) as any;
|
|
412
|
+
const activeV1Call = createDeferred<string>();
|
|
413
|
+
const otherDeviceReader = createDeferred<Uint8Array>();
|
|
414
|
+
activeV1Call.promise.catch(() => undefined);
|
|
415
|
+
otherDeviceReader.promise.catch(() => undefined);
|
|
416
|
+
bleTransport.runPromise = activeV1Call;
|
|
417
|
+
bleTransport.v2FramePromises.set('device-b', otherDeviceReader);
|
|
418
|
+
|
|
419
|
+
await bleTransport.acquire({
|
|
420
|
+
uuid: device.id,
|
|
421
|
+
expectedProtocol: 'V1',
|
|
422
|
+
forceCleanRunPromise: true,
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
expect(bleTransport.v2FramePromises.get('device-b')).toBe(otherDeviceReader);
|
|
426
|
+
await bleTransport.release(device.id);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test('rejects a pending V2 reader when its device frame state resets', async () => {
|
|
430
|
+
const nobleBle = createNobleBle();
|
|
431
|
+
const bleTransport = configureTransport(nobleBle) as any;
|
|
432
|
+
const reader = createDeferred<Uint8Array>();
|
|
433
|
+
bleTransport.v2FramePromises.set('device-a', reader);
|
|
434
|
+
const result = Promise.race([
|
|
435
|
+
reader.promise.then(
|
|
436
|
+
() => 'resolved',
|
|
437
|
+
() => 'rejected'
|
|
438
|
+
),
|
|
439
|
+
new Promise(resolve => {
|
|
440
|
+
setTimeout(() => resolve('pending'), 20);
|
|
441
|
+
}),
|
|
442
|
+
]);
|
|
443
|
+
|
|
444
|
+
bleTransport.resetProtocolV2Frames('device-a');
|
|
445
|
+
|
|
446
|
+
await expect(result).resolves.toBe('rejected');
|
|
447
|
+
});
|
|
448
|
+
|
|
204
449
|
test('throws when both protocol probes fail', async () => {
|
|
205
450
|
const device = { id: 'dead-device-id', name: 'Unknown Device' };
|
|
206
451
|
const nobleBle = createNobleBle(device);
|
|
@@ -216,23 +461,82 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
216
461
|
expect(transport.getProtocolType(device.id)).toBeUndefined();
|
|
217
462
|
});
|
|
218
463
|
|
|
219
|
-
test('
|
|
464
|
+
test('keeps a first expected Protocol V2 probe miss retryable', async () => {
|
|
465
|
+
const device = { id: 'first-v2-id', name: 'OneKey Pro 2' };
|
|
466
|
+
const nobleBle = createNobleBle(device);
|
|
467
|
+
const transport = configureTransport(nobleBle);
|
|
468
|
+
jest.spyOn(transport as any, 'probeProtocolV2').mockResolvedValue(false);
|
|
469
|
+
|
|
470
|
+
await expect(
|
|
471
|
+
transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
|
|
472
|
+
).rejects.toMatchObject({
|
|
473
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
expect(nobleBle.connect).toHaveBeenCalledTimes(1);
|
|
477
|
+
expect(transport.getProtocolType(device.id)).toBeUndefined();
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test('keeps a second expected Protocol V2 probe miss retryable', async () => {
|
|
481
|
+
const device = { id: 'retry-pro2-id', name: 'OneKey Pro 2' };
|
|
482
|
+
const nobleBle = createNobleBle(device);
|
|
483
|
+
const transport = configureTransport(nobleBle);
|
|
484
|
+
jest.spyOn(transport as any, 'probeProtocolV2').mockResolvedValue(false);
|
|
485
|
+
|
|
486
|
+
await expect(
|
|
487
|
+
transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
|
|
488
|
+
).rejects.toMatchObject({
|
|
489
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
490
|
+
});
|
|
491
|
+
await expect(
|
|
492
|
+
transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
|
|
493
|
+
).rejects.toMatchObject({
|
|
494
|
+
errorCode: HardwareErrorCode.RuntimeError,
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
expect(transport.getProtocolType(device.id)).toBeUndefined();
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test('reports a stale bond when a previously confirmed Protocol V2 device stops responding', async () => {
|
|
501
|
+
const device = { id: 'reset-pro2-id', name: 'OneKey Pro 2' };
|
|
502
|
+
const nobleBle = createNobleBle(device);
|
|
503
|
+
const transport = configureTransport(nobleBle);
|
|
504
|
+
const probe = jest.spyOn(transport as any, 'probeProtocolV2').mockResolvedValue(true);
|
|
505
|
+
|
|
506
|
+
await transport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
|
|
507
|
+
await transport.release(device.id);
|
|
508
|
+
probe.mockResolvedValue(false);
|
|
509
|
+
|
|
510
|
+
await expect(
|
|
511
|
+
transport.acquire({ uuid: device.id, expectedProtocol: 'V2' })
|
|
512
|
+
).rejects.toMatchObject({
|
|
513
|
+
errorCode: HardwareErrorCode.BleDeviceBondError,
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
expect(nobleBle.unsubscribe).toHaveBeenCalledWith(device.id);
|
|
517
|
+
expect(nobleBle.disconnect).toHaveBeenCalledWith(device.id);
|
|
518
|
+
expect(transport.getProtocolType(device.id)).toBeUndefined();
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test('does not take a Protocol V2 hint from the BLE name', async () => {
|
|
220
522
|
const device = { id: 'named-pro2-id', name: 'OneKey Pro 2' };
|
|
221
523
|
const nobleBle = createNobleBle(device);
|
|
222
524
|
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
223
|
-
const probeResponse = ProtocolV2.encodeFrame(
|
|
224
|
-
schemas,
|
|
225
|
-
'Success',
|
|
226
|
-
{ message: 'ok' },
|
|
227
|
-
{ router: PROTOCOL_V2_CHANNEL_BLE_UART }
|
|
228
|
-
);
|
|
229
525
|
|
|
230
526
|
nobleBle.onNotification.mockImplementation(handler => {
|
|
231
527
|
notificationHandler = handler;
|
|
232
528
|
return jest.fn();
|
|
233
529
|
});
|
|
530
|
+
let responseSeq = 0;
|
|
234
531
|
nobleBle.write.mockImplementation(() => {
|
|
235
|
-
|
|
532
|
+
responseSeq += 1;
|
|
533
|
+
const response = ProtocolV2.encodeFrame(
|
|
534
|
+
schemas,
|
|
535
|
+
'Success',
|
|
536
|
+
{ message: 'ok' },
|
|
537
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
|
|
538
|
+
);
|
|
539
|
+
setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0);
|
|
236
540
|
return Promise.resolve();
|
|
237
541
|
});
|
|
238
542
|
const transport = configureTransport(nobleBle);
|
|
@@ -244,15 +548,15 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
244
548
|
protocolType: 'V2',
|
|
245
549
|
})
|
|
246
550
|
);
|
|
247
|
-
expect(nobleBle.write).
|
|
551
|
+
expect(nobleBle.write.mock.calls.length).toBeGreaterThan(1);
|
|
248
552
|
expect(transport.getProtocolType(device.id)).toBe('V2');
|
|
249
553
|
await expect(transport.call(device.id, 'Ping', { message: 'after-probe' })).resolves.toEqual({
|
|
250
554
|
type: 'Success',
|
|
251
555
|
message: { message: 'ok' },
|
|
252
556
|
});
|
|
253
|
-
const sentSeqs = nobleBle.write.mock.calls
|
|
254
|
-
Number.parseInt(hex.slice(12, 14), 16)
|
|
255
|
-
|
|
557
|
+
const sentSeqs = nobleBle.write.mock.calls
|
|
558
|
+
.map(([, hex]) => Number.parseInt(hex.slice(12, 14), 16))
|
|
559
|
+
.filter(seq => seq > 0);
|
|
256
560
|
expect(sentSeqs).toEqual([1, 2]);
|
|
257
561
|
} finally {
|
|
258
562
|
await transport.release(device.id);
|
|
@@ -304,19 +608,20 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
304
608
|
const device = { id: 'repeated-acquire-pro2-id', name: 'OneKey Pro 2' };
|
|
305
609
|
const nobleBle = createNobleBle(device);
|
|
306
610
|
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
307
|
-
const response = ProtocolV2.encodeFrame(
|
|
308
|
-
schemas,
|
|
309
|
-
'Success',
|
|
310
|
-
{ message: 'ok' },
|
|
311
|
-
{ router: PROTOCOL_V2_CHANNEL_BLE_UART }
|
|
312
|
-
);
|
|
313
|
-
|
|
314
611
|
nobleBle.onNotification.mockImplementation(handler => {
|
|
315
612
|
notificationHandler = handler;
|
|
316
613
|
return jest.fn();
|
|
317
614
|
});
|
|
615
|
+
let responseSeq = 0;
|
|
318
616
|
nobleBle.write.mockImplementation(() => {
|
|
319
|
-
|
|
617
|
+
responseSeq += 1;
|
|
618
|
+
const sequencedResponse = ProtocolV2.encodeFrame(
|
|
619
|
+
schemas,
|
|
620
|
+
'Success',
|
|
621
|
+
{ message: 'ok' },
|
|
622
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
|
|
623
|
+
);
|
|
624
|
+
setTimeout(() => notificationHandler?.(device.id, bytesToHex(sequencedResponse)), 0);
|
|
320
625
|
return Promise.resolve();
|
|
321
626
|
});
|
|
322
627
|
const transport = configureTransport(nobleBle);
|
|
@@ -331,12 +636,228 @@ describe('ElectronBleTransport protocol detection', () => {
|
|
|
331
636
|
message: { message: 'ok' },
|
|
332
637
|
});
|
|
333
638
|
|
|
639
|
+
const sentSeqs = nobleBle.write.mock.calls
|
|
640
|
+
.map(([, hex]) => Number.parseInt(hex.slice(12, 14), 16))
|
|
641
|
+
.filter(seq => seq > 0);
|
|
642
|
+
expect(sentSeqs).toEqual([1, 2, 3]);
|
|
643
|
+
} finally {
|
|
644
|
+
await transport.release(device.id);
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test('subscribes to host disconnects once for the transport lifetime', async () => {
|
|
649
|
+
// The previous design registered a listener inside every acquire() and
|
|
650
|
+
// overwrote the cleanup entry without disposing the old one, so each
|
|
651
|
+
// acquire leaked a live handler and a delayed event from a superseded
|
|
652
|
+
// connection could tear down the current one. One transport-lifetime
|
|
653
|
+
// subscription removes that whole class of bug.
|
|
654
|
+
const device = { id: 'single-subscription-pro2-id', name: 'OneKey Pro 2' };
|
|
655
|
+
const nobleBle = createNobleBle(device);
|
|
656
|
+
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
657
|
+
nobleBle.onNotification.mockImplementation(handler => {
|
|
658
|
+
notificationHandler = handler;
|
|
659
|
+
return jest.fn();
|
|
660
|
+
});
|
|
661
|
+
let responseSeq = 0;
|
|
662
|
+
nobleBle.write.mockImplementation(() => {
|
|
663
|
+
responseSeq += 1;
|
|
664
|
+
const response = ProtocolV2.encodeFrame(
|
|
665
|
+
schemas,
|
|
666
|
+
'Success',
|
|
667
|
+
{ message: 'ok' },
|
|
668
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
|
|
669
|
+
);
|
|
670
|
+
setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0);
|
|
671
|
+
return Promise.resolve();
|
|
672
|
+
});
|
|
673
|
+
const bleTransport = configureTransport(nobleBle);
|
|
674
|
+
|
|
675
|
+
try {
|
|
676
|
+
expect(nobleBle.onDeviceDisconnected).toHaveBeenCalledTimes(1);
|
|
677
|
+
|
|
678
|
+
await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
|
|
679
|
+
await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
|
|
680
|
+
|
|
681
|
+
expect(nobleBle.onDeviceDisconnected).toHaveBeenCalledTimes(1);
|
|
682
|
+
expect(bleTransport.getProtocolType(device.id)).toBe('V2');
|
|
683
|
+
await expect(
|
|
684
|
+
bleTransport.call(device.id, 'Ping', { message: 'after-reacquire' })
|
|
685
|
+
).resolves.toEqual({
|
|
686
|
+
type: 'Success',
|
|
687
|
+
message: { message: 'ok' },
|
|
688
|
+
});
|
|
689
|
+
} finally {
|
|
690
|
+
await bleTransport.release(device.id);
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
test('reports a device that drops after a logical release (OK-60486)', async () => {
|
|
695
|
+
// A logical release keeps the native link alive for the keep-alive window.
|
|
696
|
+
// A drop during that window must still reach consumers, or the UI keeps
|
|
697
|
+
// showing the device as connected forever.
|
|
698
|
+
const device = { id: 'idle-drop-pro2-id', name: 'OneKey Pro 2' };
|
|
699
|
+
const nobleBle = createNobleBle(device);
|
|
700
|
+
const emitter = new EventEmitter();
|
|
701
|
+
let disconnectHandler:
|
|
702
|
+
| ((d: { id: string; name: string; reason?: EBleDisconnectReason }) => void)
|
|
703
|
+
| undefined;
|
|
704
|
+
nobleBle.onDeviceDisconnected.mockImplementation(handler => {
|
|
705
|
+
disconnectHandler = handler;
|
|
706
|
+
return jest.fn();
|
|
707
|
+
});
|
|
708
|
+
echoProtocolV2(nobleBle, device.id);
|
|
709
|
+
const transportDisconnect = jest.fn();
|
|
710
|
+
emitter.on('transport-device-disconnect', transportDisconnect);
|
|
711
|
+
const bleTransport = configureTransport(nobleBle, emitter);
|
|
712
|
+
|
|
713
|
+
await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
|
|
714
|
+
await bleTransport.release(device.id);
|
|
715
|
+
|
|
716
|
+
disconnectHandler?.({ ...device, reason: EBleDisconnectReason.DeviceDisconnected });
|
|
717
|
+
|
|
718
|
+
expect(transportDisconnect).toHaveBeenCalledWith({
|
|
719
|
+
id: device.id,
|
|
720
|
+
connectId: device.id,
|
|
721
|
+
name: device.name,
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
test('reports an idle keep-alive release as a device disconnect', async () => {
|
|
726
|
+
// The main process reclaims idle links on its own timer. That is still a
|
|
727
|
+
// closed link, and consumers track link liveness, so it is reported like
|
|
728
|
+
// any other drop. Nothing reconnects on its own, so the state settles once
|
|
729
|
+
// until the user acts.
|
|
730
|
+
const device = { id: 'keep-alive-pro2-id', name: 'OneKey Pro 2' };
|
|
731
|
+
const nobleBle = createNobleBle(device);
|
|
732
|
+
const emitter = new EventEmitter();
|
|
733
|
+
let disconnectHandler:
|
|
734
|
+
| ((d: { id: string; name: string; reason?: EBleDisconnectReason }) => void)
|
|
735
|
+
| undefined;
|
|
736
|
+
nobleBle.onDeviceDisconnected.mockImplementation(handler => {
|
|
737
|
+
disconnectHandler = handler;
|
|
738
|
+
return jest.fn();
|
|
739
|
+
});
|
|
740
|
+
echoProtocolV2(nobleBle, device.id);
|
|
741
|
+
const transportDisconnect = jest.fn();
|
|
742
|
+
emitter.on('transport-device-disconnect', transportDisconnect);
|
|
743
|
+
const bleTransport = configureTransport(nobleBle, emitter);
|
|
744
|
+
|
|
745
|
+
await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
|
|
746
|
+
disconnectHandler?.({ ...device, reason: EBleDisconnectReason.IdleKeepAlive });
|
|
747
|
+
|
|
748
|
+
expect(transportDisconnect).toHaveBeenCalledWith({
|
|
749
|
+
id: device.id,
|
|
750
|
+
connectId: device.id,
|
|
751
|
+
name: device.name,
|
|
752
|
+
});
|
|
753
|
+
// The link really is gone, so cached link state must be dropped too.
|
|
754
|
+
expect(bleTransport.getProtocolType(device.id)).toBeUndefined();
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
test('treats a disconnect with no reason as a real device drop', async () => {
|
|
758
|
+
// An older host bridge does not send `reason`; defaulting to "device left"
|
|
759
|
+
// preserves the pre-existing behaviour rather than silently ignoring it.
|
|
760
|
+
const device = { id: 'legacy-host-pro2-id', name: 'OneKey Pro 2' };
|
|
761
|
+
const nobleBle = createNobleBle(device);
|
|
762
|
+
const emitter = new EventEmitter();
|
|
763
|
+
let disconnectHandler: ((d: { id: string; name: string }) => void) | undefined;
|
|
764
|
+
nobleBle.onDeviceDisconnected.mockImplementation(handler => {
|
|
765
|
+
disconnectHandler = handler;
|
|
766
|
+
return jest.fn();
|
|
767
|
+
});
|
|
768
|
+
echoProtocolV2(nobleBle, device.id);
|
|
769
|
+
const transportDisconnect = jest.fn();
|
|
770
|
+
emitter.on('transport-device-disconnect', transportDisconnect);
|
|
771
|
+
const bleTransport = configureTransport(nobleBle, emitter);
|
|
772
|
+
|
|
773
|
+
await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
|
|
774
|
+
disconnectHandler?.(device);
|
|
775
|
+
|
|
776
|
+
expect(transportDisconnect).toHaveBeenCalledWith({
|
|
777
|
+
id: device.id,
|
|
778
|
+
connectId: device.id,
|
|
779
|
+
name: device.name,
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
test('preserves the active Protocol V2 link when the same schema is configured again', async () => {
|
|
784
|
+
const device = { id: 'stable-schema-pro2-id', name: 'OneKey Pro 2' };
|
|
785
|
+
const nobleBle = createNobleBle(device);
|
|
786
|
+
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
787
|
+
nobleBle.onNotification.mockImplementation(handler => {
|
|
788
|
+
notificationHandler = handler;
|
|
789
|
+
return jest.fn();
|
|
790
|
+
});
|
|
791
|
+
let responseSeq = 0;
|
|
792
|
+
nobleBle.write.mockImplementation(() => {
|
|
793
|
+
responseSeq += 1;
|
|
794
|
+
const response = ProtocolV2.encodeFrame(
|
|
795
|
+
schemas,
|
|
796
|
+
'Success',
|
|
797
|
+
{ message: 'ok' },
|
|
798
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART, seq: responseSeq }
|
|
799
|
+
);
|
|
800
|
+
setTimeout(() => notificationHandler?.(device.id, bytesToHex(response)), 0);
|
|
801
|
+
return Promise.resolve();
|
|
802
|
+
});
|
|
803
|
+
const bleTransport = configureTransport(nobleBle);
|
|
804
|
+
|
|
805
|
+
try {
|
|
806
|
+
await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
|
|
807
|
+
const invalidateAllLinks = jest.spyOn(
|
|
808
|
+
(bleTransport as any).protocolV2Links,
|
|
809
|
+
'invalidateAllLinks'
|
|
810
|
+
);
|
|
811
|
+
bleTransport.configureProtocolV2(protocolV2Schema);
|
|
812
|
+
await new Promise<void>(resolve => {
|
|
813
|
+
setTimeout(resolve, 0);
|
|
814
|
+
});
|
|
815
|
+
expect(invalidateAllLinks).not.toHaveBeenCalled();
|
|
816
|
+
await expect(
|
|
817
|
+
bleTransport.call(device.id, 'Ping', { message: 'same-schema' })
|
|
818
|
+
).resolves.toEqual({
|
|
819
|
+
type: 'Success',
|
|
820
|
+
message: { message: 'ok' },
|
|
821
|
+
});
|
|
334
822
|
const sentSeqs = nobleBle.write.mock.calls.map(([, hex]) =>
|
|
335
823
|
Number.parseInt(hex.slice(12, 14), 16)
|
|
336
824
|
);
|
|
337
825
|
expect(sentSeqs).toEqual([1, 2]);
|
|
338
826
|
} finally {
|
|
339
|
-
await
|
|
827
|
+
await bleTransport.release(device.id);
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
test('rejects oversized Protocol V2 requests before writing to Electron BLE', async () => {
|
|
832
|
+
const device = { id: 'oversized-frame-pro2-id', name: 'OneKey Pro 2' };
|
|
833
|
+
const nobleBle = createNobleBle(device);
|
|
834
|
+
let notificationHandler: ((deviceId: string, data: string) => void) | undefined;
|
|
835
|
+
const probeResponse = ProtocolV2.encodeFrame(
|
|
836
|
+
schemas,
|
|
837
|
+
'Success',
|
|
838
|
+
{ message: 'ok' },
|
|
839
|
+
{ router: PROTOCOL_V2_CHANNEL_BLE_UART }
|
|
840
|
+
);
|
|
841
|
+
nobleBle.onNotification.mockImplementation(handler => {
|
|
842
|
+
notificationHandler = handler;
|
|
843
|
+
return jest.fn();
|
|
844
|
+
});
|
|
845
|
+
nobleBle.write.mockImplementation(() => {
|
|
846
|
+
setTimeout(() => notificationHandler?.(device.id, bytesToHex(probeResponse)), 0);
|
|
847
|
+
return Promise.resolve();
|
|
848
|
+
});
|
|
849
|
+
const bleTransport = configureTransport(nobleBle);
|
|
850
|
+
|
|
851
|
+
try {
|
|
852
|
+
await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V2' });
|
|
853
|
+
expect(nobleBle.write).toHaveBeenCalledTimes(1);
|
|
854
|
+
|
|
855
|
+
await expect(
|
|
856
|
+
bleTransport.call(device.id, 'Ping', { message: 'x'.repeat(2048) })
|
|
857
|
+
).rejects.toThrow(/Protocol V2 frame too large for transport/);
|
|
858
|
+
expect(nobleBle.write).toHaveBeenCalledTimes(1);
|
|
859
|
+
} finally {
|
|
860
|
+
await bleTransport.release(device.id);
|
|
340
861
|
}
|
|
341
862
|
});
|
|
342
863
|
});
|