@onekeyfe/hd-transport-electron 1.2.0-alpha.7 → 1.2.0-alpha.71
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/ble-ops.d.ts +6 -0
- package/dist/ble-ops.d.ts.map +1 -1
- package/dist/ble-packet-capacity.d.ts +2 -0
- package/dist/ble-packet-capacity.d.ts.map +1 -0
- package/dist/{index-02fc07fe.js → index-bd1cfdb6.js} +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +1 -1
- package/dist/{noble-ble-handler-94c42077.js → noble-ble-handler-7ac9c43d.js} +276 -234
- package/dist/noble-ble-handler.d.ts.map +1 -1
- package/dist/noble-ble-timeouts.d.ts +3 -0
- package/dist/noble-ble-timeouts.d.ts.map +1 -0
- package/dist/types/desktop-api.d.ts +5 -0
- package/dist/types/desktop-api.d.ts.map +1 -1
- package/dist/types/noble-extended.d.ts +1 -0
- package/dist/types/noble-extended.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/ble-ops.test.ts +21 -0
- package/src/__tests__/ble-packet-capacity.test.ts +13 -0
- package/src/__tests__/noble-ble-handler.test.ts +354 -0
- package/src/ble-ops.ts +40 -10
- package/src/ble-packet-capacity.ts +13 -0
- package/src/noble-ble-handler.ts +273 -253
- package/src/noble-ble-timeouts.ts +2 -0
- package/src/types/desktop-api.ts +2 -1
- package/src/types/noble-extended.ts +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"noble-ble-handler.d.ts","sourceRoot":"","sources":["../src/noble-ble-handler.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"noble-ble-handler.d.ts","sourceRoot":"","sources":["../src/noble-ble-handler.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAsB,WAAW,EAAE,MAAM,UAAU,CAAC;AA++ChE,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAyJpE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"noble-ble-timeouts.d.ts","sourceRoot":"","sources":["../src/noble-ble-timeouts.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,kCAAkC,OAAQ,CAAC;AACxD,eAAO,MAAM,+BAA+B,QAAS,CAAC"}
|
|
@@ -6,6 +6,7 @@ export interface NobleBleAPI {
|
|
|
6
6
|
getDevice: (uuid: string) => Promise<{
|
|
7
7
|
id: string;
|
|
8
8
|
name: string;
|
|
9
|
+
mtu?: number;
|
|
9
10
|
} | null>;
|
|
10
11
|
connect: (uuid: string) => Promise<void>;
|
|
11
12
|
disconnect: (uuid: string) => Promise<void>;
|
|
@@ -13,6 +14,10 @@ export interface NobleBleAPI {
|
|
|
13
14
|
unsubscribe: (uuid: string) => Promise<void>;
|
|
14
15
|
write: (uuid: string, data: string) => Promise<void>;
|
|
15
16
|
onNotification: (callback: (deviceId: string, data: string) => void) => () => void;
|
|
17
|
+
onMtuChanged?: (callback: (device: {
|
|
18
|
+
id: string;
|
|
19
|
+
mtu: number;
|
|
20
|
+
}) => void) => () => void;
|
|
16
21
|
onDeviceDisconnected: (callback: (device: {
|
|
17
22
|
id: string;
|
|
18
23
|
name: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"desktop-api.d.ts","sourceRoot":"","sources":["../../src/types/desktop-api.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC,CAAC;IACzD,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"desktop-api.d.ts","sourceRoot":"","sources":["../../src/types/desktop-api.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC,CAAC;IACzD,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IACxF,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD,cAAc,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IACnF,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IACvF,oBAAoB,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IAC/F,iBAAiB,EAAE,MAAM,OAAO,CAAC;QAC/B,SAAS,EAAE,OAAO,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,EAAE,OAAO,CAAC;QACrB,WAAW,EAAE,OAAO,CAAC;KACtB,CAAC,CAAC;CACJ;AAGD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,WAAW,CAAC;CACxB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"noble-extended.d.ts","sourceRoot":"","sources":["../../src/types/noble-extended.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAGnE,MAAM,WAAW,UAAW,SAAQ,oBAAoB;IACtD,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"noble-extended.d.ts","sourceRoot":"","sources":["../../src/types/noble-extended.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAGnE,MAAM,WAAW,UAAW,SAAQ,oBAAoB;IACtD,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAGD,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,cAAc,CAAC;CACxB;AAGD,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CACX,YAAY,EAAE,MAAM,EAAE,EACtB,eAAe,EAAE,OAAO,EACxB,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,GACjC,IAAI,CAAC;IACR,YAAY,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC1C,EAAE,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAClE,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;IACxE,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAC9E,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;CACrF;AAGD,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC5C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC7C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;CAC9C;AAGD,wBAAgB,OAAO,CACrB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,EACjC,OAAO,EAAE,MAAM,EACf,GAAG,IAAI,EAAE,GAAG,EAAE,GACb,IAAI,CAMN"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-transport-electron",
|
|
3
|
-
"version": "1.2.0-alpha.
|
|
3
|
+
"version": "1.2.0-alpha.71",
|
|
4
4
|
"author": "OneKey",
|
|
5
5
|
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"electron-log": ">=4.0.0"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@onekeyfe/hd-core": "1.2.0-alpha.
|
|
29
|
-
"@onekeyfe/hd-shared": "1.2.0-alpha.
|
|
30
|
-
"@onekeyfe/hd-transport": "1.2.0-alpha.
|
|
28
|
+
"@onekeyfe/hd-core": "1.2.0-alpha.71",
|
|
29
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.71",
|
|
30
|
+
"@onekeyfe/hd-transport": "1.2.0-alpha.71",
|
|
31
31
|
"@stoprocent/noble": "2.3.16",
|
|
32
32
|
"p-retry": "^4.6.2"
|
|
33
33
|
},
|
|
@@ -36,5 +36,5 @@
|
|
|
36
36
|
"electron": "^25.0.0",
|
|
37
37
|
"typescript": "^5.3.3"
|
|
38
38
|
},
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "c58f39f2e5c54a98faa8844cd8b6b0453a17d80c"
|
|
40
40
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { runBleCallbackOperation } from '../ble-ops';
|
|
2
|
+
|
|
3
|
+
describe('Electron Noble callback operations', () => {
|
|
4
|
+
test('settles when Noble never invokes the callback', async () => {
|
|
5
|
+
const result = await runBleCallbackOperation(() => undefined, {
|
|
6
|
+
timeoutMs: 5,
|
|
7
|
+
timeoutBehavior: 'resolve',
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
expect(result).toBeUndefined();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('rejects callback errors before the timeout', async () => {
|
|
14
|
+
await expect(
|
|
15
|
+
runBleCallbackOperation(callback => callback(new Error('operation failed')), {
|
|
16
|
+
timeoutMs: 50,
|
|
17
|
+
timeoutBehavior: 'reject',
|
|
18
|
+
})
|
|
19
|
+
).rejects.toThrow('operation failed');
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { resolveBlePacketCapacity } from '../ble-packet-capacity';
|
|
2
|
+
|
|
3
|
+
describe('resolveBlePacketCapacity', () => {
|
|
4
|
+
test('uses ATT MTU payload capacity with an upper bound', () => {
|
|
5
|
+
expect(resolveBlePacketCapacity(247, 244, 192)).toBe(244);
|
|
6
|
+
expect(resolveBlePacketCapacity(185, 244, 192)).toBe(182);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test('preserves the compatibility fallback when MTU is unavailable', () => {
|
|
10
|
+
expect(resolveBlePacketCapacity(null, 244, 192)).toBe(192);
|
|
11
|
+
expect(resolveBlePacketCapacity(undefined, 244, 192)).toBe(192);
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { EOneKeyBleMessageKeys } from '@onekeyfe/hd-shared';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
NOBLE_BLE_CONNECTION_TIMEOUT_MS,
|
|
6
|
+
NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS,
|
|
7
|
+
} from '../noble-ble-timeouts';
|
|
8
|
+
|
|
9
|
+
import type { WebContents } from 'electron';
|
|
10
|
+
|
|
11
|
+
type IpcHandler = (...args: unknown[]) => Promise<unknown> | unknown;
|
|
12
|
+
|
|
13
|
+
const createPeripheral = (id: string, localName?: string) => ({
|
|
14
|
+
id,
|
|
15
|
+
state: 'disconnected',
|
|
16
|
+
advertisement: {
|
|
17
|
+
localName,
|
|
18
|
+
serviceUuids: ['0001', 'fffd'],
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('Electron Noble BLE device discovery', () => {
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
jest.useRealTimers();
|
|
25
|
+
jest.resetModules();
|
|
26
|
+
jest.clearAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('allows enough time for a slow targeted scan and connection', () => {
|
|
30
|
+
expect(NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS).toBe(5_000);
|
|
31
|
+
expect(NOBLE_BLE_CONNECTION_TIMEOUT_MS).toBe(10_000);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('waits for Noble to stop scanning before enumeration resolves', async () => {
|
|
35
|
+
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
36
|
+
|
|
37
|
+
const handlers = new Map<string, IpcHandler>();
|
|
38
|
+
const ipcMain = {
|
|
39
|
+
handle: jest.fn((channel: string, handler: IpcHandler) => {
|
|
40
|
+
handlers.set(channel, handler);
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
44
|
+
state: string;
|
|
45
|
+
startScanning: jest.Mock;
|
|
46
|
+
stopScanning: jest.Mock;
|
|
47
|
+
};
|
|
48
|
+
let resolveScanStarted = () => undefined;
|
|
49
|
+
const scanStarted = new Promise<void>(resolve => {
|
|
50
|
+
resolveScanStarted = resolve;
|
|
51
|
+
});
|
|
52
|
+
let stopScanningCallback: (() => void) | undefined;
|
|
53
|
+
noble.state = 'poweredOn';
|
|
54
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
55
|
+
callback?.();
|
|
56
|
+
noble.emit('discover', createPeripheral('onekey-device', 'Pro2 A1B2'));
|
|
57
|
+
resolveScanStarted();
|
|
58
|
+
});
|
|
59
|
+
noble.stopScanning = jest.fn(callback => {
|
|
60
|
+
stopScanningCallback = callback;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
64
|
+
jest.doMock('electron', () => ({ ipcMain }));
|
|
65
|
+
jest.doMock('electron-log', () => ({
|
|
66
|
+
info: jest.fn(),
|
|
67
|
+
debug: jest.fn(),
|
|
68
|
+
error: jest.fn(),
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
const { setupNobleBleHandlers } = await import('../noble-ble-handler');
|
|
72
|
+
setupNobleBleHandlers({
|
|
73
|
+
on: jest.fn(),
|
|
74
|
+
send: jest.fn(),
|
|
75
|
+
} as unknown as WebContents);
|
|
76
|
+
|
|
77
|
+
const enumerate = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE);
|
|
78
|
+
if (!enumerate) {
|
|
79
|
+
throw new Error('Electron Noble BLE enumerate handler was not registered');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let enumerationResolved = false;
|
|
83
|
+
const devicesPromise = Promise.resolve(enumerate()).then(devices => {
|
|
84
|
+
enumerationResolved = true;
|
|
85
|
+
return devices;
|
|
86
|
+
});
|
|
87
|
+
await scanStarted;
|
|
88
|
+
jest.advanceTimersByTime(5_000);
|
|
89
|
+
await Promise.resolve();
|
|
90
|
+
|
|
91
|
+
expect(noble.stopScanning).toHaveBeenCalledTimes(1);
|
|
92
|
+
expect(enumerationResolved).toBe(false);
|
|
93
|
+
|
|
94
|
+
stopScanningCallback?.();
|
|
95
|
+
await expect(devicesPromise).resolves.toEqual([
|
|
96
|
+
expect.objectContaining({ id: 'onekey-device' }),
|
|
97
|
+
]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('waits for a targeted scan to stop before connecting', async () => {
|
|
101
|
+
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
102
|
+
|
|
103
|
+
const handlers = new Map<string, IpcHandler>();
|
|
104
|
+
const ipcMain = {
|
|
105
|
+
handle: jest.fn((channel: string, handler: IpcHandler) => {
|
|
106
|
+
handlers.set(channel, handler);
|
|
107
|
+
}),
|
|
108
|
+
};
|
|
109
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
110
|
+
state: string;
|
|
111
|
+
startScanning: jest.Mock;
|
|
112
|
+
stopScanning: jest.Mock;
|
|
113
|
+
};
|
|
114
|
+
let resolveScanStarted = () => undefined;
|
|
115
|
+
const scanStarted = new Promise<void>(resolve => {
|
|
116
|
+
resolveScanStarted = resolve;
|
|
117
|
+
});
|
|
118
|
+
let stopScanningCallback: (() => void) | undefined;
|
|
119
|
+
const peripheral = Object.assign(
|
|
120
|
+
new EventEmitter(),
|
|
121
|
+
createPeripheral('target-device', 'Pro2 A1B2'),
|
|
122
|
+
{
|
|
123
|
+
connect: jest.fn((callback: (error?: Error) => void) => {
|
|
124
|
+
callback(new Error('expected test connection failure'));
|
|
125
|
+
}),
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
noble.state = 'poweredOn';
|
|
129
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
130
|
+
callback?.();
|
|
131
|
+
noble.emit('discover', peripheral);
|
|
132
|
+
resolveScanStarted();
|
|
133
|
+
});
|
|
134
|
+
noble.stopScanning = jest.fn(callback => {
|
|
135
|
+
stopScanningCallback = callback;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
139
|
+
jest.doMock('electron', () => ({ ipcMain }));
|
|
140
|
+
jest.doMock('electron-log', () => ({
|
|
141
|
+
info: jest.fn(),
|
|
142
|
+
debug: jest.fn(),
|
|
143
|
+
error: jest.fn(),
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
const { setupNobleBleHandlers } = await import('../noble-ble-handler');
|
|
147
|
+
setupNobleBleHandlers({
|
|
148
|
+
on: jest.fn(),
|
|
149
|
+
send: jest.fn(),
|
|
150
|
+
} as unknown as WebContents);
|
|
151
|
+
|
|
152
|
+
const connect = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT);
|
|
153
|
+
if (!connect) {
|
|
154
|
+
throw new Error('Electron Noble BLE connect handler was not registered');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const connectPromise = Promise.resolve(connect(undefined, 'target-device'));
|
|
158
|
+
await scanStarted;
|
|
159
|
+
|
|
160
|
+
expect(noble.stopScanning).toHaveBeenCalledTimes(1);
|
|
161
|
+
expect(peripheral.connect).not.toHaveBeenCalled();
|
|
162
|
+
|
|
163
|
+
stopScanningCallback?.();
|
|
164
|
+
await expect(connectPromise).rejects.toThrow('expected test connection failure');
|
|
165
|
+
expect(peripheral.connect).toHaveBeenCalledTimes(1);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('disconnects a connection callback that arrives after timeout', async () => {
|
|
169
|
+
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
170
|
+
|
|
171
|
+
const handlers = new Map<string, IpcHandler>();
|
|
172
|
+
const ipcMain = {
|
|
173
|
+
handle: jest.fn((channel: string, handler: IpcHandler) => {
|
|
174
|
+
handlers.set(channel, handler);
|
|
175
|
+
}),
|
|
176
|
+
};
|
|
177
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
178
|
+
state: string;
|
|
179
|
+
startScanning: jest.Mock;
|
|
180
|
+
stopScanning: jest.Mock;
|
|
181
|
+
};
|
|
182
|
+
let connectCallback: ((error?: Error) => void) | undefined;
|
|
183
|
+
let resolveConnectStarted = () => undefined;
|
|
184
|
+
const connectStarted = new Promise<void>(resolve => {
|
|
185
|
+
resolveConnectStarted = resolve;
|
|
186
|
+
});
|
|
187
|
+
const peripheral = Object.assign(
|
|
188
|
+
new EventEmitter(),
|
|
189
|
+
createPeripheral('slow-device', 'Pro2 A1B2'),
|
|
190
|
+
{
|
|
191
|
+
connect: jest.fn((callback: (error?: Error) => void) => {
|
|
192
|
+
connectCallback = callback;
|
|
193
|
+
resolveConnectStarted();
|
|
194
|
+
}),
|
|
195
|
+
disconnect: jest.fn((callback: () => void) => callback()),
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
noble.state = 'poweredOn';
|
|
199
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
200
|
+
callback?.();
|
|
201
|
+
noble.emit('discover', peripheral);
|
|
202
|
+
});
|
|
203
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
204
|
+
|
|
205
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
206
|
+
jest.doMock('electron', () => ({ ipcMain }));
|
|
207
|
+
jest.doMock('electron-log', () => ({
|
|
208
|
+
info: jest.fn(),
|
|
209
|
+
debug: jest.fn(),
|
|
210
|
+
error: jest.fn(),
|
|
211
|
+
}));
|
|
212
|
+
|
|
213
|
+
const { setupNobleBleHandlers } = await import('../noble-ble-handler');
|
|
214
|
+
setupNobleBleHandlers({
|
|
215
|
+
on: jest.fn(),
|
|
216
|
+
send: jest.fn(),
|
|
217
|
+
} as unknown as WebContents);
|
|
218
|
+
|
|
219
|
+
const connect = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT);
|
|
220
|
+
if (!connect) {
|
|
221
|
+
throw new Error('Electron Noble BLE connect handler was not registered');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const connectPromise = Promise.resolve(connect(undefined, 'slow-device'));
|
|
225
|
+
await connectStarted;
|
|
226
|
+
expect(peripheral.connect).toHaveBeenCalledTimes(1);
|
|
227
|
+
|
|
228
|
+
jest.advanceTimersByTime(NOBLE_BLE_CONNECTION_TIMEOUT_MS);
|
|
229
|
+
await expect(connectPromise).rejects.toThrow('Connection timeout');
|
|
230
|
+
|
|
231
|
+
connectCallback?.();
|
|
232
|
+
await Promise.resolve();
|
|
233
|
+
expect(peripheral.disconnect).toHaveBeenCalledTimes(1);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('does not enumerate a Pro2 Find My advertisement with the communication service', async () => {
|
|
237
|
+
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
238
|
+
|
|
239
|
+
const handlers = new Map<string, IpcHandler>();
|
|
240
|
+
const ipcMain = {
|
|
241
|
+
handle: jest.fn((channel: string, handler: IpcHandler) => {
|
|
242
|
+
handlers.set(channel, handler);
|
|
243
|
+
}),
|
|
244
|
+
};
|
|
245
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
246
|
+
state: string;
|
|
247
|
+
startScanning: jest.Mock;
|
|
248
|
+
stopScanning: jest.Mock;
|
|
249
|
+
};
|
|
250
|
+
let resolveScanStarted = () => undefined;
|
|
251
|
+
const scanStarted = new Promise<void>(resolve => {
|
|
252
|
+
resolveScanStarted = resolve;
|
|
253
|
+
});
|
|
254
|
+
noble.state = 'poweredOn';
|
|
255
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
256
|
+
callback?.();
|
|
257
|
+
noble.emit('discover', createPeripheral('find-my-device', 'Pro2 A1B2 - Find My'));
|
|
258
|
+
noble.emit('discover', createPeripheral('onekey-device', 'Pro2 A1B2'));
|
|
259
|
+
resolveScanStarted();
|
|
260
|
+
});
|
|
261
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
262
|
+
|
|
263
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
264
|
+
jest.doMock('electron', () => ({ ipcMain }));
|
|
265
|
+
jest.doMock('electron-log', () => ({
|
|
266
|
+
info: jest.fn(),
|
|
267
|
+
debug: jest.fn(),
|
|
268
|
+
error: jest.fn(),
|
|
269
|
+
}));
|
|
270
|
+
|
|
271
|
+
const { setupNobleBleHandlers } = await import('../noble-ble-handler');
|
|
272
|
+
setupNobleBleHandlers({
|
|
273
|
+
on: jest.fn(),
|
|
274
|
+
send: jest.fn(),
|
|
275
|
+
} as unknown as WebContents);
|
|
276
|
+
|
|
277
|
+
const enumerate = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE);
|
|
278
|
+
if (!enumerate) {
|
|
279
|
+
throw new Error('Electron Noble BLE enumerate handler was not registered');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const devicesPromise = Promise.resolve(enumerate());
|
|
283
|
+
await scanStarted;
|
|
284
|
+
jest.advanceTimersByTime(5000);
|
|
285
|
+
|
|
286
|
+
await expect(devicesPromise).resolves.toEqual([
|
|
287
|
+
expect.objectContaining({
|
|
288
|
+
id: 'onekey-device',
|
|
289
|
+
name: 'Pro2 A1B2',
|
|
290
|
+
}),
|
|
291
|
+
]);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test('does not enumerate a Find My peripheral first discovered without a name', async () => {
|
|
295
|
+
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
296
|
+
|
|
297
|
+
const handlers = new Map<string, IpcHandler>();
|
|
298
|
+
const ipcMain = {
|
|
299
|
+
handle: jest.fn((channel: string, handler: IpcHandler) => {
|
|
300
|
+
handlers.set(channel, handler);
|
|
301
|
+
}),
|
|
302
|
+
};
|
|
303
|
+
const noble = new EventEmitter() as EventEmitter & {
|
|
304
|
+
state: string;
|
|
305
|
+
startScanning: jest.Mock;
|
|
306
|
+
stopScanning: jest.Mock;
|
|
307
|
+
};
|
|
308
|
+
let resolveScanStarted = () => undefined;
|
|
309
|
+
const scanStarted = new Promise<void>(resolve => {
|
|
310
|
+
resolveScanStarted = resolve;
|
|
311
|
+
});
|
|
312
|
+
const findMyPeripheral = createPeripheral('find-my-device');
|
|
313
|
+
noble.state = 'poweredOn';
|
|
314
|
+
noble.startScanning = jest.fn((_services, _duplicates, callback) => {
|
|
315
|
+
callback?.();
|
|
316
|
+
noble.emit('discover', findMyPeripheral);
|
|
317
|
+
findMyPeripheral.advertisement.localName = 'Pro2 A1B2 - Find My';
|
|
318
|
+
noble.emit('discover', findMyPeripheral);
|
|
319
|
+
noble.emit('discover', createPeripheral('onekey-device', 'Pro2 A1B2'));
|
|
320
|
+
resolveScanStarted();
|
|
321
|
+
});
|
|
322
|
+
noble.stopScanning = jest.fn(callback => callback?.());
|
|
323
|
+
|
|
324
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
325
|
+
jest.doMock('electron', () => ({ ipcMain }));
|
|
326
|
+
jest.doMock('electron-log', () => ({
|
|
327
|
+
info: jest.fn(),
|
|
328
|
+
debug: jest.fn(),
|
|
329
|
+
error: jest.fn(),
|
|
330
|
+
}));
|
|
331
|
+
|
|
332
|
+
const { setupNobleBleHandlers } = await import('../noble-ble-handler');
|
|
333
|
+
setupNobleBleHandlers({
|
|
334
|
+
on: jest.fn(),
|
|
335
|
+
send: jest.fn(),
|
|
336
|
+
} as unknown as WebContents);
|
|
337
|
+
|
|
338
|
+
const enumerate = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE);
|
|
339
|
+
if (!enumerate) {
|
|
340
|
+
throw new Error('Electron Noble BLE enumerate handler was not registered');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const devicesPromise = Promise.resolve(enumerate());
|
|
344
|
+
await scanStarted;
|
|
345
|
+
jest.advanceTimersByTime(5000);
|
|
346
|
+
|
|
347
|
+
await expect(devicesPromise).resolves.toEqual([
|
|
348
|
+
expect.objectContaining({
|
|
349
|
+
id: 'onekey-device',
|
|
350
|
+
name: 'Pro2 A1B2',
|
|
351
|
+
}),
|
|
352
|
+
]);
|
|
353
|
+
});
|
|
354
|
+
});
|
package/src/ble-ops.ts
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
import type { Characteristic } from '@stoprocent/noble';
|
|
2
2
|
import type { Logger } from './types/noble-extended';
|
|
3
3
|
|
|
4
|
+
type BleCallback = (error?: Error) => void;
|
|
5
|
+
|
|
6
|
+
export function runBleCallbackOperation(
|
|
7
|
+
operation: (callback: BleCallback) => void,
|
|
8
|
+
options: { timeoutMs: number; timeoutBehavior: 'resolve' | 'reject' }
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
return new Promise<void>((resolve, reject) => {
|
|
11
|
+
let settled = false;
|
|
12
|
+
const settle = (error?: Error) => {
|
|
13
|
+
if (settled) return;
|
|
14
|
+
settled = true;
|
|
15
|
+
clearTimeout(timeout);
|
|
16
|
+
if (error) {
|
|
17
|
+
reject(error);
|
|
18
|
+
} else {
|
|
19
|
+
resolve();
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const timeout = setTimeout(() => {
|
|
23
|
+
if (options.timeoutBehavior === 'reject') {
|
|
24
|
+
settle(new Error(`BLE operation timed out after ${options.timeoutMs}ms`));
|
|
25
|
+
} else {
|
|
26
|
+
settle();
|
|
27
|
+
}
|
|
28
|
+
}, options.timeoutMs);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
operation(settle);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
settle(error instanceof Error ? error : new Error(String(error)));
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
4
38
|
export interface SoftRefreshParams {
|
|
5
39
|
deviceId: string;
|
|
6
40
|
notifyCharacteristic: Characteristic | null | undefined;
|
|
@@ -30,18 +64,14 @@ export async function softRefreshSubscription(params: SoftRefreshParams): Promis
|
|
|
30
64
|
|
|
31
65
|
subscriptionOperations.set(deviceId, 'subscribing');
|
|
32
66
|
|
|
33
|
-
await
|
|
34
|
-
|
|
67
|
+
await runBleCallbackOperation(callback => notifyCharacteristic.unsubscribe(callback), {
|
|
68
|
+
timeoutMs: 250,
|
|
69
|
+
timeoutBehavior: 'resolve',
|
|
35
70
|
});
|
|
36
71
|
|
|
37
|
-
await
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
reject(error);
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
resolve();
|
|
44
|
-
});
|
|
72
|
+
await runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
|
|
73
|
+
timeoutMs: 8_000,
|
|
74
|
+
timeoutBehavior: 'reject',
|
|
45
75
|
});
|
|
46
76
|
|
|
47
77
|
notifyCharacteristic.removeAllListeners('data');
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const BLE_ATT_HEADER_BYTES = 3;
|
|
2
|
+
|
|
3
|
+
export function resolveBlePacketCapacity(
|
|
4
|
+
mtu: number | null | undefined,
|
|
5
|
+
maximumPacketCapacity: number,
|
|
6
|
+
fallbackPacketCapacity: number
|
|
7
|
+
): number {
|
|
8
|
+
if (typeof mtu !== 'number' || !Number.isFinite(mtu) || mtu <= BLE_ATT_HEADER_BYTES) {
|
|
9
|
+
return fallbackPacketCapacity;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return Math.min(maximumPacketCapacity, Math.floor(mtu) - BLE_ATT_HEADER_BYTES);
|
|
13
|
+
}
|