@onekeyfe/hd-transport-electron 1.2.0-alpha.67 → 1.2.0-alpha.69
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-packet-capacity.d.ts +2 -0
- package/dist/ble-packet-capacity.d.ts.map +1 -0
- package/dist/{index-5f728113.js → index-bd1cfdb6.js} +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +1 -1
- package/dist/{noble-ble-handler-d58af248.js → noble-ble-handler-7ac9c43d.js} +110 -64
- 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-packet-capacity.test.ts +13 -0
- package/src/__tests__/noble-ble-handler.test.ts +354 -0
- package/src/ble-packet-capacity.ts +13 -0
- package/src/noble-ble-handler.ts +123 -50
- package/src/noble-ble-timeouts.ts +2 -0
- package/src/types/desktop-api.ts +2 -1
- package/src/types/noble-extended.ts +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ble-packet-capacity.d.ts","sourceRoot":"","sources":["../src/ble-packet-capacity.ts"],"names":[],"mappings":"AAEA,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC9B,qBAAqB,EAAE,MAAM,EAC7B,sBAAsB,EAAE,MAAM,GAC7B,MAAM,CAMR"}
|
|
@@ -32,7 +32,7 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
|
|
|
32
32
|
|
|
33
33
|
function initNobleBleSupport(webContents) {
|
|
34
34
|
return __awaiter(this, void 0, void 0, function* () {
|
|
35
|
-
const { setupNobleBleHandlers } = yield Promise.resolve().then(function () { return require('./noble-ble-handler-
|
|
35
|
+
const { setupNobleBleHandlers } = yield Promise.resolve().then(function () { return require('./noble-ble-handler-7ac9c43d.js'); });
|
|
36
36
|
setupNobleBleHandlers(webContents);
|
|
37
37
|
});
|
|
38
38
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ interface DeviceInfo extends OneKeyDeviceInfoBase {
|
|
|
6
6
|
id: string;
|
|
7
7
|
name: string;
|
|
8
8
|
state: string;
|
|
9
|
+
mtu?: number;
|
|
9
10
|
}
|
|
10
11
|
interface CharacteristicPair {
|
|
11
12
|
write: Characteristic;
|
|
@@ -38,6 +39,7 @@ interface NobleBleAPI {
|
|
|
38
39
|
getDevice: (uuid: string) => Promise<{
|
|
39
40
|
id: string;
|
|
40
41
|
name: string;
|
|
42
|
+
mtu?: number;
|
|
41
43
|
} | null>;
|
|
42
44
|
connect: (uuid: string) => Promise<void>;
|
|
43
45
|
disconnect: (uuid: string) => Promise<void>;
|
|
@@ -45,6 +47,10 @@ interface NobleBleAPI {
|
|
|
45
47
|
unsubscribe: (uuid: string) => Promise<void>;
|
|
46
48
|
write: (uuid: string, data: string) => Promise<void>;
|
|
47
49
|
onNotification: (callback: (deviceId: string, data: string) => void) => () => void;
|
|
50
|
+
onMtuChanged?: (callback: (device: {
|
|
51
|
+
id: string;
|
|
52
|
+
mtu: number;
|
|
53
|
+
}) => void) => () => void;
|
|
48
54
|
onDeviceDisconnected: (callback: (device: {
|
|
49
55
|
id: string;
|
|
50
56
|
name: string;
|
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-bd1cfdb6.js');
|
|
4
4
|
var hdShared = require('@onekeyfe/hd-shared');
|
|
5
5
|
var pRetry = require('p-retry');
|
|
6
6
|
|
|
@@ -8,6 +8,14 @@ function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'defau
|
|
|
8
8
|
|
|
9
9
|
var pRetry__default = /*#__PURE__*/_interopDefaultLegacy(pRetry);
|
|
10
10
|
|
|
11
|
+
const BLE_ATT_HEADER_BYTES = 3;
|
|
12
|
+
function resolveBlePacketCapacity(mtu, maximumPacketCapacity, fallbackPacketCapacity) {
|
|
13
|
+
if (typeof mtu !== 'number' || !Number.isFinite(mtu) || mtu <= BLE_ATT_HEADER_BYTES) {
|
|
14
|
+
return fallbackPacketCapacity;
|
|
15
|
+
}
|
|
16
|
+
return Math.min(maximumPacketCapacity, Math.floor(mtu) - BLE_ATT_HEADER_BYTES);
|
|
17
|
+
}
|
|
18
|
+
|
|
11
19
|
function safeLog(logger, level, message, ...args) {
|
|
12
20
|
if (logger) {
|
|
13
21
|
logger[level](message, ...args);
|
|
@@ -78,6 +86,9 @@ function softRefreshSubscription(params) {
|
|
|
78
86
|
});
|
|
79
87
|
}
|
|
80
88
|
|
|
89
|
+
const NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS = 5000;
|
|
90
|
+
const NOBLE_BLE_CONNECTION_TIMEOUT_MS = 10000;
|
|
91
|
+
|
|
81
92
|
let noble = null;
|
|
82
93
|
let logger = null;
|
|
83
94
|
const bluetoothState = {
|
|
@@ -95,19 +106,18 @@ const notificationCallbacks = new Map();
|
|
|
95
106
|
const subscribedDevices = new Map();
|
|
96
107
|
const subscriptionOperations = new Map();
|
|
97
108
|
const deviceDisconnectListeners = new Map();
|
|
109
|
+
const deviceMtuListeners = new Map();
|
|
98
110
|
const ONEKEY_SERVICE_UUIDS = [hdShared.ONEKEY_SERVICE_UUID];
|
|
99
111
|
const ONEKEY_SERVICE_UUID_ALIASES = hdShared.createKnownBleUuidAliases(hdShared.ONEKEY_SERVICE_UUID);
|
|
100
112
|
const ONEKEY_WRITE_UUID_ALIASES = hdShared.createKnownBleUuidAliases(hdShared.ONEKEY_WRITE_CHARACTERISTIC_UUID);
|
|
101
113
|
const ONEKEY_NOTIFY_UUID_ALIASES = hdShared.createKnownBleUuidAliases(hdShared.ONEKEY_NOTIFY_CHARACTERISTIC_UUID);
|
|
102
114
|
const BLUETOOTH_INIT_TIMEOUT = 10000;
|
|
103
|
-
const DEVICE_SCAN_TIMEOUT =
|
|
104
|
-
const FAST_SCAN_TIMEOUT = 8000;
|
|
115
|
+
const DEVICE_SCAN_TIMEOUT = 5000;
|
|
105
116
|
const DEVICE_CHECK_INTERVAL = 500;
|
|
106
|
-
const CONNECTION_TIMEOUT = 8000;
|
|
107
117
|
const SERVICE_DISCOVERY_TIMEOUT = 10000;
|
|
108
118
|
const BLE_CLEANUP_TIMEOUT = 250;
|
|
109
|
-
const
|
|
110
|
-
const
|
|
119
|
+
const BLE_PACKET_SIZE_FALLBACK = 192;
|
|
120
|
+
const BLE_PACKET_SIZE_MAXIMUM = 244;
|
|
111
121
|
const RETRY_CONFIG = { MAX_ATTEMPTS: 15, WRITE_TIMEOUT: 2000 };
|
|
112
122
|
const IS_WINDOWS = process.platform === 'win32';
|
|
113
123
|
const ABORTABLE_WRITE_ERROR_PATTERNS = [
|
|
@@ -116,10 +126,14 @@ const ABORTABLE_WRITE_ERROR_PATTERNS = [
|
|
|
116
126
|
function isOneKeyPeripheral(peripheral) {
|
|
117
127
|
var _a, _b;
|
|
118
128
|
const serviceUuids = (_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.serviceUuids;
|
|
129
|
+
const localName = (_b = peripheral.advertisement) === null || _b === void 0 ? void 0 : _b.localName;
|
|
130
|
+
if (!(localName === null || localName === void 0 ? void 0 : localName.trim()) || hdShared.isPro2FindMyAdvertisementName(localName)) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
119
133
|
return (hdShared.hasOnekeyCommunicationService(serviceUuids) &&
|
|
120
134
|
hdShared.isOnekeyBluetoothDevice({
|
|
121
135
|
id: peripheral.id,
|
|
122
|
-
localName
|
|
136
|
+
localName,
|
|
123
137
|
serviceUuids,
|
|
124
138
|
}));
|
|
125
139
|
}
|
|
@@ -295,6 +309,11 @@ function cleanupDevice(deviceId, webContents, options = {}) {
|
|
|
295
309
|
disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
|
|
296
310
|
deviceDisconnectListeners.delete(deviceId);
|
|
297
311
|
}
|
|
312
|
+
const mtuEntry = deviceMtuListeners.get(deviceId);
|
|
313
|
+
if (mtuEntry) {
|
|
314
|
+
mtuEntry.peripheral.removeListener('mtu', mtuEntry.listener);
|
|
315
|
+
deviceMtuListeners.delete(deviceId);
|
|
316
|
+
}
|
|
298
317
|
connectedDevices.delete(deviceId);
|
|
299
318
|
deviceCharacteristics.delete(deviceId);
|
|
300
319
|
notificationCallbacks.delete(deviceId);
|
|
@@ -338,8 +357,25 @@ function setupDisconnectListener(peripheral, deviceId, webContents) {
|
|
|
338
357
|
};
|
|
339
358
|
deviceDisconnectListeners.set(deviceId, { peripheral, listener });
|
|
340
359
|
peripheral.on('disconnect', listener);
|
|
360
|
+
setupMtuListener(peripheral, deviceId, webContents);
|
|
341
361
|
}
|
|
342
|
-
function
|
|
362
|
+
function setupMtuListener(peripheral, deviceId, webContents) {
|
|
363
|
+
const existing = deviceMtuListeners.get(deviceId);
|
|
364
|
+
if (existing) {
|
|
365
|
+
existing.peripheral.removeListener('mtu', existing.listener);
|
|
366
|
+
}
|
|
367
|
+
const listener = (mtu) => {
|
|
368
|
+
if (!Number.isFinite(mtu) || mtu <= 0)
|
|
369
|
+
return;
|
|
370
|
+
webContents.send(hdShared.EOneKeyBleMessageKeys.NOBLE_BLE_MTU_CHANGED, {
|
|
371
|
+
id: deviceId,
|
|
372
|
+
mtu,
|
|
373
|
+
});
|
|
374
|
+
};
|
|
375
|
+
deviceMtuListeners.set(deviceId, { peripheral, listener });
|
|
376
|
+
peripheral.on('mtu', listener);
|
|
377
|
+
}
|
|
378
|
+
function writeCharacteristicWithoutResponse(deviceId, writeCharacteristic, buffer) {
|
|
343
379
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
344
380
|
return new Promise((resolve, reject) => {
|
|
345
381
|
writeCharacteristic.write(buffer, true, (error) => {
|
|
@@ -371,7 +407,7 @@ function attemptWindowsWriteUntilPaired(deviceId, doGetWriteCharacteristic, payl
|
|
|
371
407
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, `Write characteristic not available for ${deviceId}`);
|
|
372
408
|
}
|
|
373
409
|
try {
|
|
374
|
-
yield
|
|
410
|
+
yield writeCharacteristicWithoutResponse(deviceId, latestWrite, payload);
|
|
375
411
|
}
|
|
376
412
|
catch (e) {
|
|
377
413
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
@@ -439,44 +475,37 @@ function transmitHexDataToDevice(deviceId, hexData) {
|
|
|
439
475
|
}
|
|
440
476
|
const toBuffer = Buffer.from(hexData, 'hex');
|
|
441
477
|
const doGetWriteCharacteristic = () => { var _a; return (_a = deviceCharacteristics.get(deviceId)) === null || _a === void 0 ? void 0 : _a.write; };
|
|
478
|
+
const packetCapacity = resolveBlePacketCapacity(peripheral.mtu, BLE_PACKET_SIZE_MAXIMUM, BLE_PACKET_SIZE_FALLBACK);
|
|
442
479
|
if (!IS_WINDOWS || pairedDevices.has(deviceId)) {
|
|
443
480
|
const writeCharacteristic = doGetWriteCharacteristic();
|
|
444
481
|
if (!writeCharacteristic) {
|
|
445
482
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleCharacteristicNotFound, `Write characteristic not available for ${deviceId}`);
|
|
446
483
|
}
|
|
447
|
-
if (toBuffer.length <=
|
|
448
|
-
yield
|
|
449
|
-
yield writeCharacteristicWithAck(deviceId, writeCharacteristic, toBuffer);
|
|
484
|
+
if (toBuffer.length <= packetCapacity) {
|
|
485
|
+
yield writeCharacteristicWithoutResponse(deviceId, writeCharacteristic, toBuffer);
|
|
450
486
|
return;
|
|
451
487
|
}
|
|
452
488
|
for (let offset = 0; offset < toBuffer.length;) {
|
|
453
|
-
const chunkSize = Math.min(
|
|
489
|
+
const chunkSize = Math.min(packetCapacity, toBuffer.length - offset);
|
|
454
490
|
const chunk = toBuffer.subarray(offset, offset + chunkSize);
|
|
455
491
|
offset += chunkSize;
|
|
456
492
|
const latest = doGetWriteCharacteristic();
|
|
457
493
|
if (!latest) {
|
|
458
494
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleCharacteristicNotFound, `Write characteristic not available for ${deviceId}`);
|
|
459
495
|
}
|
|
460
|
-
yield
|
|
461
|
-
if (offset < toBuffer.length) {
|
|
462
|
-
yield hdShared.wait(UNIFIED_WRITE_DELAY);
|
|
463
|
-
}
|
|
496
|
+
yield writeCharacteristicWithoutResponse(deviceId, latest, chunk);
|
|
464
497
|
}
|
|
465
498
|
return;
|
|
466
499
|
}
|
|
467
|
-
if (toBuffer.length <=
|
|
468
|
-
yield hdShared.wait(UNIFIED_WRITE_DELAY);
|
|
500
|
+
if (toBuffer.length <= packetCapacity) {
|
|
469
501
|
yield attemptWindowsWriteUntilPaired(deviceId, doGetWriteCharacteristic, toBuffer, 'single');
|
|
470
502
|
return;
|
|
471
503
|
}
|
|
472
504
|
for (let offset = 0, idx = 0; offset < toBuffer.length; idx++) {
|
|
473
|
-
const chunkSize = Math.min(
|
|
505
|
+
const chunkSize = Math.min(packetCapacity, toBuffer.length - offset);
|
|
474
506
|
const chunk = toBuffer.subarray(offset, offset + chunkSize);
|
|
475
507
|
offset += chunkSize;
|
|
476
508
|
yield attemptWindowsWriteUntilPaired(deviceId, doGetWriteCharacteristic, chunk, `chunk-${idx + 1}`);
|
|
477
|
-
if (offset < toBuffer.length) {
|
|
478
|
-
yield hdShared.wait(UNIFIED_WRITE_DELAY);
|
|
479
|
-
}
|
|
480
509
|
}
|
|
481
510
|
});
|
|
482
511
|
}
|
|
@@ -509,6 +538,14 @@ function ensureDiscoverListener() {
|
|
|
509
538
|
logger === null || logger === void 0 ? void 0 : logger.debug('[NobleBLE] Discover listener already registered');
|
|
510
539
|
}
|
|
511
540
|
}
|
|
541
|
+
function waitForNobleScanStop(nobleInstance) {
|
|
542
|
+
return index.__awaiter(this, void 0, void 0, function* () {
|
|
543
|
+
yield runBleCallbackOperation(callback => nobleInstance.stopScanning(() => callback()), {
|
|
544
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
545
|
+
timeoutBehavior: 'resolve',
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
}
|
|
512
549
|
function performTargetedScan(targetDeviceId) {
|
|
513
550
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
514
551
|
if (!noble) {
|
|
@@ -517,6 +554,25 @@ function performTargetedScan(targetDeviceId) {
|
|
|
517
554
|
const nobleInstance = noble;
|
|
518
555
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Starting targeted scan for device:', targetDeviceId);
|
|
519
556
|
return new Promise((resolve, reject) => {
|
|
557
|
+
let settled = false;
|
|
558
|
+
const finish = (peripheral, error) => index.__awaiter(this, void 0, void 0, function* () {
|
|
559
|
+
if (settled)
|
|
560
|
+
return;
|
|
561
|
+
settled = true;
|
|
562
|
+
if (timeoutId)
|
|
563
|
+
clearTimeout(timeoutId);
|
|
564
|
+
nobleInstance.removeListener('discover', onDiscover);
|
|
565
|
+
yield waitForNobleScanStop(nobleInstance);
|
|
566
|
+
if (error) {
|
|
567
|
+
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to start targeted scan:', error);
|
|
568
|
+
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleScanError, error.message));
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
if (peripheral) {
|
|
572
|
+
discoveredDevices.set(peripheral.id, peripheral);
|
|
573
|
+
}
|
|
574
|
+
resolve(peripheral);
|
|
575
|
+
});
|
|
520
576
|
const onDiscover = (peripheral) => {
|
|
521
577
|
var _a;
|
|
522
578
|
if (peripheral.id === targetDeviceId && isOneKeyPeripheral(peripheral)) {
|
|
@@ -524,26 +580,17 @@ function performTargetedScan(targetDeviceId) {
|
|
|
524
580
|
id: peripheral.id,
|
|
525
581
|
name: (_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName,
|
|
526
582
|
});
|
|
527
|
-
|
|
528
|
-
nobleInstance.removeListener('discover', onDiscover);
|
|
529
|
-
nobleInstance.stopScanning();
|
|
530
|
-
discoveredDevices.set(peripheral.id, peripheral);
|
|
531
|
-
resolve(peripheral);
|
|
583
|
+
finish(peripheral).catch(reject);
|
|
532
584
|
}
|
|
533
585
|
};
|
|
534
586
|
const timeoutId = setTimeout(() => {
|
|
535
|
-
nobleInstance.removeListener('discover', onDiscover);
|
|
536
|
-
nobleInstance.stopScanning();
|
|
537
587
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
|
|
538
|
-
|
|
539
|
-
},
|
|
588
|
+
finish(null).catch(reject);
|
|
589
|
+
}, NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS);
|
|
540
590
|
nobleInstance.on('discover', onDiscover);
|
|
541
591
|
nobleInstance.startScanning([], false, (error) => {
|
|
542
592
|
if (error) {
|
|
543
|
-
|
|
544
|
-
nobleInstance.removeListener('discover', onDiscover);
|
|
545
|
-
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to start targeted scan:', error);
|
|
546
|
-
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleScanError, error.message));
|
|
593
|
+
finish(null, error).catch(reject);
|
|
547
594
|
return;
|
|
548
595
|
}
|
|
549
596
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan started for device:', targetDeviceId);
|
|
@@ -566,12 +613,12 @@ function enumerateDevices() {
|
|
|
566
613
|
return new Promise((resolve, reject) => {
|
|
567
614
|
const devices = [];
|
|
568
615
|
let intervalId;
|
|
569
|
-
const cleanup = () => {
|
|
616
|
+
const cleanup = () => index.__awaiter(this, void 0, void 0, function* () {
|
|
570
617
|
clearTimeout(timeoutId);
|
|
571
618
|
if (intervalId)
|
|
572
619
|
clearInterval(intervalId);
|
|
573
|
-
nobleInstance
|
|
574
|
-
};
|
|
620
|
+
yield waitForNobleScanStop(nobleInstance);
|
|
621
|
+
});
|
|
575
622
|
const checkDevices = () => {
|
|
576
623
|
discoveredDevices.forEach((peripheral, id) => {
|
|
577
624
|
var _a;
|
|
@@ -587,23 +634,23 @@ function enumerateDevices() {
|
|
|
587
634
|
}
|
|
588
635
|
});
|
|
589
636
|
};
|
|
590
|
-
const timeoutId = setTimeout(() => {
|
|
637
|
+
const timeoutId = setTimeout(() => index.__awaiter(this, void 0, void 0, function* () {
|
|
591
638
|
checkDevices();
|
|
592
|
-
cleanup();
|
|
639
|
+
yield cleanup();
|
|
593
640
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scan completed, found devices:', devices.length);
|
|
594
641
|
resolve(devices);
|
|
595
|
-
}, DEVICE_SCAN_TIMEOUT);
|
|
642
|
+
}), DEVICE_SCAN_TIMEOUT);
|
|
596
643
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning for OneKey BLE devices');
|
|
597
|
-
nobleInstance.startScanning([], false, (error) => {
|
|
644
|
+
nobleInstance.startScanning([], false, (error) => index.__awaiter(this, void 0, void 0, function* () {
|
|
598
645
|
if (error) {
|
|
599
|
-
cleanup();
|
|
646
|
+
yield cleanup();
|
|
600
647
|
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Failed to start scanning:', error);
|
|
601
648
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleScanError, error.message));
|
|
602
649
|
return;
|
|
603
650
|
}
|
|
604
651
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning started for OneKey devices');
|
|
605
652
|
intervalId = setInterval(checkDevices, DEVICE_CHECK_INTERVAL);
|
|
606
|
-
});
|
|
653
|
+
}));
|
|
607
654
|
});
|
|
608
655
|
});
|
|
609
656
|
}
|
|
@@ -612,10 +659,7 @@ function stopScanning() {
|
|
|
612
659
|
if (!noble)
|
|
613
660
|
return;
|
|
614
661
|
const nobleInstance = noble;
|
|
615
|
-
yield
|
|
616
|
-
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
617
|
-
timeoutBehavior: 'resolve',
|
|
618
|
-
});
|
|
662
|
+
yield waitForNobleScanStop(nobleInstance);
|
|
619
663
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Scanning stopped');
|
|
620
664
|
});
|
|
621
665
|
}
|
|
@@ -638,22 +682,12 @@ function getDevice(deviceId) {
|
|
|
638
682
|
const peripheral = discoveredDevices.get(deviceId);
|
|
639
683
|
if (peripheral) {
|
|
640
684
|
const deviceName = ((_a = peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device';
|
|
641
|
-
return {
|
|
642
|
-
commType: 'electron-ble',
|
|
643
|
-
id: peripheral.id,
|
|
644
|
-
name: deviceName,
|
|
645
|
-
state: peripheral.state || 'disconnected',
|
|
646
|
-
};
|
|
685
|
+
return Object.assign({ commType: 'electron-ble', id: peripheral.id, name: deviceName, state: peripheral.state || 'disconnected' }, (typeof peripheral.mtu === 'number' ? { mtu: peripheral.mtu } : {}));
|
|
647
686
|
}
|
|
648
687
|
const connectedPeripheral = connectedDevices.get(deviceId);
|
|
649
688
|
if (connectedPeripheral) {
|
|
650
689
|
const deviceName = ((_b = connectedPeripheral.advertisement) === null || _b === void 0 ? void 0 : _b.localName) || 'Unknown Device';
|
|
651
|
-
return {
|
|
652
|
-
commType: 'electron-ble',
|
|
653
|
-
id: connectedPeripheral.id,
|
|
654
|
-
name: deviceName,
|
|
655
|
-
state: connectedPeripheral.state || 'connected',
|
|
656
|
-
};
|
|
690
|
+
return Object.assign({ commType: 'electron-ble', id: connectedPeripheral.id, name: deviceName, state: connectedPeripheral.state || 'connected' }, (typeof connectedPeripheral.mtu === 'number' ? { mtu: connectedPeripheral.mtu } : {}));
|
|
657
691
|
}
|
|
658
692
|
return {
|
|
659
693
|
commType: 'electron-ble',
|
|
@@ -770,7 +804,7 @@ function forceReconnectPeripheral(peripheral, deviceId) {
|
|
|
770
804
|
}), { timeoutMs: BLE_CLEANUP_TIMEOUT, timeoutBehavior: 'resolve' });
|
|
771
805
|
}
|
|
772
806
|
yield runBleCallbackOperation(callback => peripheral.connect(callback), {
|
|
773
|
-
timeoutMs:
|
|
807
|
+
timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
|
|
774
808
|
timeoutBehavior: 'reject',
|
|
775
809
|
});
|
|
776
810
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Force reconnect successful');
|
|
@@ -940,12 +974,24 @@ function connectDevice(deviceId, webContents) {
|
|
|
940
974
|
return;
|
|
941
975
|
}
|
|
942
976
|
return new Promise((resolve, reject) => {
|
|
977
|
+
let connectionTimedOut = false;
|
|
943
978
|
const timeout = setTimeout(() => {
|
|
979
|
+
connectionTimedOut = true;
|
|
944
980
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, 'Connection timeout'));
|
|
945
|
-
},
|
|
981
|
+
}, NOBLE_BLE_CONNECTION_TIMEOUT_MS);
|
|
946
982
|
const connectedPeripheral = peripheral;
|
|
947
983
|
connectedPeripheral.connect((error) => index.__awaiter(this, void 0, void 0, function* () {
|
|
948
984
|
clearTimeout(timeout);
|
|
985
|
+
if (connectionTimedOut) {
|
|
986
|
+
if (!error) {
|
|
987
|
+
try {
|
|
988
|
+
connectedPeripheral.disconnect(() => undefined);
|
|
989
|
+
}
|
|
990
|
+
catch (_a) {
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
949
995
|
if (error) {
|
|
950
996
|
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection failed:', error);
|
|
951
997
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.BleConnectedError, error.message));
|
|
@@ -1064,7 +1110,7 @@ function subscribeNotifications(deviceId, callback) {
|
|
|
1064
1110
|
timeoutBehavior: 'resolve',
|
|
1065
1111
|
});
|
|
1066
1112
|
yield runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
|
|
1067
|
-
timeoutMs:
|
|
1113
|
+
timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
|
|
1068
1114
|
timeoutBehavior: 'reject',
|
|
1069
1115
|
});
|
|
1070
1116
|
notifyCharacteristic.on('data', (data) => {
|
|
@@ -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.69",
|
|
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.69",
|
|
29
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.69",
|
|
30
|
+
"@onekeyfe/hd-transport": "1.2.0-alpha.69",
|
|
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": "c6baed25917e3c3f027f83dfc23cde85400de558"
|
|
40
40
|
}
|
|
@@ -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
|
+
});
|
|
@@ -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
|
+
}
|
package/src/noble-ble-handler.ts
CHANGED
|
@@ -15,13 +15,19 @@ import {
|
|
|
15
15
|
createKnownBleUuidAliases,
|
|
16
16
|
hasOnekeyCommunicationService,
|
|
17
17
|
isOnekeyBluetoothDevice,
|
|
18
|
+
isPro2FindMyAdvertisementName,
|
|
18
19
|
matchesKnownBleUuid,
|
|
19
20
|
wait,
|
|
20
21
|
} from '@onekeyfe/hd-shared';
|
|
21
22
|
import pRetry from 'p-retry';
|
|
22
23
|
|
|
24
|
+
import { resolveBlePacketCapacity } from './ble-packet-capacity';
|
|
23
25
|
import { safeLog } from './types/noble-extended';
|
|
24
26
|
import { runBleCallbackOperation, softRefreshSubscription } from './ble-ops';
|
|
27
|
+
import {
|
|
28
|
+
NOBLE_BLE_CONNECTION_TIMEOUT_MS,
|
|
29
|
+
NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS,
|
|
30
|
+
} from './noble-ble-timeouts';
|
|
25
31
|
|
|
26
32
|
import type { IpcMainInvokeEvent, WebContents } from 'electron';
|
|
27
33
|
import type { Characteristic, Peripheral, Service } from '@stoprocent/noble';
|
|
@@ -60,6 +66,10 @@ const deviceDisconnectListeners = new Map<
|
|
|
60
66
|
string,
|
|
61
67
|
{ peripheral: Peripheral; listener: () => void }
|
|
62
68
|
>();
|
|
69
|
+
const deviceMtuListeners = new Map<
|
|
70
|
+
string,
|
|
71
|
+
{ peripheral: Peripheral; listener: (mtu: number) => void }
|
|
72
|
+
>();
|
|
63
73
|
|
|
64
74
|
// Windows-only response watchdog state moved to utils/windows-ble-recovery
|
|
65
75
|
|
|
@@ -75,16 +85,14 @@ const ONEKEY_NOTIFY_UUID_ALIASES = createKnownBleUuidAliases(ONEKEY_NOTIFY_CHARA
|
|
|
75
85
|
|
|
76
86
|
// Timeout and interval constants
|
|
77
87
|
const BLUETOOTH_INIT_TIMEOUT = 10000; // 10 seconds for Bluetooth initialization
|
|
78
|
-
const DEVICE_SCAN_TIMEOUT =
|
|
79
|
-
const FAST_SCAN_TIMEOUT = 8000; // 8 seconds for targeted scanning (Pro2 has longer advertising interval)
|
|
88
|
+
const DEVICE_SCAN_TIMEOUT = 5000; // 5 seconds for device scanning
|
|
80
89
|
const DEVICE_CHECK_INTERVAL = 500; // 500ms interval for periodic device checks
|
|
81
|
-
const CONNECTION_TIMEOUT = 8000; // 8 seconds for device connection (BLE reconnect after release can be slow)
|
|
82
90
|
const SERVICE_DISCOVERY_TIMEOUT = 10000; // 10 seconds for service discovery
|
|
83
91
|
const BLE_CLEANUP_TIMEOUT = 250;
|
|
84
92
|
|
|
85
93
|
// Write-related constants
|
|
86
|
-
const
|
|
87
|
-
const
|
|
94
|
+
const BLE_PACKET_SIZE_FALLBACK = 192;
|
|
95
|
+
const BLE_PACKET_SIZE_MAXIMUM = 244;
|
|
88
96
|
const RETRY_CONFIG = { MAX_ATTEMPTS: 15, WRITE_TIMEOUT: 2000 } as const;
|
|
89
97
|
const IS_WINDOWS = process.platform === 'win32';
|
|
90
98
|
const ABORTABLE_WRITE_ERROR_PATTERNS = [
|
|
@@ -93,11 +101,19 @@ const ABORTABLE_WRITE_ERROR_PATTERNS = [
|
|
|
93
101
|
|
|
94
102
|
function isOneKeyPeripheral(peripheral: Peripheral) {
|
|
95
103
|
const serviceUuids = peripheral.advertisement?.serviceUuids;
|
|
104
|
+
const localName = peripheral.advertisement?.localName;
|
|
105
|
+
|
|
106
|
+
// Noble localName is the current advertisement name, so reject the Pro2
|
|
107
|
+
// Find My endpoint before the communication-service fast path accepts it.
|
|
108
|
+
if (!localName?.trim() || isPro2FindMyAdvertisementName(localName)) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
96
112
|
return (
|
|
97
113
|
hasOnekeyCommunicationService(serviceUuids) &&
|
|
98
114
|
isOnekeyBluetoothDevice({
|
|
99
115
|
id: peripheral.id,
|
|
100
|
-
localName
|
|
116
|
+
localName,
|
|
101
117
|
serviceUuids,
|
|
102
118
|
})
|
|
103
119
|
);
|
|
@@ -353,6 +369,11 @@ function cleanupDevice(
|
|
|
353
369
|
disconnectEntry.peripheral.removeListener('disconnect', disconnectEntry.listener);
|
|
354
370
|
deviceDisconnectListeners.delete(deviceId);
|
|
355
371
|
}
|
|
372
|
+
const mtuEntry = deviceMtuListeners.get(deviceId);
|
|
373
|
+
if (mtuEntry) {
|
|
374
|
+
mtuEntry.peripheral.removeListener('mtu', mtuEntry.listener);
|
|
375
|
+
deviceMtuListeners.delete(deviceId);
|
|
376
|
+
}
|
|
356
377
|
connectedDevices.delete(deviceId);
|
|
357
378
|
deviceCharacteristics.delete(deviceId);
|
|
358
379
|
notificationCallbacks.delete(deviceId);
|
|
@@ -412,11 +433,33 @@ function setupDisconnectListener(
|
|
|
412
433
|
};
|
|
413
434
|
deviceDisconnectListeners.set(deviceId, { peripheral, listener });
|
|
414
435
|
peripheral.on('disconnect', listener);
|
|
436
|
+
setupMtuListener(peripheral, deviceId, webContents);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function setupMtuListener(
|
|
440
|
+
peripheral: Peripheral,
|
|
441
|
+
deviceId: string,
|
|
442
|
+
webContents: WebContents
|
|
443
|
+
): void {
|
|
444
|
+
const existing = deviceMtuListeners.get(deviceId);
|
|
445
|
+
if (existing) {
|
|
446
|
+
existing.peripheral.removeListener('mtu', existing.listener);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const listener = (mtu: number) => {
|
|
450
|
+
if (!Number.isFinite(mtu) || mtu <= 0) return;
|
|
451
|
+
webContents.send(EOneKeyBleMessageKeys.NOBLE_BLE_MTU_CHANGED, {
|
|
452
|
+
id: deviceId,
|
|
453
|
+
mtu,
|
|
454
|
+
});
|
|
455
|
+
};
|
|
456
|
+
deviceMtuListeners.set(deviceId, { peripheral, listener });
|
|
457
|
+
peripheral.on('mtu', listener);
|
|
415
458
|
}
|
|
416
459
|
|
|
417
460
|
// ===== Write helpers (inline) =====
|
|
418
461
|
|
|
419
|
-
async function
|
|
462
|
+
async function writeCharacteristicWithoutResponse(
|
|
420
463
|
deviceId: string,
|
|
421
464
|
writeCharacteristic: Characteristic,
|
|
422
465
|
buffer: Buffer
|
|
@@ -464,7 +507,7 @@ async function attemptWindowsWriteUntilPaired(
|
|
|
464
507
|
}
|
|
465
508
|
|
|
466
509
|
try {
|
|
467
|
-
await
|
|
510
|
+
await writeCharacteristicWithoutResponse(deviceId, latestWrite, payload);
|
|
468
511
|
} catch (e) {
|
|
469
512
|
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
470
513
|
logger?.error('[BLE-Write] Windows write error', {
|
|
@@ -548,6 +591,11 @@ async function transmitHexDataToDevice(deviceId: string, hexData: string): Promi
|
|
|
548
591
|
|
|
549
592
|
const toBuffer = Buffer.from(hexData, 'hex');
|
|
550
593
|
const doGetWriteCharacteristic = () => deviceCharacteristics.get(deviceId)?.write;
|
|
594
|
+
const packetCapacity = resolveBlePacketCapacity(
|
|
595
|
+
peripheral.mtu,
|
|
596
|
+
BLE_PACKET_SIZE_MAXIMUM,
|
|
597
|
+
BLE_PACKET_SIZE_FALLBACK
|
|
598
|
+
);
|
|
551
599
|
|
|
552
600
|
if (!IS_WINDOWS || pairedDevices.has(deviceId)) {
|
|
553
601
|
// macOS / Linux or already paired on Windows: direct write
|
|
@@ -558,14 +606,13 @@ async function transmitHexDataToDevice(deviceId: string, hexData: string): Promi
|
|
|
558
606
|
`Write characteristic not available for ${deviceId}`
|
|
559
607
|
);
|
|
560
608
|
}
|
|
561
|
-
if (toBuffer.length <=
|
|
562
|
-
await
|
|
563
|
-
await writeCharacteristicWithAck(deviceId, writeCharacteristic, toBuffer);
|
|
609
|
+
if (toBuffer.length <= packetCapacity) {
|
|
610
|
+
await writeCharacteristicWithoutResponse(deviceId, writeCharacteristic, toBuffer);
|
|
564
611
|
return;
|
|
565
612
|
}
|
|
566
613
|
// chunked
|
|
567
614
|
for (let offset = 0; offset < toBuffer.length; ) {
|
|
568
|
-
const chunkSize = Math.min(
|
|
615
|
+
const chunkSize = Math.min(packetCapacity, toBuffer.length - offset);
|
|
569
616
|
const chunk = toBuffer.subarray(offset, offset + chunkSize);
|
|
570
617
|
offset += chunkSize;
|
|
571
618
|
const latest = doGetWriteCharacteristic();
|
|
@@ -575,23 +622,19 @@ async function transmitHexDataToDevice(deviceId: string, hexData: string): Promi
|
|
|
575
622
|
`Write characteristic not available for ${deviceId}`
|
|
576
623
|
);
|
|
577
624
|
}
|
|
578
|
-
await
|
|
579
|
-
if (offset < toBuffer.length) {
|
|
580
|
-
await wait(UNIFIED_WRITE_DELAY);
|
|
581
|
-
}
|
|
625
|
+
await writeCharacteristicWithoutResponse(deviceId, latest, chunk);
|
|
582
626
|
}
|
|
583
627
|
return;
|
|
584
628
|
}
|
|
585
629
|
|
|
586
630
|
// Windows unpaired path: use loop
|
|
587
|
-
if (toBuffer.length <=
|
|
588
|
-
await wait(UNIFIED_WRITE_DELAY);
|
|
631
|
+
if (toBuffer.length <= packetCapacity) {
|
|
589
632
|
await attemptWindowsWriteUntilPaired(deviceId, doGetWriteCharacteristic, toBuffer, 'single');
|
|
590
633
|
return;
|
|
591
634
|
}
|
|
592
635
|
// chunked loop
|
|
593
636
|
for (let offset = 0, idx = 0; offset < toBuffer.length; idx++) {
|
|
594
|
-
const chunkSize = Math.min(
|
|
637
|
+
const chunkSize = Math.min(packetCapacity, toBuffer.length - offset);
|
|
595
638
|
const chunk = toBuffer.subarray(offset, offset + chunkSize);
|
|
596
639
|
offset += chunkSize;
|
|
597
640
|
await attemptWindowsWriteUntilPaired(
|
|
@@ -600,9 +643,6 @@ async function transmitHexDataToDevice(deviceId: string, hexData: string): Promi
|
|
|
600
643
|
chunk,
|
|
601
644
|
`chunk-${idx + 1}`
|
|
602
645
|
);
|
|
603
|
-
if (offset < toBuffer.length) {
|
|
604
|
-
await wait(UNIFIED_WRITE_DELAY);
|
|
605
|
-
}
|
|
606
646
|
}
|
|
607
647
|
}
|
|
608
648
|
|
|
@@ -641,6 +681,13 @@ function ensureDiscoverListener(): void {
|
|
|
641
681
|
}
|
|
642
682
|
}
|
|
643
683
|
|
|
684
|
+
async function waitForNobleScanStop(nobleInstance: NobleModule): Promise<void> {
|
|
685
|
+
await runBleCallbackOperation(callback => nobleInstance.stopScanning(() => callback()), {
|
|
686
|
+
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
687
|
+
timeoutBehavior: 'resolve',
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
644
691
|
// Perform targeted scan for a specific device ID
|
|
645
692
|
// Uses self-contained local listener pattern - no global state needed
|
|
646
693
|
async function performTargetedScan(targetDeviceId: string): Promise<Peripheral | null> {
|
|
@@ -654,6 +701,26 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
|
|
|
654
701
|
logger?.info('[NobleBLE] Starting targeted scan for device:', targetDeviceId);
|
|
655
702
|
|
|
656
703
|
return new Promise((resolve, reject) => {
|
|
704
|
+
let settled = false;
|
|
705
|
+
|
|
706
|
+
const finish = async (peripheral: Peripheral | null, error?: Error) => {
|
|
707
|
+
if (settled) return;
|
|
708
|
+
settled = true;
|
|
709
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
710
|
+
nobleInstance.removeListener('discover', onDiscover);
|
|
711
|
+
await waitForNobleScanStop(nobleInstance);
|
|
712
|
+
|
|
713
|
+
if (error) {
|
|
714
|
+
logger?.error('[NobleBLE] Failed to start targeted scan:', error);
|
|
715
|
+
reject(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.message));
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (peripheral) {
|
|
719
|
+
discoveredDevices.set(peripheral.id, peripheral);
|
|
720
|
+
}
|
|
721
|
+
resolve(peripheral);
|
|
722
|
+
};
|
|
723
|
+
|
|
657
724
|
// Local discover listener - only matches target device
|
|
658
725
|
const onDiscover = (peripheral: Peripheral) => {
|
|
659
726
|
if (peripheral.id === targetDeviceId && isOneKeyPeripheral(peripheral)) {
|
|
@@ -661,21 +728,14 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
|
|
|
661
728
|
id: peripheral.id,
|
|
662
729
|
name: peripheral.advertisement?.localName,
|
|
663
730
|
});
|
|
664
|
-
|
|
665
|
-
nobleInstance.removeListener('discover', onDiscover);
|
|
666
|
-
nobleInstance.stopScanning();
|
|
667
|
-
discoveredDevices.set(peripheral.id, peripheral);
|
|
668
|
-
resolve(peripheral);
|
|
731
|
+
finish(peripheral).catch(reject);
|
|
669
732
|
}
|
|
670
733
|
};
|
|
671
734
|
|
|
672
|
-
// Timeout handler - must be after onDiscover so it can reference it
|
|
673
735
|
const timeoutId = setTimeout(() => {
|
|
674
|
-
nobleInstance.removeListener('discover', onDiscover);
|
|
675
|
-
nobleInstance.stopScanning();
|
|
676
736
|
logger?.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId);
|
|
677
|
-
|
|
678
|
-
},
|
|
737
|
+
finish(null).catch(reject);
|
|
738
|
+
}, NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS);
|
|
679
739
|
|
|
680
740
|
// Add local listener for this scan
|
|
681
741
|
nobleInstance.on('discover', onDiscover);
|
|
@@ -683,10 +743,7 @@ async function performTargetedScan(targetDeviceId: string): Promise<Peripheral |
|
|
|
683
743
|
// Start scanning — no service UUID filter (Pro2 may use different service UUID)
|
|
684
744
|
nobleInstance.startScanning([], false, (error?: Error) => {
|
|
685
745
|
if (error) {
|
|
686
|
-
|
|
687
|
-
nobleInstance.removeListener('discover', onDiscover);
|
|
688
|
-
logger?.error('[NobleBLE] Failed to start targeted scan:', error);
|
|
689
|
-
reject(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.message));
|
|
746
|
+
finish(null, error).catch(reject);
|
|
690
747
|
return;
|
|
691
748
|
}
|
|
692
749
|
logger?.info('[NobleBLE] Targeted scan started for device:', targetDeviceId);
|
|
@@ -720,11 +777,13 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
|
|
|
720
777
|
const devices: DeviceInfo[] = [];
|
|
721
778
|
let intervalId: ReturnType<typeof setInterval> | undefined;
|
|
722
779
|
|
|
723
|
-
// Cleanup function: clears
|
|
724
|
-
|
|
780
|
+
// Cleanup function: clears timers and waits until Noble confirms scanning
|
|
781
|
+
// has stopped. Resolving enumerate before this callback creates a race with
|
|
782
|
+
// an immediately-following connection attempt.
|
|
783
|
+
const cleanup = async () => {
|
|
725
784
|
clearTimeout(timeoutId);
|
|
726
785
|
if (intervalId) clearInterval(intervalId);
|
|
727
|
-
nobleInstance
|
|
786
|
+
await waitForNobleScanStop(nobleInstance);
|
|
728
787
|
};
|
|
729
788
|
|
|
730
789
|
// Collect discovered devices into the devices array
|
|
@@ -744,10 +803,10 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
|
|
|
744
803
|
};
|
|
745
804
|
|
|
746
805
|
// Set timeout for scanning — use longer timeout to catch slow-advertising devices like Pro2
|
|
747
|
-
const timeoutId = setTimeout(() => {
|
|
806
|
+
const timeoutId = setTimeout(async () => {
|
|
748
807
|
// Final collection before resolving — catches devices discovered near the deadline
|
|
749
808
|
checkDevices();
|
|
750
|
-
cleanup();
|
|
809
|
+
await cleanup();
|
|
751
810
|
logger?.info('[NobleBLE] Scan completed, found devices:', devices.length);
|
|
752
811
|
resolve(devices);
|
|
753
812
|
}, DEVICE_SCAN_TIMEOUT);
|
|
@@ -755,9 +814,9 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
|
|
|
755
814
|
// Start scanning without a service UUID filter so Pro2 advertisements with
|
|
756
815
|
// short vendor UUIDs can be found, but only OneKey candidates are logged/returned.
|
|
757
816
|
logger?.info('[NobleBLE] Scanning for OneKey BLE devices');
|
|
758
|
-
nobleInstance.startScanning([], false, (error?: Error) => {
|
|
817
|
+
nobleInstance.startScanning([], false, async (error?: Error) => {
|
|
759
818
|
if (error) {
|
|
760
|
-
cleanup();
|
|
819
|
+
await cleanup();
|
|
761
820
|
logger?.error('[NobleBLE] Failed to start scanning:', error);
|
|
762
821
|
reject(ERRORS.TypedError(HardwareErrorCode.BleScanError, error.message));
|
|
763
822
|
return;
|
|
@@ -775,10 +834,7 @@ async function enumerateDevices(): Promise<DeviceInfo[]> {
|
|
|
775
834
|
async function stopScanning(): Promise<void> {
|
|
776
835
|
if (!noble) return;
|
|
777
836
|
const nobleInstance = noble;
|
|
778
|
-
await
|
|
779
|
-
timeoutMs: BLE_CLEANUP_TIMEOUT,
|
|
780
|
-
timeoutBehavior: 'resolve',
|
|
781
|
-
});
|
|
837
|
+
await waitForNobleScanStop(nobleInstance);
|
|
782
838
|
logger?.info('[NobleBLE] Scanning stopped');
|
|
783
839
|
}
|
|
784
840
|
|
|
@@ -808,6 +864,7 @@ function getDevice(deviceId: string): DeviceInfo | null {
|
|
|
808
864
|
id: peripheral.id,
|
|
809
865
|
name: deviceName,
|
|
810
866
|
state: peripheral.state || 'disconnected',
|
|
867
|
+
...(typeof peripheral.mtu === 'number' ? { mtu: peripheral.mtu } : {}),
|
|
811
868
|
};
|
|
812
869
|
}
|
|
813
870
|
|
|
@@ -820,6 +877,7 @@ function getDevice(deviceId: string): DeviceInfo | null {
|
|
|
820
877
|
id: connectedPeripheral.id,
|
|
821
878
|
name: deviceName,
|
|
822
879
|
state: connectedPeripheral.state || 'connected',
|
|
880
|
+
...(typeof connectedPeripheral.mtu === 'number' ? { mtu: connectedPeripheral.mtu } : {}),
|
|
823
881
|
};
|
|
824
882
|
}
|
|
825
883
|
|
|
@@ -996,7 +1054,7 @@ async function forceReconnectPeripheral(peripheral: Peripheral, deviceId: string
|
|
|
996
1054
|
|
|
997
1055
|
// Step 3: Re-establish connection
|
|
998
1056
|
await runBleCallbackOperation(callback => peripheral.connect(callback), {
|
|
999
|
-
timeoutMs:
|
|
1057
|
+
timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
|
|
1000
1058
|
timeoutBehavior: 'reject',
|
|
1001
1059
|
});
|
|
1002
1060
|
logger?.info('[NobleBLE] Force reconnect successful');
|
|
@@ -1268,15 +1326,30 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1268
1326
|
}
|
|
1269
1327
|
|
|
1270
1328
|
return new Promise((resolve, reject) => {
|
|
1329
|
+
let connectionTimedOut = false;
|
|
1271
1330
|
const timeout = setTimeout(() => {
|
|
1331
|
+
connectionTimedOut = true;
|
|
1272
1332
|
reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError, 'Connection timeout'));
|
|
1273
|
-
},
|
|
1333
|
+
}, NOBLE_BLE_CONNECTION_TIMEOUT_MS);
|
|
1274
1334
|
|
|
1275
1335
|
// TypeScript type assertion - peripheral is guaranteed to be defined at this point
|
|
1276
1336
|
const connectedPeripheral = peripheral as Peripheral;
|
|
1277
1337
|
connectedPeripheral.connect(async (error: Error | undefined) => {
|
|
1278
1338
|
clearTimeout(timeout);
|
|
1279
1339
|
|
|
1340
|
+
// Noble may invoke the callback after the SDK timed out and released the request.
|
|
1341
|
+
// Ignore it to avoid initializing disposed commands or leaving an orphaned connection.
|
|
1342
|
+
if (connectionTimedOut) {
|
|
1343
|
+
if (!error) {
|
|
1344
|
+
try {
|
|
1345
|
+
connectedPeripheral.disconnect(() => undefined);
|
|
1346
|
+
} catch {
|
|
1347
|
+
// Best-effort cleanup only.
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1280
1353
|
if (error) {
|
|
1281
1354
|
logger?.error('[NobleBLE] Connection failed:', error);
|
|
1282
1355
|
reject(ERRORS.TypedError(HardwareErrorCode.BleConnectedError, error.message));
|
|
@@ -1450,7 +1523,7 @@ async function subscribeNotifications(
|
|
|
1450
1523
|
timeoutBehavior: 'resolve',
|
|
1451
1524
|
});
|
|
1452
1525
|
await runBleCallbackOperation(callback => notifyCharacteristic.subscribe(callback), {
|
|
1453
|
-
timeoutMs:
|
|
1526
|
+
timeoutMs: NOBLE_BLE_CONNECTION_TIMEOUT_MS,
|
|
1454
1527
|
timeoutBehavior: 'reject',
|
|
1455
1528
|
});
|
|
1456
1529
|
|
package/src/types/desktop-api.ts
CHANGED
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
// Noble BLE API interface - core BLE functionality
|
|
7
7
|
export interface NobleBleAPI {
|
|
8
8
|
enumerate: () => Promise<{ id: string; name: string }[]>;
|
|
9
|
-
getDevice: (uuid: string) => Promise<{ id: string; name: string } | null>;
|
|
9
|
+
getDevice: (uuid: string) => Promise<{ id: string; name: string; mtu?: number } | null>;
|
|
10
10
|
connect: (uuid: string) => Promise<void>;
|
|
11
11
|
disconnect: (uuid: string) => Promise<void>;
|
|
12
12
|
subscribe: (uuid: string) => Promise<void>;
|
|
13
13
|
unsubscribe: (uuid: string) => Promise<void>;
|
|
14
14
|
write: (uuid: string, data: string) => Promise<void>;
|
|
15
15
|
onNotification: (callback: (deviceId: string, data: string) => void) => () => void;
|
|
16
|
+
onMtuChanged?: (callback: (device: { id: string; mtu: number }) => void) => () => void;
|
|
16
17
|
onDeviceDisconnected: (callback: (device: { id: string; name: string }) => void) => () => void;
|
|
17
18
|
checkAvailability: () => Promise<{
|
|
18
19
|
available: boolean;
|