@onekeyfe/hd-transport-electron 1.2.0-alpha.162 → 1.2.0-alpha.163
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-b37d926c.js → index-b18020df.js} +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/{noble-ble-handler-e8b87173.js → noble-ble-handler-8b4037fe.js} +38 -35
- package/dist/noble-ble-handler.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/noble-ble-handler.ts +87 -46
- package/src/types/noble-extended.ts +1 -0
|
@@ -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-8b4037fe.js'); });
|
|
36
36
|
setupNobleBleHandlers(webContents);
|
|
37
37
|
});
|
|
38
38
|
}
|
package/dist/index.d.ts
CHANGED
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-b18020df.js');
|
|
4
4
|
var hdShared = require('@onekeyfe/hd-shared');
|
|
5
5
|
var pRetry = require('p-retry');
|
|
6
6
|
|
|
@@ -116,7 +116,8 @@ const DEVICE_SCAN_TIMEOUT = 5000;
|
|
|
116
116
|
const DEVICE_CHECK_INTERVAL = 500;
|
|
117
117
|
const SERVICE_DISCOVERY_TIMEOUT = 10000;
|
|
118
118
|
const BLE_CLEANUP_TIMEOUT = 250;
|
|
119
|
-
const
|
|
119
|
+
const BLE_DISCONNECT_CONFIRM_TIMEOUT_MS = 3000;
|
|
120
|
+
const BLE_IDLE_DISCONNECT_MS = 20000;
|
|
120
121
|
const BLE_BUSY_BACKSTOP_MS = 10 * 60000;
|
|
121
122
|
const BLE_PACKET_SIZE_FALLBACK = 192;
|
|
122
123
|
const BLE_PACKET_SIZE_MAXIMUM = 244;
|
|
@@ -242,6 +243,7 @@ function initializeNoble() {
|
|
|
242
243
|
try {
|
|
243
244
|
noble = require('@stoprocent/noble');
|
|
244
245
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Noble library loaded');
|
|
246
|
+
setupPersistentStateListener();
|
|
245
247
|
yield new Promise((resolve, reject) => {
|
|
246
248
|
if (!noble) {
|
|
247
249
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not initialized'));
|
|
@@ -251,7 +253,6 @@ function initializeNoble() {
|
|
|
251
253
|
resolve();
|
|
252
254
|
return;
|
|
253
255
|
}
|
|
254
|
-
setupPersistentStateListener();
|
|
255
256
|
const timeout = setTimeout(() => {
|
|
256
257
|
reject(hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Bluetooth initialization timeout'));
|
|
257
258
|
}, BLUETOOTH_INIT_TIMEOUT);
|
|
@@ -336,7 +337,14 @@ function armIdleDisconnect(deviceId, ms = BLE_IDLE_DISCONNECT_MS, reason = 'idle
|
|
|
336
337
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Keep-alive timeout, disconnecting device', { deviceId, reason });
|
|
337
338
|
const peripheral = connectedDevices.get(deviceId);
|
|
338
339
|
const deviceName = ((_a = peripheral === null || peripheral === void 0 ? void 0 : peripheral.advertisement) === null || _a === void 0 ? void 0 : _a.localName) || 'Unknown Device';
|
|
339
|
-
const pending =
|
|
340
|
+
const pending = unsubscribeNotifications(deviceId)
|
|
341
|
+
.catch(error => {
|
|
342
|
+
logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Keep-alive unsubscribe failed, disconnecting anyway', {
|
|
343
|
+
deviceId,
|
|
344
|
+
error: error instanceof Error ? error.message : String(error),
|
|
345
|
+
});
|
|
346
|
+
})
|
|
347
|
+
.then(() => disconnectDevice(deviceId))
|
|
340
348
|
.then(() => {
|
|
341
349
|
broadcastToAllWebContents(hdShared.EOneKeyBleMessageKeys.BLE_DEVICE_DISCONNECTED, {
|
|
342
350
|
id: deviceId,
|
|
@@ -409,6 +417,7 @@ function handleDeviceDisconnect(deviceId, webContents) {
|
|
|
409
417
|
});
|
|
410
418
|
cleanupDevice(deviceId, webContents, {
|
|
411
419
|
cleanupConnection: true,
|
|
420
|
+
cleanupDiscoveredCache: true,
|
|
412
421
|
sendDisconnectEvent: true,
|
|
413
422
|
cancelOperations: true,
|
|
414
423
|
reason: 'auto-disconnect',
|
|
@@ -966,23 +975,18 @@ function discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId) {
|
|
|
966
975
|
}
|
|
967
976
|
function setupConnectionAndDiscoverServices(peripheral, deviceId, webContents) {
|
|
968
977
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
969
|
-
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
970
978
|
try {
|
|
971
|
-
|
|
979
|
+
yield forceReconnectPeripheral(peripheral, deviceId);
|
|
972
980
|
}
|
|
973
|
-
catch (
|
|
974
|
-
logger === null || logger === void 0 ? void 0 : logger.
|
|
975
|
-
deviceId,
|
|
976
|
-
error: initialError,
|
|
977
|
-
});
|
|
981
|
+
catch (resetError) {
|
|
982
|
+
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Connection reset before discovery failed, continuing', resetError);
|
|
978
983
|
}
|
|
979
|
-
yield forceReconnectPeripheral(peripheral, deviceId);
|
|
980
984
|
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
981
985
|
try {
|
|
982
986
|
return yield discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
983
987
|
}
|
|
984
|
-
catch (
|
|
985
|
-
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed
|
|
988
|
+
catch (discoveryError) {
|
|
989
|
+
logger === null || logger === void 0 ? void 0 : logger.error('[NobleBLE] Service discovery failed, attempting fresh scan...', discoveryError);
|
|
986
990
|
return freshScanAndDiscover(deviceId, webContents);
|
|
987
991
|
}
|
|
988
992
|
});
|
|
@@ -1042,7 +1046,7 @@ function tryDirectConnectById(deviceId) {
|
|
|
1042
1046
|
});
|
|
1043
1047
|
}
|
|
1044
1048
|
function connectDevice(deviceId, webContents) {
|
|
1045
|
-
var _a;
|
|
1049
|
+
var _a, _b;
|
|
1046
1050
|
return index.__awaiter(this, void 0, void 0, function* () {
|
|
1047
1051
|
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Connect device request:', {
|
|
1048
1052
|
deviceId,
|
|
@@ -1054,29 +1058,28 @@ function connectDevice(deviceId, webContents) {
|
|
|
1054
1058
|
});
|
|
1055
1059
|
let peripheral = (_a = discoveredDevices.get(deviceId)) !== null && _a !== void 0 ? _a : connectedDevices.get(deviceId);
|
|
1056
1060
|
if (!peripheral) {
|
|
1057
|
-
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device not discovered, attempting targeted scan for:', deviceId);
|
|
1058
1061
|
if (!noble) {
|
|
1059
1062
|
yield initializeNoble();
|
|
1060
1063
|
}
|
|
1061
1064
|
if (!noble) {
|
|
1062
1065
|
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
1063
1066
|
}
|
|
1064
|
-
|
|
1065
|
-
if (peripheral) {
|
|
1066
|
-
discoveredDevices.set(deviceId, peripheral);
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
if (!peripheral) {
|
|
1067
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device not cached, scanning for:', deviceId);
|
|
1070
1068
|
try {
|
|
1071
|
-
|
|
1072
|
-
if (!foundPeripheral) {
|
|
1073
|
-
throw hdShared.ERRORS.TypedError(hdShared.HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found even after targeted scan`);
|
|
1074
|
-
}
|
|
1075
|
-
peripheral = foundPeripheral;
|
|
1069
|
+
peripheral = (_b = (yield performTargetedScan(deviceId))) !== null && _b !== void 0 ? _b : undefined;
|
|
1076
1070
|
}
|
|
1077
|
-
catch (
|
|
1078
|
-
logger === null || logger === void 0 ? void 0 : logger.
|
|
1079
|
-
|
|
1071
|
+
catch (scanError) {
|
|
1072
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Targeted scan failed, falling back to direct connect', {
|
|
1073
|
+
deviceId,
|
|
1074
|
+
error: scanError instanceof Error ? scanError.message : String(scanError),
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
if (!peripheral) {
|
|
1078
|
+
logger === null || logger === void 0 ? void 0 : logger.info('[NobleBLE] Device not advertising, attempting direct connect by id:', deviceId);
|
|
1079
|
+
peripheral = yield tryDirectConnectById(deviceId);
|
|
1080
|
+
if (peripheral) {
|
|
1081
|
+
discoveredDevices.set(deviceId, peripheral);
|
|
1082
|
+
}
|
|
1080
1083
|
}
|
|
1081
1084
|
}
|
|
1082
1085
|
if (!peripheral) {
|
|
@@ -1176,11 +1179,15 @@ function disconnectDevice(deviceId) {
|
|
|
1176
1179
|
deviceDisconnectListeners.delete(deviceId);
|
|
1177
1180
|
}
|
|
1178
1181
|
yield runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
1179
|
-
timeoutMs:
|
|
1182
|
+
timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
|
|
1180
1183
|
timeoutBehavior: 'resolve',
|
|
1181
1184
|
});
|
|
1185
|
+
if (peripheral.state === 'connected') {
|
|
1186
|
+
logger === null || logger === void 0 ? void 0 : logger.warn('[NobleBLE] Physical disconnect did not confirm in time', { deviceId });
|
|
1187
|
+
}
|
|
1182
1188
|
cleanupDevice(deviceId, undefined, {
|
|
1183
1189
|
cleanupConnection: true,
|
|
1190
|
+
cleanupDiscoveredCache: true,
|
|
1184
1191
|
sendDisconnectEvent: false,
|
|
1185
1192
|
cancelOperations: true,
|
|
1186
1193
|
reason: 'manual-disconnect',
|
|
@@ -1391,10 +1398,6 @@ function setupNobleBleHandlers(webContents) {
|
|
|
1391
1398
|
yield disconnectDevice(deviceId).catch(() => undefined);
|
|
1392
1399
|
}
|
|
1393
1400
|
yield stopScanning().catch(() => undefined);
|
|
1394
|
-
if (noble && persistentStateListener) {
|
|
1395
|
-
noble.removeListener('stateChange', persistentStateListener);
|
|
1396
|
-
persistentStateListener = null;
|
|
1397
|
-
}
|
|
1398
1401
|
cleanupNobleListeners();
|
|
1399
1402
|
discoveredDevices.clear();
|
|
1400
1403
|
safeLog(logger, 'info', 'Noble BLE cleanup completed');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"noble-ble-handler.d.ts","sourceRoot":"","sources":["../src/noble-ble-handler.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAA+B,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"noble-ble-handler.d.ts","sourceRoot":"","sources":["../src/noble-ble-handler.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAA+B,WAAW,EAAE,MAAM,UAAU,CAAC;AAEzE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAoFhE,wBAAgB,+BAA+B,CAAC,OAAO,CAAC,EAAE,oBAAoB,UAI7E;AAwoDD,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAkMpE"}
|
|
@@ -22,6 +22,7 @@ export interface NobleModule {
|
|
|
22
22
|
export interface Logger {
|
|
23
23
|
info(message: string, ...args: any[]): void;
|
|
24
24
|
debug(message: string, ...args: any[]): void;
|
|
25
|
+
warn(message: string, ...args: any[]): void;
|
|
25
26
|
error(message: string, ...args: any[]): void;
|
|
26
27
|
}
|
|
27
28
|
export declare function safeLog(logger: Logger | null, level: 'info' | 'debug' | 'error', message: string, ...args: any[]): void;
|
|
@@ -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;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"}
|
|
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,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;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.163",
|
|
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.163",
|
|
29
|
+
"@onekeyfe/hd-shared": "1.2.0-alpha.163",
|
|
30
|
+
"@onekeyfe/hd-transport": "1.2.0-alpha.163",
|
|
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": "afed1c82345fc277cadf49677ab8371e6c6f3f9a"
|
|
40
40
|
}
|
package/src/noble-ble-handler.ts
CHANGED
|
@@ -90,8 +90,18 @@ const DEVICE_SCAN_TIMEOUT = 5000; // 5 seconds for device scanning
|
|
|
90
90
|
const DEVICE_CHECK_INTERVAL = 500; // 500ms interval for periodic device checks
|
|
91
91
|
const SERVICE_DISCOVERY_TIMEOUT = 10000; // 10 seconds for service discovery
|
|
92
92
|
const BLE_CLEANUP_TIMEOUT = 250;
|
|
93
|
+
// A physical teardown must actually reach the OS before the device can return
|
|
94
|
+
// to a clean state; 250ms routinely declared success while CoreBluetooth was
|
|
95
|
+
// still disconnecting, leaving no trace when the teardown never completed.
|
|
96
|
+
const BLE_DISCONNECT_CONFIRM_TIMEOUT_MS = 3000;
|
|
93
97
|
// Renderer release is logical only; this timer physically frees the device.
|
|
94
|
-
|
|
98
|
+
// Keep it SHORT: the Classic 1S goes protocol-deaf when a link is dropped
|
|
99
|
+
// after sitting idle for minutes (field logs 2026-08-19: every deaf window
|
|
100
|
+
// followed a 3-minute-idle disconnect, while disconnects right after traffic
|
|
101
|
+
// have never produced one across 6.5.0's per-call teardown history). A hot
|
|
102
|
+
// disconnect ~20s after the last operation stays inside the proven-safe
|
|
103
|
+
// pattern and also shrinks the window in which phones cannot see the device.
|
|
104
|
+
const BLE_IDLE_DISCONNECT_MS = 20_000;
|
|
95
105
|
// Ceiling while a call is in flight: no outstanding write, but not forever.
|
|
96
106
|
const BLE_BUSY_BACKSTOP_MS = 10 * 60_000;
|
|
97
107
|
|
|
@@ -257,6 +267,11 @@ async function initializeNoble(): Promise<void> {
|
|
|
257
267
|
noble = require('@stoprocent/noble') as NobleModule;
|
|
258
268
|
logger?.info('[NobleBLE] Noble library loaded');
|
|
259
269
|
|
|
270
|
+
// Register the process-lifetime state listener before any early return:
|
|
271
|
+
// the poweredOn fast path below would otherwise skip it for the whole
|
|
272
|
+
// session, leaving poweredOff cache/state reconciliation dead.
|
|
273
|
+
setupPersistentStateListener();
|
|
274
|
+
|
|
260
275
|
// Wait for Bluetooth to be ready
|
|
261
276
|
await new Promise<void>((resolve, reject) => {
|
|
262
277
|
if (!noble) {
|
|
@@ -269,9 +284,6 @@ async function initializeNoble(): Promise<void> {
|
|
|
269
284
|
return;
|
|
270
285
|
}
|
|
271
286
|
|
|
272
|
-
// Setup persistent state listener before initialization
|
|
273
|
-
setupPersistentStateListener();
|
|
274
|
-
|
|
275
287
|
const timeout = setTimeout(() => {
|
|
276
288
|
reject(
|
|
277
289
|
ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Bluetooth initialization timeout')
|
|
@@ -398,7 +410,21 @@ function armIdleDisconnect(
|
|
|
398
410
|
logger?.info('[NobleBLE] Keep-alive timeout, disconnecting device', { deviceId, reason });
|
|
399
411
|
const peripheral = connectedDevices.get(deviceId);
|
|
400
412
|
const deviceName = peripheral?.advertisement?.localName || 'Unknown Device';
|
|
401
|
-
|
|
413
|
+
// Unsubscribe CCCD before dropping the link, matching every other
|
|
414
|
+
// teardown path: the 1S leaves its notify session half-open when the
|
|
415
|
+
// link drops without an unsubscribe and then ignores application
|
|
416
|
+
// protocol traffic on NEW links for tens of minutes (field log
|
|
417
|
+
// 2026-08-19: the lone unsubscribe-skipping idle teardown caused a
|
|
418
|
+
// 6-attempt reconnect loop; unsubscribe-first teardowns reconnected
|
|
419
|
+
// instantly).
|
|
420
|
+
const pending = unsubscribeNotifications(deviceId)
|
|
421
|
+
.catch(error => {
|
|
422
|
+
logger?.warn('[NobleBLE] Keep-alive unsubscribe failed, disconnecting anyway', {
|
|
423
|
+
deviceId,
|
|
424
|
+
error: error instanceof Error ? error.message : String(error),
|
|
425
|
+
});
|
|
426
|
+
})
|
|
427
|
+
.then(() => disconnectDevice(deviceId))
|
|
402
428
|
.then(() => {
|
|
403
429
|
// A call is still in flight here; it must reject, not hang.
|
|
404
430
|
// Both 'idle' and 'busy-backstop' report IdleKeepAlive on purpose:
|
|
@@ -508,6 +534,9 @@ function handleDeviceDisconnect(deviceId: string, webContents: WebContents): voi
|
|
|
508
534
|
|
|
509
535
|
cleanupDevice(deviceId, webContents, {
|
|
510
536
|
cleanupConnection: true,
|
|
537
|
+
// Same stale-object hazard as manual disconnect: an externally dropped
|
|
538
|
+
// link invalidates the cached peripheral for the next reconnect.
|
|
539
|
+
cleanupDiscoveredCache: true,
|
|
511
540
|
sendDisconnectEvent: true,
|
|
512
541
|
cancelOperations: true,
|
|
513
542
|
reason: 'auto-disconnect',
|
|
@@ -1305,26 +1334,28 @@ async function setupConnectionAndDiscoverServices(
|
|
|
1305
1334
|
deviceId: string,
|
|
1306
1335
|
webContents: WebContents
|
|
1307
1336
|
): Promise<CharacteristicPair> {
|
|
1308
|
-
|
|
1309
|
-
|
|
1337
|
+
// Reset the link before any protocol traffic reaches the device. 6.5.0 did
|
|
1338
|
+
// this on every cold setup and never produced a protocol-deaf Classic 1S
|
|
1339
|
+
// (control log 2026-08-20: 16/16 links answered within ~1s, including after
|
|
1340
|
+
// a 15-minute idle). Demoting it to a discovery-failure fallback removed the
|
|
1341
|
+
// reset entirely for this device, because discovery always succeeds: the link
|
|
1342
|
+
// comes up, GATT resolves from cache, and the device then answers nothing on
|
|
1343
|
+
// a session it no longer serves. Costs ~1.3s per cold setup; a kept-alive
|
|
1344
|
+
// link with an intact subscription returns before reaching here, so reuse
|
|
1345
|
+
// inside a workflow is unaffected.
|
|
1310
1346
|
try {
|
|
1311
|
-
|
|
1312
|
-
} catch (
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
});
|
|
1347
|
+
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1348
|
+
} catch (resetError) {
|
|
1349
|
+
// A failed reset must not abort the attempt: discovery on the existing
|
|
1350
|
+
// link, then the fresh-scan fallback, still have a chance to recover.
|
|
1351
|
+
logger?.error('[NobleBLE] Connection reset before discovery failed, continuing', resetError);
|
|
1317
1352
|
}
|
|
1318
|
-
|
|
1319
|
-
await forceReconnectPeripheral(peripheral, deviceId);
|
|
1320
1353
|
setupDisconnectListener(peripheral, deviceId, webContents);
|
|
1354
|
+
|
|
1321
1355
|
try {
|
|
1322
1356
|
return await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId);
|
|
1323
|
-
} catch (
|
|
1324
|
-
logger?.error(
|
|
1325
|
-
'[NobleBLE] Service discovery failed after reconnect, attempting fresh scan...',
|
|
1326
|
-
reconnectError
|
|
1327
|
-
);
|
|
1357
|
+
} catch (discoveryError) {
|
|
1358
|
+
logger?.error('[NobleBLE] Service discovery failed, attempting fresh scan...', discoveryError);
|
|
1328
1359
|
return freshScanAndDiscover(deviceId, webContents);
|
|
1329
1360
|
}
|
|
1330
1361
|
}
|
|
@@ -1413,10 +1444,7 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1413
1444
|
// enumerate clears the discovery map; a kept-alive link outlives it.
|
|
1414
1445
|
let peripheral = discoveredDevices.get(deviceId) ?? connectedDevices.get(deviceId);
|
|
1415
1446
|
|
|
1416
|
-
// If device not discovered, try a targeted scan for this specific device
|
|
1417
1447
|
if (!peripheral) {
|
|
1418
|
-
logger?.info('[NobleBLE] Device not discovered, attempting targeted scan for:', deviceId);
|
|
1419
|
-
|
|
1420
1448
|
// Initialize Noble if not already done
|
|
1421
1449
|
if (!noble) {
|
|
1422
1450
|
await initializeNoble();
|
|
@@ -1426,27 +1454,31 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
|
|
|
1426
1454
|
throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not available');
|
|
1427
1455
|
}
|
|
1428
1456
|
|
|
1429
|
-
//
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1457
|
+
// Scan before connecting by id. A live advertisement proves the device
|
|
1458
|
+
// holds no link and will open a fresh session, and it is how 6.5.0 reached
|
|
1459
|
+
// every cold connect (control logs: found in ~52ms, 16/16 sessions
|
|
1460
|
+
// answered within ~1s). Connecting by id resolves through the OS cache
|
|
1461
|
+
// instead: on a Classic 1S that can hand back a session the device no
|
|
1462
|
+
// longer serves, where the link comes up and GATT resolves from cache but
|
|
1463
|
+
// the device answers no protocol traffic at all.
|
|
1464
|
+
logger?.info('[NobleBLE] Device not cached, scanning for:', deviceId);
|
|
1465
|
+
try {
|
|
1466
|
+
peripheral = (await performTargetedScan(deviceId)) ?? undefined;
|
|
1467
|
+
} catch (scanError) {
|
|
1468
|
+
logger?.info('[NobleBLE] Targeted scan failed, falling back to direct connect', {
|
|
1469
|
+
deviceId,
|
|
1470
|
+
error: scanError instanceof Error ? scanError.message : String(scanError),
|
|
1471
|
+
});
|
|
1433
1472
|
}
|
|
1434
|
-
}
|
|
1435
1473
|
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
`Device ${deviceId} not found even after targeted scan`
|
|
1444
|
-
);
|
|
1474
|
+
if (!peripheral) {
|
|
1475
|
+
// Not advertising: held by another host, or bonded and silent. Connect
|
|
1476
|
+
// by id is then the only way in.
|
|
1477
|
+
logger?.info('[NobleBLE] Device not advertising, attempting direct connect by id:', deviceId);
|
|
1478
|
+
peripheral = await tryDirectConnectById(deviceId);
|
|
1479
|
+
if (peripheral) {
|
|
1480
|
+
discoveredDevices.set(deviceId, peripheral);
|
|
1445
1481
|
}
|
|
1446
|
-
peripheral = foundPeripheral;
|
|
1447
|
-
} catch (error) {
|
|
1448
|
-
logger?.error('[NobleBLE] Targeted scan failed:', error);
|
|
1449
|
-
throw error;
|
|
1450
1482
|
}
|
|
1451
1483
|
}
|
|
1452
1484
|
|
|
@@ -1597,11 +1629,18 @@ async function disconnectDevice(deviceId: string): Promise<void> {
|
|
|
1597
1629
|
}
|
|
1598
1630
|
|
|
1599
1631
|
await runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), {
|
|
1600
|
-
timeoutMs:
|
|
1632
|
+
timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS,
|
|
1601
1633
|
timeoutBehavior: 'resolve',
|
|
1602
1634
|
});
|
|
1635
|
+
if (peripheral.state === 'connected') {
|
|
1636
|
+
logger?.warn('[NobleBLE] Physical disconnect did not confirm in time', { deviceId });
|
|
1637
|
+
}
|
|
1603
1638
|
cleanupDevice(deviceId, undefined, {
|
|
1604
1639
|
cleanupConnection: true,
|
|
1640
|
+
// macOS returns zero GATT services when a previously-disconnected
|
|
1641
|
+
// peripheral object is reconnected; drop the discovery cache so the next
|
|
1642
|
+
// connect resolves a fresh peripheral (direct connect by id, or scan).
|
|
1643
|
+
cleanupDiscoveredCache: true,
|
|
1605
1644
|
sendDisconnectEvent: false,
|
|
1606
1645
|
cancelOperations: true,
|
|
1607
1646
|
reason: 'manual-disconnect',
|
|
@@ -1927,10 +1966,12 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
|
|
|
1927
1966
|
}
|
|
1928
1967
|
|
|
1929
1968
|
await stopScanning().catch(() => undefined);
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1969
|
+
// persistentStateListener is process-lifetime, NOT per-window: this
|
|
1970
|
+
// destroy handler also fires on a renderer soft restart, and removing
|
|
1971
|
+
// the listener here permanently killed poweredOff/poweredOn
|
|
1972
|
+
// reconciliation for the rest of the process (nothing re-registers it:
|
|
1973
|
+
// initializeNoble early-returns once noble is loaded). The null-guard
|
|
1974
|
+
// in setupPersistentStateListener keeps it deduped to one instance.
|
|
1934
1975
|
cleanupNobleListeners();
|
|
1935
1976
|
discoveredDevices.clear();
|
|
1936
1977
|
safeLog(logger, 'info', 'Noble BLE cleanup completed');
|