@onekeyfe/hd-transport-electron 1.2.3-alpha.1 → 1.2.3-alpha.10
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-985e2bf3.js → index-e2e3d2ba.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/{noble-ble-handler-263d4736.js → noble-ble-handler-d0991fa0.js} +33 -11
- package/dist/noble-ble-handler.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/noble-ble-handler.test.ts +222 -5
- package/src/noble-ble-handler.ts +39 -15
|
@@ -55,13 +55,13 @@ function invokeNobleBleIpc(request) {
|
|
|
55
55
|
|
|
56
56
|
function initNobleBleSupport(webContents) {
|
|
57
57
|
return __awaiter(this, void 0, void 0, function* () {
|
|
58
|
-
const { setupNobleBleHandlers } = yield Promise.resolve().then(function () { return require('./noble-ble-handler-
|
|
58
|
+
const { setupNobleBleHandlers } = yield Promise.resolve().then(function () { return require('./noble-ble-handler-d0991fa0.js'); });
|
|
59
59
|
setupNobleBleHandlers(webContents);
|
|
60
60
|
});
|
|
61
61
|
}
|
|
62
62
|
function disposeNobleBleSupport(releaseNoble) {
|
|
63
63
|
return __awaiter(this, void 0, void 0, function* () {
|
|
64
|
-
const handler = yield Promise.resolve().then(function () { return require('./noble-ble-handler-
|
|
64
|
+
const handler = yield Promise.resolve().then(function () { return require('./noble-ble-handler-d0991fa0.js'); });
|
|
65
65
|
yield handler.disposeNobleBleSupport(releaseNoble);
|
|
66
66
|
});
|
|
67
67
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var index = require('./index-
|
|
3
|
+
var index = require('./index-e2e3d2ba.js');
|
|
4
4
|
var hdShared = require('@onekeyfe/hd-shared');
|
|
5
5
|
var pRetry = require('p-retry');
|
|
6
6
|
|
|
@@ -123,8 +123,7 @@ function assertBleActive() {
|
|
|
123
123
|
}
|
|
124
124
|
function createNobleBleConnectionError(error, messagePrefix = '') {
|
|
125
125
|
const errorMessage = error.message;
|
|
126
|
-
const isInvalidMacOsBond =
|
|
127
|
-
(error.nativeErrorCode === 15 && error.nativeErrorDomain === 'CBATTErrorDomain');
|
|
126
|
+
const isInvalidMacOsBond = error.nativeErrorCode === 14 && error.nativeErrorDomain === 'CBErrorDomain';
|
|
128
127
|
if (isInvalidMacOsBond) {
|
|
129
128
|
const nativeErrorMessage = `${messagePrefix}${errorMessage}`;
|
|
130
129
|
return hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleBondInvalid, `${hdShared.HardwareErrorCodeMessage[hdShared.HardwareErrorCode.BleBondInvalid]} (${nativeErrorMessage})`, {
|
|
@@ -887,7 +886,6 @@ function enumerateDevices(isWindowDestroyed) {
|
|
|
887
886
|
return [];
|
|
888
887
|
const nobleInstance = noble;
|
|
889
888
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Starting device enumeration');
|
|
890
|
-
discoveredDevices.clear();
|
|
891
889
|
ensureDiscoverListener();
|
|
892
890
|
return new Promise((resolve, reject) => {
|
|
893
891
|
const devices = [];
|
|
@@ -1256,11 +1254,15 @@ function tryDirectConnectById(deviceId) {
|
|
|
1256
1254
|
return peripheral;
|
|
1257
1255
|
}
|
|
1258
1256
|
catch (error) {
|
|
1259
|
-
directConnectCooldownUntil.set(deviceId, Date.now() + DIRECT_CONNECT_COOLDOWN_MS);
|
|
1260
1257
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Direct connect-by-id failed, falling back to scan', {
|
|
1261
1258
|
deviceId,
|
|
1262
1259
|
error: String(error),
|
|
1263
1260
|
});
|
|
1261
|
+
const nativeError = error;
|
|
1262
|
+
if (nativeError.nativeErrorCode === 14 && nativeError.nativeErrorDomain === 'CBErrorDomain') {
|
|
1263
|
+
throw createNobleBleConnectionError(nativeError);
|
|
1264
|
+
}
|
|
1265
|
+
directConnectCooldownUntil.set(deviceId, Date.now() + DIRECT_CONNECT_COOLDOWN_MS);
|
|
1264
1266
|
return undefined;
|
|
1265
1267
|
}
|
|
1266
1268
|
finally {
|
|
@@ -1281,7 +1283,10 @@ function connectDevice(deviceId, webContents) {
|
|
|
1281
1283
|
totalDiscovered: discoveredDevices.size,
|
|
1282
1284
|
totalConnected: connectedDevices.size,
|
|
1283
1285
|
});
|
|
1284
|
-
let peripheral = (_a =
|
|
1286
|
+
let peripheral = (_a = connectedDevices.get(deviceId)) !== null && _a !== void 0 ? _a : discoveredDevices.get(deviceId);
|
|
1287
|
+
if ((peripheral === null || peripheral === void 0 ? void 0 : peripheral.state) !== 'connected') {
|
|
1288
|
+
peripheral = undefined;
|
|
1289
|
+
}
|
|
1285
1290
|
if (!peripheral) {
|
|
1286
1291
|
if (!noble) {
|
|
1287
1292
|
yield initializeNoble();
|
|
@@ -1308,17 +1313,29 @@ function connectDevice(deviceId, webContents) {
|
|
|
1308
1313
|
return undefined;
|
|
1309
1314
|
}
|
|
1310
1315
|
});
|
|
1316
|
+
let staleBondError;
|
|
1311
1317
|
const connectById = () => index.__awaiter(this, void 0, void 0, function* () {
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1318
|
+
try {
|
|
1319
|
+
const found = yield tryDirectConnectById(deviceId);
|
|
1320
|
+
if (found) {
|
|
1321
|
+
discoveredDevices.set(deviceId, found);
|
|
1322
|
+
}
|
|
1323
|
+
return found;
|
|
1324
|
+
}
|
|
1325
|
+
catch (error) {
|
|
1326
|
+
if (error.errorCode !== hdShared.HardwareErrorCode.BleBondInvalid) {
|
|
1327
|
+
throw error;
|
|
1328
|
+
}
|
|
1329
|
+
staleBondError = error;
|
|
1330
|
+
return undefined;
|
|
1315
1331
|
}
|
|
1316
|
-
return found;
|
|
1317
1332
|
});
|
|
1318
1333
|
peripheral = byIdFirst ? yield connectById() : yield scanForPeripheral();
|
|
1319
1334
|
if (!peripheral) {
|
|
1320
1335
|
peripheral = byIdFirst ? yield scanForPeripheral() : yield connectById();
|
|
1321
1336
|
}
|
|
1337
|
+
if (!peripheral && staleBondError)
|
|
1338
|
+
throw staleBondError;
|
|
1322
1339
|
}
|
|
1323
1340
|
assertBleActive();
|
|
1324
1341
|
if (!peripheral) {
|
|
@@ -1515,7 +1532,12 @@ function subscribeNotifications(deviceId, callback) {
|
|
|
1515
1532
|
});
|
|
1516
1533
|
}
|
|
1517
1534
|
catch (error) {
|
|
1518
|
-
|
|
1535
|
+
const nativeError = error;
|
|
1536
|
+
if (nativeError.nativeErrorCode === 15 &&
|
|
1537
|
+
nativeError.nativeErrorDomain === 'CBATTErrorDomain') {
|
|
1538
|
+
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleDeviceNotBonded, hdShared.HardwareErrorCodeMessage[hdShared.HardwareErrorCode.BleDeviceNotBonded], { nativeErrorMessage: `Notification subscription failed: ${nativeError.message}` });
|
|
1539
|
+
}
|
|
1540
|
+
throw createNobleBleConnectionError(nativeError, 'Notification subscription failed: ');
|
|
1519
1541
|
}
|
|
1520
1542
|
finally {
|
|
1521
1543
|
if (!disposing)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"noble-ble-handler.d.ts","sourceRoot":"","sources":["../src/noble-ble-handler.ts"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAA+B,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,KAAK,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAiC1F,KAAK,mBAAmB,GAAG,KAAK,GAAG;IACjC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,mBAAmB,EAAE,aAAa,SAAK,+
|
|
1
|
+
{"version":3,"file":"noble-ble-handler.d.ts","sourceRoot":"","sources":["../src/noble-ble-handler.ts"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAA+B,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,KAAK,EAAE,wBAAwB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAiC1F,KAAK,mBAAmB,GAAG,KAAK,GAAG;IACjC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,mBAAmB,EAAE,aAAa,SAAK,+CAgB3F;AAED,wBAAgB,8BAA8B,CAAC,KAAK,EAAE,OAAO,GAAG,wBAAwB,CAgCvF;AA+KD,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE,oBAAoB,UAI7E;AA2zDD,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAkOpE;AAOD,wBAAgB,sBAAsB,CACpC,YAAY,GAAE,CAAC,QAAQ,EAAE;IAAE,IAAI,IAAI,IAAI,CAAA;CAAE,KAAK,IAAkC,GAC/E,OAAO,CAAC,IAAI,CAAC,CAiEf"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onekeyfe/hd-transport-electron",
|
|
3
|
-
"version": "1.2.3-alpha.
|
|
3
|
+
"version": "1.2.3-alpha.10",
|
|
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.3-alpha.
|
|
29
|
-
"@onekeyfe/hd-shared": "1.2.3-alpha.
|
|
30
|
-
"@onekeyfe/hd-transport": "1.2.3-alpha.
|
|
28
|
+
"@onekeyfe/hd-core": "1.2.3-alpha.10",
|
|
29
|
+
"@onekeyfe/hd-shared": "1.2.3-alpha.10",
|
|
30
|
+
"@onekeyfe/hd-transport": "1.2.3-alpha.10",
|
|
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": "2295285a846281941c21955684281446f587d993"
|
|
40
40
|
}
|
|
@@ -68,7 +68,7 @@ describe('Electron Noble BLE device discovery', () => {
|
|
|
68
68
|
})
|
|
69
69
|
)
|
|
70
70
|
).toMatchObject({
|
|
71
|
-
errorCode: HardwareErrorCode.
|
|
71
|
+
errorCode: HardwareErrorCode.BleConnectedError,
|
|
72
72
|
});
|
|
73
73
|
expect(createNobleBleConnectionError(new Error('connection failed'))).toMatchObject({
|
|
74
74
|
errorCode: HardwareErrorCode.BleConnectedError,
|
|
@@ -225,6 +225,214 @@ describe('Electron Noble BLE device discovery', () => {
|
|
|
225
225
|
]);
|
|
226
226
|
});
|
|
227
227
|
|
|
228
|
+
test.each(['Pro A1B2', 'Pro2 A1B2'])(
|
|
229
|
+
'retains %s across scan rounds until Bluetooth is powered off',
|
|
230
|
+
async name => {
|
|
231
|
+
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
232
|
+
|
|
233
|
+
const handlers = new Map<string, IpcHandler>();
|
|
234
|
+
const ipcMain = {
|
|
235
|
+
handle: jest.fn((channel: string, handler: IpcHandler) => {
|
|
236
|
+
handlers.set(channel, handler);
|
|
237
|
+
}),
|
|
238
|
+
removeHandler: jest.fn((channel: string) => {
|
|
239
|
+
handlers.delete(channel);
|
|
240
|
+
}),
|
|
241
|
+
};
|
|
242
|
+
const noble = Object.assign(new EventEmitter(), {
|
|
243
|
+
state: 'poweredOn',
|
|
244
|
+
startScanning: jest.fn(),
|
|
245
|
+
stopScanning: jest.fn(callback => callback?.()),
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
249
|
+
jest.doMock('electron', () => ({ ipcMain }));
|
|
250
|
+
jest.doMock('electron-log', () => ({
|
|
251
|
+
info: jest.fn(),
|
|
252
|
+
debug: jest.fn(),
|
|
253
|
+
error: jest.fn(),
|
|
254
|
+
}));
|
|
255
|
+
|
|
256
|
+
const { setupNobleBleHandlers } = await import('../noble-ble-handler');
|
|
257
|
+
setupNobleBleHandlers({
|
|
258
|
+
on: jest.fn(),
|
|
259
|
+
send: jest.fn(),
|
|
260
|
+
} as unknown as WebContents);
|
|
261
|
+
|
|
262
|
+
const enumerate = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_ENUMERATE);
|
|
263
|
+
if (!enumerate) {
|
|
264
|
+
throw new Error('Electron Noble BLE enumerate handler was not registered');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const scan = async (peripherals: ReturnType<typeof createPeripheral>[]) => {
|
|
268
|
+
let resolveScanStarted = () => undefined;
|
|
269
|
+
const scanStarted = new Promise<void>(resolve => {
|
|
270
|
+
resolveScanStarted = resolve;
|
|
271
|
+
});
|
|
272
|
+
noble.startScanning.mockImplementationOnce((_services, _duplicates, callback) => {
|
|
273
|
+
callback?.();
|
|
274
|
+
peripherals.forEach(peripheral => noble.emit('discover', peripheral));
|
|
275
|
+
resolveScanStarted();
|
|
276
|
+
});
|
|
277
|
+
const devices = Promise.resolve(enumerate());
|
|
278
|
+
await scanStarted;
|
|
279
|
+
jest.advanceTimersByTime(5_000);
|
|
280
|
+
return devices;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const firstDevice = createPeripheral('first-device', name);
|
|
284
|
+
const secondDevice = createPeripheral('second-device', 'Pro2 C3D4');
|
|
285
|
+
await expect(scan([firstDevice])).resolves.toEqual([
|
|
286
|
+
expect.objectContaining({ id: 'first-device', name }),
|
|
287
|
+
]);
|
|
288
|
+
await expect(scan([secondDevice, secondDevice])).resolves.toEqual([
|
|
289
|
+
expect.objectContaining({ id: 'first-device', name }),
|
|
290
|
+
expect.objectContaining({ id: 'second-device', name: 'Pro2 C3D4' }),
|
|
291
|
+
]);
|
|
292
|
+
await expect(scan([])).resolves.toHaveLength(2);
|
|
293
|
+
|
|
294
|
+
noble.state = 'poweredOff';
|
|
295
|
+
noble.emit('stateChange', 'poweredOff');
|
|
296
|
+
noble.state = 'poweredOn';
|
|
297
|
+
noble.emit('stateChange', 'poweredOn');
|
|
298
|
+
await expect(scan([])).resolves.toEqual([]);
|
|
299
|
+
}
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
test.each(['Pro A1B2', 'Pro2 A1B2'])(
|
|
303
|
+
'refreshes a retained disconnected %s peripheral before cold connecting',
|
|
304
|
+
async name => {
|
|
305
|
+
const handlers = new Map<string, IpcHandler>();
|
|
306
|
+
const stalePeripheral = Object.assign(new EventEmitter(), createPeripheral('device', name), {
|
|
307
|
+
connect: jest.fn(),
|
|
308
|
+
});
|
|
309
|
+
const freshPeripheral = Object.assign(new EventEmitter(), createPeripheral('device', name), {
|
|
310
|
+
connect: jest.fn((callback: (error?: Error) => void) => {
|
|
311
|
+
callback(new Error('expected fresh connection failure'));
|
|
312
|
+
}),
|
|
313
|
+
});
|
|
314
|
+
const noble = Object.assign(new EventEmitter(), {
|
|
315
|
+
state: 'poweredOn',
|
|
316
|
+
startScanning: jest.fn((_services, _duplicates, callback) => {
|
|
317
|
+
callback?.();
|
|
318
|
+
noble.emit('discover', freshPeripheral);
|
|
319
|
+
}),
|
|
320
|
+
stopScanning: jest.fn(callback => callback?.()),
|
|
321
|
+
connectAsync: jest.fn(() => Promise.resolve(undefined)),
|
|
322
|
+
});
|
|
323
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
324
|
+
jest.doMock('electron', () => ({
|
|
325
|
+
ipcMain: {
|
|
326
|
+
handle: (channel: string, handler: IpcHandler) => handlers.set(channel, handler),
|
|
327
|
+
removeHandler: (channel: string) => handlers.delete(channel),
|
|
328
|
+
},
|
|
329
|
+
}));
|
|
330
|
+
jest.doMock('electron-log', () => ({
|
|
331
|
+
info: jest.fn(),
|
|
332
|
+
debug: jest.fn(),
|
|
333
|
+
error: jest.fn(),
|
|
334
|
+
}));
|
|
335
|
+
const { setupNobleBleHandlers } = await import('../noble-ble-handler');
|
|
336
|
+
setupNobleBleHandlers({ on: jest.fn(), send: jest.fn() } as unknown as WebContents);
|
|
337
|
+
const availability = handlers.get(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK);
|
|
338
|
+
const connect = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT);
|
|
339
|
+
if (!availability || !connect) throw new Error('Noble handlers were not registered');
|
|
340
|
+
await availability();
|
|
341
|
+
noble.emit('discover', stalePeripheral);
|
|
342
|
+
|
|
343
|
+
await expect(Promise.resolve(connect(undefined, stalePeripheral.id))).resolves.toMatchObject({
|
|
344
|
+
success: false,
|
|
345
|
+
error: { message: 'expected fresh connection failure' },
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
expect(stalePeripheral.connect).not.toHaveBeenCalled();
|
|
349
|
+
expect(freshPeripheral.connect).toHaveBeenCalledTimes(1);
|
|
350
|
+
expect(noble.startScanning).toHaveBeenCalledTimes(1);
|
|
351
|
+
expect(noble.connectAsync).toHaveBeenCalledTimes(name.startsWith('Pro2') ? 1 : 0);
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
test.each([
|
|
356
|
+
{ rediscovered: false, expectedCode: HardwareErrorCode.BleBondInvalid },
|
|
357
|
+
{ rediscovered: true, expectedCode: HardwareErrorCode.BleConnectedError },
|
|
358
|
+
])(
|
|
359
|
+
'reports a macOS stale bond only when fallback scan misses (rediscovered=$rediscovered)',
|
|
360
|
+
async ({ rediscovered, expectedCode }) => {
|
|
361
|
+
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
362
|
+
const handlers = new Map<string, IpcHandler>();
|
|
363
|
+
const stalePeripheral = createPeripheral('device', 'Pro2 A1B2');
|
|
364
|
+
const freshPeripheral = Object.assign(
|
|
365
|
+
new EventEmitter(),
|
|
366
|
+
createPeripheral('device', 'Pro2 A1B2'),
|
|
367
|
+
{
|
|
368
|
+
connect: jest.fn((callback: (error?: Error) => void) => {
|
|
369
|
+
callback(new Error('fresh connection failed'));
|
|
370
|
+
}),
|
|
371
|
+
}
|
|
372
|
+
);
|
|
373
|
+
const staleBondError = Object.assign(new Error('Peer removed pairing information'), {
|
|
374
|
+
nativeErrorCode: 14,
|
|
375
|
+
nativeErrorDomain: 'CBErrorDomain',
|
|
376
|
+
});
|
|
377
|
+
let scanStarted: (() => void) | undefined;
|
|
378
|
+
const scanning = new Promise<void>(resolve => {
|
|
379
|
+
scanStarted = resolve;
|
|
380
|
+
});
|
|
381
|
+
const noble = Object.assign(new EventEmitter(), {
|
|
382
|
+
state: 'poweredOn',
|
|
383
|
+
startScanning: jest.fn((_services, _duplicates, callback) => {
|
|
384
|
+
callback?.();
|
|
385
|
+
if (rediscovered) noble.emit('discover', freshPeripheral);
|
|
386
|
+
scanStarted?.();
|
|
387
|
+
}),
|
|
388
|
+
stopScanning: jest.fn(callback => callback?.()),
|
|
389
|
+
connectAsync: jest.fn(() => Promise.reject(staleBondError)),
|
|
390
|
+
});
|
|
391
|
+
jest.doMock('@stoprocent/noble', () => noble);
|
|
392
|
+
jest.doMock('electron', () => ({
|
|
393
|
+
ipcMain: {
|
|
394
|
+
handle: (channel: string, handler: IpcHandler) => handlers.set(channel, handler),
|
|
395
|
+
removeHandler: (channel: string) => handlers.delete(channel),
|
|
396
|
+
},
|
|
397
|
+
}));
|
|
398
|
+
jest.doMock('electron-log', () => ({
|
|
399
|
+
info: jest.fn(),
|
|
400
|
+
debug: jest.fn(),
|
|
401
|
+
error: jest.fn(),
|
|
402
|
+
}));
|
|
403
|
+
const { setupNobleBleHandlers } = await import('../noble-ble-handler');
|
|
404
|
+
setupNobleBleHandlers({ on: jest.fn(), send: jest.fn() } as unknown as WebContents);
|
|
405
|
+
const availability = handlers.get(EOneKeyBleMessageKeys.BLE_AVAILABILITY_CHECK);
|
|
406
|
+
const connect = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT);
|
|
407
|
+
if (!availability || !connect) throw new Error('Noble handlers were not registered');
|
|
408
|
+
await availability();
|
|
409
|
+
noble.emit('discover', stalePeripheral);
|
|
410
|
+
|
|
411
|
+
const connecting = Promise.resolve(connect(undefined, stalePeripheral.id));
|
|
412
|
+
await scanning;
|
|
413
|
+
jest.advanceTimersByTime(1500);
|
|
414
|
+
await expect(connecting).resolves.toMatchObject({
|
|
415
|
+
success: false,
|
|
416
|
+
error: { errorCode: expectedCode },
|
|
417
|
+
});
|
|
418
|
+
if (!rediscovered) {
|
|
419
|
+
const secondScan = new Promise<void>(resolve => {
|
|
420
|
+
scanStarted = resolve;
|
|
421
|
+
});
|
|
422
|
+
const retry = Promise.resolve(connect(undefined, stalePeripheral.id));
|
|
423
|
+
await secondScan;
|
|
424
|
+
jest.advanceTimersByTime(1500);
|
|
425
|
+
await expect(retry).resolves.toMatchObject({
|
|
426
|
+
success: false,
|
|
427
|
+
error: { errorCode: HardwareErrorCode.BleBondInvalid },
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
expect(noble.connectAsync).toHaveBeenCalledTimes(rediscovered ? 1 : 2);
|
|
431
|
+
expect(noble.startScanning).toHaveBeenCalledTimes(rediscovered ? 1 : 2);
|
|
432
|
+
expect(freshPeripheral.connect).toHaveBeenCalledTimes(rediscovered ? 1 : 0);
|
|
433
|
+
}
|
|
434
|
+
);
|
|
435
|
+
|
|
228
436
|
test('waits for a targeted scan to stop before connecting', async () => {
|
|
229
437
|
jest.useFakeTimers({ doNotFake: ['performance'] });
|
|
230
438
|
|
|
@@ -578,7 +786,7 @@ describe('Electron Noble BLE device discovery', () => {
|
|
|
578
786
|
type: 'NobleBleIpcError',
|
|
579
787
|
success: false,
|
|
580
788
|
error: {
|
|
581
|
-
errorCode: HardwareErrorCode.
|
|
789
|
+
errorCode: HardwareErrorCode.BleDeviceNotBonded,
|
|
582
790
|
params: {
|
|
583
791
|
nativeErrorMessage: 'Notification subscription failed: Encryption is insufficient',
|
|
584
792
|
},
|
|
@@ -846,9 +1054,12 @@ describe('Noble BLE process shutdown', () => {
|
|
|
846
1054
|
discoverServices: jest.fn(),
|
|
847
1055
|
});
|
|
848
1056
|
native.emit('discover', peripheral);
|
|
1057
|
+
native.startScanning.mockImplementationOnce((_uuids, _duplicates, callback) => {
|
|
1058
|
+
callback?.();
|
|
1059
|
+
native.emit('discover', peripheral);
|
|
1060
|
+
});
|
|
849
1061
|
const connecting = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT)?.({}, peripheral.id);
|
|
850
|
-
await
|
|
851
|
-
await Promise.resolve();
|
|
1062
|
+
await flushCallbacks();
|
|
852
1063
|
expect(peripheral.connect).toHaveBeenCalledTimes(1);
|
|
853
1064
|
const disposing = sdk.disposeNobleBleSupport();
|
|
854
1065
|
await flushCallbacks();
|
|
@@ -886,7 +1097,13 @@ describe('Noble BLE process shutdown', () => {
|
|
|
886
1097
|
);
|
|
887
1098
|
const cancelConnect = jest.fn();
|
|
888
1099
|
if (route === 'direct') Object.assign(native, { connectAsync, cancelConnect });
|
|
889
|
-
else
|
|
1100
|
+
else {
|
|
1101
|
+
native.emit('discover', peripheral);
|
|
1102
|
+
native.startScanning.mockImplementationOnce((_uuids, _duplicates, callback) => {
|
|
1103
|
+
callback?.();
|
|
1104
|
+
native.emit('discover', peripheral);
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
890
1107
|
const connecting = handlers.get(EOneKeyBleMessageKeys.NOBLE_BLE_CONNECT)?.({}, peripheral.id);
|
|
891
1108
|
await flushCallbacks();
|
|
892
1109
|
if (route === 'direct') {
|
package/src/noble-ble-handler.ts
CHANGED
|
@@ -75,8 +75,7 @@ type NobleBleNativeError = Error & {
|
|
|
75
75
|
export function createNobleBleConnectionError(error: NobleBleNativeError, messagePrefix = '') {
|
|
76
76
|
const errorMessage = error.message;
|
|
77
77
|
const isInvalidMacOsBond =
|
|
78
|
-
|
|
79
|
-
(error.nativeErrorCode === 15 && error.nativeErrorDomain === 'CBATTErrorDomain');
|
|
78
|
+
error.nativeErrorCode === 14 && error.nativeErrorDomain === 'CBErrorDomain';
|
|
80
79
|
if (isInvalidMacOsBond) {
|
|
81
80
|
const nativeErrorMessage = `${messagePrefix}${errorMessage}`;
|
|
82
81
|
return ERRORS.TypedError(
|
|
@@ -1167,8 +1166,7 @@ async function enumerateDevices(isWindowDestroyed: () => boolean): Promise<Devic
|
|
|
1167
1166
|
|
|
1168
1167
|
logger?.info('[NobleBLE] Starting device enumeration');
|
|
1169
1168
|
|
|
1170
|
-
//
|
|
1171
|
-
discoveredDevices.clear();
|
|
1169
|
+
// Keep prior discoveries when a polling round misses an advertisement.
|
|
1172
1170
|
|
|
1173
1171
|
// Ensure discover listener is properly set up before scanning
|
|
1174
1172
|
// This is crucial to fix the issue where devices are not found after web-usb failures
|
|
@@ -1743,11 +1741,15 @@ async function tryDirectConnectById(deviceId: string): Promise<Peripheral | unde
|
|
|
1743
1741
|
logger?.info('[NobleBLE] Direct connect-by-id succeeded', { deviceId });
|
|
1744
1742
|
return peripheral;
|
|
1745
1743
|
} catch (error) {
|
|
1746
|
-
directConnectCooldownUntil.set(deviceId, Date.now() + DIRECT_CONNECT_COOLDOWN_MS);
|
|
1747
1744
|
logger?.info('[NobleBLE] Direct connect-by-id failed, falling back to scan', {
|
|
1748
1745
|
deviceId,
|
|
1749
1746
|
error: String(error),
|
|
1750
1747
|
});
|
|
1748
|
+
const nativeError = error as NobleBleNativeError;
|
|
1749
|
+
if (nativeError.nativeErrorCode === 14 && nativeError.nativeErrorDomain === 'CBErrorDomain') {
|
|
1750
|
+
throw createNobleBleConnectionError(nativeError);
|
|
1751
|
+
}
|
|
1752
|
+
directConnectCooldownUntil.set(deviceId, Date.now() + DIRECT_CONNECT_COOLDOWN_MS);
|
|
1751
1753
|
return undefined;
|
|
1752
1754
|
} finally {
|
|
1753
1755
|
clearTimeout(timer);
|
|
@@ -1766,8 +1768,12 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1766
1768
|
totalConnected: connectedDevices.size,
|
|
1767
1769
|
});
|
|
1768
1770
|
|
|
1769
|
-
//
|
|
1770
|
-
|
|
1771
|
+
// Retained discoveries are display metadata, not proof of a reusable native link.
|
|
1772
|
+
// Prefer the live connection when an older discovery exists for the same device.
|
|
1773
|
+
let peripheral = connectedDevices.get(deviceId) ?? discoveredDevices.get(deviceId);
|
|
1774
|
+
if (peripheral?.state !== 'connected') {
|
|
1775
|
+
peripheral = undefined;
|
|
1776
|
+
}
|
|
1771
1777
|
|
|
1772
1778
|
if (!peripheral) {
|
|
1773
1779
|
// Initialize Noble if not already done
|
|
@@ -1806,12 +1812,21 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1806
1812
|
}
|
|
1807
1813
|
};
|
|
1808
1814
|
|
|
1815
|
+
let staleBondError: Error | undefined;
|
|
1809
1816
|
const connectById = async () => {
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1817
|
+
try {
|
|
1818
|
+
const found = await tryDirectConnectById(deviceId);
|
|
1819
|
+
if (found) {
|
|
1820
|
+
discoveredDevices.set(deviceId, found);
|
|
1821
|
+
}
|
|
1822
|
+
return found;
|
|
1823
|
+
} catch (error) {
|
|
1824
|
+
if ((error as { errorCode?: number }).errorCode !== HardwareErrorCode.BleBondInvalid) {
|
|
1825
|
+
throw error;
|
|
1826
|
+
}
|
|
1827
|
+
staleBondError = error as Error;
|
|
1828
|
+
return undefined;
|
|
1813
1829
|
}
|
|
1814
|
-
return found;
|
|
1815
1830
|
};
|
|
1816
1831
|
|
|
1817
1832
|
peripheral = byIdFirst ? await connectById() : await scanForPeripheral();
|
|
@@ -1820,6 +1835,7 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1820
1835
|
// silent), or not reachable by id. Try the other one before giving up.
|
|
1821
1836
|
peripheral = byIdFirst ? await scanForPeripheral() : await connectById();
|
|
1822
1837
|
}
|
|
1838
|
+
if (!peripheral && staleBondError) throw staleBondError;
|
|
1823
1839
|
}
|
|
1824
1840
|
|
|
1825
1841
|
assertBleActive();
|
|
@@ -2117,10 +2133,18 @@ async function subscribeNotifications(
|
|
|
2117
2133
|
ms: Date.now() - subscribeStartedAt,
|
|
2118
2134
|
});
|
|
2119
2135
|
} catch (error) {
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2136
|
+
const nativeError = error as NobleBleNativeError;
|
|
2137
|
+
if (
|
|
2138
|
+
nativeError.nativeErrorCode === 15 &&
|
|
2139
|
+
nativeError.nativeErrorDomain === 'CBATTErrorDomain'
|
|
2140
|
+
) {
|
|
2141
|
+
throw ERRORS.TypedError(
|
|
2142
|
+
HardwareErrorCode.BleDeviceNotBonded,
|
|
2143
|
+
HardwareErrorCodeMessage[HardwareErrorCode.BleDeviceNotBonded],
|
|
2144
|
+
{ nativeErrorMessage: `Notification subscription failed: ${nativeError.message}` }
|
|
2145
|
+
);
|
|
2146
|
+
}
|
|
2147
|
+
throw createNobleBleConnectionError(nativeError, 'Notification subscription failed: ');
|
|
2124
2148
|
} finally {
|
|
2125
2149
|
// 🔒 CRITICAL: Always clear operation state (even on error)
|
|
2126
2150
|
if (!disposing) subscriptionOperations.set(deviceId, 'idle');
|