Skip to content

Commit dbf5d29

Browse files
authored
feat: enhance BLE service discovery with p-retry and fresh scan fallback (#532)
* chore: add p-retry dependency to hd-transport-electron package * chore: i18n * feat: enhance BLE service discovery with p-retry and fresh scan fallback * chore: remove obsolete BLE log and main process files
1 parent 4fdbb21 commit dbf5d29

File tree

17 files changed

+230
-60
lines changed

17 files changed

+230
-60
lines changed

packages/connect-examples/electron-example/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "hardware-example",
33
"productName": "HardwareExample",
44
"executableName": "onekey-hardware-example",
5-
"version": "1.1.4-alpha.4",
5+
"version": "1.1.5",
66
"author": "OneKey",
77
"description": "End-to-end encrypted workspaces for teams",
88
"main": "dist/index.js",

packages/connect-examples/expo-example/package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "expo-example",
3-
"version": "1.1.4-alpha.4",
3+
"version": "1.1.5",
44
"scripts": {
55
"start": "cross-env CONNECT_SRC=https://localhost:8087/ yarn expo start --dev-client",
66
"android": "yarn expo run:android",
@@ -19,10 +19,10 @@
1919
"@noble/ed25519": "^2.1.0",
2020
"@noble/hashes": "^1.3.3",
2121
"@noble/secp256k1": "^1.7.1",
22-
"@onekeyfe/hd-ble-sdk": "1.1.4-alpha.4",
23-
"@onekeyfe/hd-common-connect-sdk": "1.1.4-alpha.4",
24-
"@onekeyfe/hd-core": "1.1.4-alpha.4",
25-
"@onekeyfe/hd-web-sdk": "1.1.4-alpha.4",
22+
"@onekeyfe/hd-ble-sdk": "1.1.5",
23+
"@onekeyfe/hd-common-connect-sdk": "1.1.5",
24+
"@onekeyfe/hd-core": "1.1.5",
25+
"@onekeyfe/hd-web-sdk": "1.1.5",
2626
"@onekeyfe/react-native-ble-utils": "^0.1.3",
2727
"@polkadot/util-crypto": "13.1.1",
2828
"@react-native-async-storage/async-storage": "1.21.0",

packages/connect-examples/expo-playground/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@onekeyfe/onekey-hardware-playground",
3-
"version": "1.1.4-alpha.4",
3+
"version": "1.1.5",
44
"private": true,
55
"sideEffects": [
66
"app/utils/shim.js",
@@ -17,9 +17,9 @@
1717
},
1818
"dependencies": {
1919
"@noble/hashes": "^1.8.0",
20-
"@onekeyfe/hd-core": "1.1.4-alpha.4",
21-
"@onekeyfe/hd-shared": "1.1.4-alpha.4",
22-
"@onekeyfe/hd-web-sdk": "1.1.4-alpha.4",
20+
"@onekeyfe/hd-core": "1.1.5",
21+
"@onekeyfe/hd-shared": "1.1.5",
22+
"@onekeyfe/hd-web-sdk": "1.1.5",
2323
"@radix-ui/react-checkbox": "^1.3.2",
2424
"@radix-ui/react-dialog": "^1.1.14",
2525
"@radix-ui/react-dropdown-menu": "^2.1.15",

packages/core/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@onekeyfe/hd-core",
3-
"version": "1.1.4-alpha.4",
3+
"version": "1.1.5",
44
"description": "> TODO: description",
55
"author": "OneKey",
66
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
@@ -25,8 +25,8 @@
2525
"url": "https://github.com/OneKeyHQ/hardware-js-sdk/issues"
2626
},
2727
"dependencies": {
28-
"@onekeyfe/hd-shared": "1.1.4-alpha.4",
29-
"@onekeyfe/hd-transport": "1.1.4-alpha.4",
28+
"@onekeyfe/hd-shared": "1.1.5",
29+
"@onekeyfe/hd-transport": "1.1.5",
3030
"axios": "^0.27.2",
3131
"bignumber.js": "^9.0.2",
3232
"bytebuffer": "^5.0.1",

packages/hd-ble-sdk/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@onekeyfe/hd-ble-sdk",
3-
"version": "1.1.4-alpha.4",
3+
"version": "1.1.5",
44
"author": "OneKey",
55
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
66
"license": "ISC",
@@ -20,8 +20,8 @@
2020
"lint:fix": "eslint . --fix"
2121
},
2222
"dependencies": {
23-
"@onekeyfe/hd-core": "1.1.4-alpha.4",
24-
"@onekeyfe/hd-shared": "1.1.4-alpha.4",
25-
"@onekeyfe/hd-transport-react-native": "1.1.4-alpha.4"
23+
"@onekeyfe/hd-core": "1.1.5",
24+
"@onekeyfe/hd-shared": "1.1.5",
25+
"@onekeyfe/hd-transport-react-native": "1.1.5"
2626
}
2727
}

packages/hd-common-connect-sdk/package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@onekeyfe/hd-common-connect-sdk",
3-
"version": "1.1.4-alpha.4",
3+
"version": "1.1.5",
44
"author": "OneKey",
55
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
66
"license": "ISC",
@@ -20,11 +20,11 @@
2020
"lint:fix": "eslint . --fix"
2121
},
2222
"dependencies": {
23-
"@onekeyfe/hd-core": "1.1.4-alpha.4",
24-
"@onekeyfe/hd-shared": "1.1.4-alpha.4",
25-
"@onekeyfe/hd-transport-emulator": "1.1.4-alpha.4",
26-
"@onekeyfe/hd-transport-http": "1.1.4-alpha.4",
27-
"@onekeyfe/hd-transport-lowlevel": "1.1.4-alpha.4",
28-
"@onekeyfe/hd-transport-web-device": "1.1.4-alpha.4"
23+
"@onekeyfe/hd-core": "1.1.5",
24+
"@onekeyfe/hd-shared": "1.1.5",
25+
"@onekeyfe/hd-transport-emulator": "1.1.5",
26+
"@onekeyfe/hd-transport-http": "1.1.5",
27+
"@onekeyfe/hd-transport-lowlevel": "1.1.5",
28+
"@onekeyfe/hd-transport-web-device": "1.1.5"
2929
}
3030
}

packages/hd-transport-electron/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@onekeyfe/hd-transport-electron",
3-
"version": "1.1.4-alpha.4",
3+
"version": "1.1.5",
44
"author": "OneKey",
55
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
66
"license": "MIT",
@@ -25,7 +25,7 @@
2525
"electron-log": ">=4.0.0"
2626
},
2727
"dependencies": {
28-
"@onekeyfe/hd-shared": "1.1.4-alpha.4"
28+
"@onekeyfe/hd-shared": "1.1.5"
2929
},
3030
"devDependencies": {
3131
"@types/web-bluetooth": "^0.0.17",

packages/hd-transport-electron/src/noble-ble-handler.ts

Lines changed: 180 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
import { COMMON_HEADER_SIZE } from '@onekeyfe/hd-transport';
2020
import type { WebContents, IpcMainInvokeEvent } from 'electron';
2121
import type { Peripheral, Service, Characteristic } from '@abandonware/noble';
22+
import pRetry from 'p-retry';
2223
import type { NobleModule, Logger, DeviceInfo, CharacteristicPair } from './types/noble-extended';
2324
import { safeLog } from './types/noble-extended';
2425

@@ -250,6 +251,9 @@ async function initializeNoble(): Promise<void> {
250251
return;
251252
}
252253

254+
// Setup persistent state listener before initialization
255+
setupPersistentStateListener();
256+
253257
const timeout = setTimeout(() => {
254258
reject(
255259
ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Bluetooth initialization timeout')
@@ -289,9 +293,6 @@ async function initializeNoble(): Promise<void> {
289293
handleDeviceDiscovered(peripheral);
290294
});
291295

292-
// Setup persistent state listener after initialization
293-
setupPersistentStateListener();
294-
295296
logger?.info('[NobleBLE] Noble initialized successfully');
296297
} catch (error) {
297298
logger?.error('[NobleBLE] Failed to initialize Noble:', error);
@@ -563,7 +564,7 @@ function getDevice(deviceId: string): DeviceInfo | null {
563564
};
564565
}
565566

566-
// Discover services and characteristics for a connected device
567+
// Core service discovery function (single attempt)
567568
async function discoverServicesAndCharacteristics(
568569
peripheral: Peripheral
569570
): Promise<CharacteristicPair> {
@@ -642,6 +643,168 @@ async function discoverServicesAndCharacteristics(
642643
});
643644
}
644645

646+
// Force reconnect to clear potential connection state issues
647+
async function forceReconnectPeripheral(peripheral: Peripheral, deviceId: string): Promise<void> {
648+
logger?.info('[NobleBLE] Forcing connection reset for device:', deviceId);
649+
650+
// Step 1: Force disconnect if connected
651+
if (peripheral.state === 'connected') {
652+
await new Promise<void>(resolve => {
653+
peripheral.disconnect(() => {
654+
logger?.info('[NobleBLE] Force disconnect completed');
655+
resolve();
656+
});
657+
});
658+
659+
// Wait for complete disconnection
660+
await wait(1000);
661+
}
662+
663+
// Step 2: Clear device state
664+
connectedDevices.delete(deviceId);
665+
deviceCharacteristics.delete(deviceId);
666+
devicePacketStates.delete(deviceId);
667+
subscribedDevices.delete(deviceId);
668+
subscriptionOperations.delete(deviceId);
669+
670+
// Step 3: Re-establish connection
671+
await new Promise<void>((resolve, reject) => {
672+
peripheral.connect((error: string) => {
673+
if (error) {
674+
logger?.error('[NobleBLE] Force reconnect failed:', error);
675+
reject(new Error(`Force reconnect failed: ${error}`));
676+
} else {
677+
logger?.info('[NobleBLE] Force reconnect successful');
678+
connectedDevices.set(deviceId, peripheral);
679+
resolve();
680+
}
681+
});
682+
});
683+
684+
// Wait for connection to stabilize
685+
await wait(500);
686+
}
687+
688+
// Enhanced connection with fresh peripheral rescan as last resort
689+
async function connectAndDiscoverWithFreshScan(deviceId: string): Promise<CharacteristicPair> {
690+
logger?.info('[NobleBLE] Attempting connection with fresh peripheral scan as fallback');
691+
692+
const currentPeripheral = discoveredDevices.get(deviceId);
693+
694+
// First attempt with existing peripheral
695+
if (currentPeripheral) {
696+
try {
697+
return await discoverServicesAndCharacteristicsWithRetry(currentPeripheral, deviceId);
698+
} catch (error) {
699+
logger?.error(
700+
'[NobleBLE] Service discovery failed with existing peripheral, attempting fresh scan...'
701+
);
702+
}
703+
}
704+
705+
// Last resort: Fresh scan to get new peripheral object
706+
logger?.info(
707+
'[NobleBLE] Performing fresh scan to get new peripheral object for device:',
708+
deviceId
709+
);
710+
711+
try {
712+
const freshPeripheral = await performTargetedScan(deviceId);
713+
if (!freshPeripheral) {
714+
throw new Error(`Device ${deviceId} not found in fresh scan`);
715+
}
716+
717+
// Update device maps with fresh peripheral
718+
discoveredDevices.set(deviceId, freshPeripheral);
719+
720+
// Connect to fresh peripheral
721+
await new Promise<void>((resolve, reject) => {
722+
freshPeripheral.connect((error: string) => {
723+
if (error) {
724+
reject(new Error(`Fresh peripheral connection failed: ${error}`));
725+
} else {
726+
connectedDevices.set(deviceId, freshPeripheral);
727+
resolve();
728+
}
729+
});
730+
});
731+
732+
// Attempt service discovery with fresh peripheral (single attempt)
733+
logger?.info('[NobleBLE] Attempting service discovery with fresh peripheral');
734+
await wait(1000); // Give fresh connection more time to stabilize
735+
736+
return await discoverServicesAndCharacteristics(freshPeripheral);
737+
} catch (error) {
738+
logger?.error('[NobleBLE] Fresh scan and connection failed:', error);
739+
throw error;
740+
}
741+
}
742+
743+
// Enhanced service discovery with p-retry for robust BLE connection
744+
async function discoverServicesAndCharacteristicsWithRetry(
745+
peripheral: Peripheral,
746+
deviceId: string
747+
): Promise<CharacteristicPair> {
748+
return pRetry(
749+
async attemptNumber => {
750+
logger?.info('[NobleBLE] Starting service discovery:', {
751+
deviceId,
752+
peripheralState: peripheral.state,
753+
attempt: attemptNumber,
754+
maxRetries: 5,
755+
targetUUIDs: ONEKEY_SERVICE_UUIDS,
756+
});
757+
758+
// Strategy: Force reconnect on 3rd attempt to clear potential state issues
759+
if (attemptNumber === 3) {
760+
logger?.info('[NobleBLE] Attempting force reconnect to clear connection state...');
761+
try {
762+
await forceReconnectPeripheral(peripheral, deviceId);
763+
} catch (error) {
764+
logger?.error('[NobleBLE] Force reconnect failed:', error);
765+
throw error;
766+
}
767+
}
768+
769+
// Progressive delay strategy - handled by p-retry, but add extra wait for higher attempts
770+
if (attemptNumber > 1) {
771+
logger?.info(`[NobleBLE] Service discovery retry attempt ${attemptNumber}/5`);
772+
}
773+
774+
// Verify connection state before attempting service discovery
775+
if (peripheral.state !== 'connected') {
776+
throw new Error(`Device not connected: ${peripheral.state}`);
777+
}
778+
779+
try {
780+
return await discoverServicesAndCharacteristics(peripheral);
781+
} catch (error) {
782+
logger?.error(`[NobleBLE] No services found (attempt ${attemptNumber}/5)`);
783+
784+
if (attemptNumber < 5) {
785+
logger?.error(`[NobleBLE] Will retry service discovery (attempt ${attemptNumber + 1}/5)`);
786+
}
787+
788+
throw error; // p-retry will handle the retry logic
789+
}
790+
},
791+
{
792+
retries: 4, // Total 5 attempts (initial + 4 retries)
793+
factor: 1.5, // Exponential backoff: 1000ms → 1500ms → 2250ms → 3000ms
794+
minTimeout: 1000, // Start with 1 second delay
795+
maxTimeout: 3000, // Maximum 3 seconds delay
796+
onFailedAttempt: error => {
797+
// This runs after each failed attempt
798+
logger?.error(`[NobleBLE] Service discovery attempt ${error.attemptNumber} failed:`, {
799+
message: error.message,
800+
retriesLeft: error.retriesLeft,
801+
nextRetryIn: `${Math.min(1000 * 1.5 ** error.attemptNumber, 3000)}ms`,
802+
});
803+
},
804+
}
805+
);
806+
}
807+
645808
// Connect to device - supports both discovered and direct connection modes
646809
async function connectDevice(deviceId: string, webContents: WebContents): Promise<void> {
647810
logger?.info('[NobleBLE] Connect device request:', {
@@ -746,14 +909,17 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
746909
// Continue to re-setup the connection properly
747910
}
748911

749-
// Discover services and characteristics
912+
// Discover services and characteristics with enhanced retry including fresh scan
750913
try {
751-
const characteristics = await discoverServicesAndCharacteristics(peripheral);
914+
const characteristics = await connectAndDiscoverWithFreshScan(deviceId);
752915
deviceCharacteristics.set(deviceId, characteristics);
753916
logger?.info('[NobleBLE] Device ready for communication:', deviceId);
754917
return;
755918
} catch (error) {
756-
logger?.error('[NobleBLE] Service/characteristic discovery failed:', error);
919+
logger?.error(
920+
'[NobleBLE] Service/characteristic discovery failed after all attempts:',
921+
error
922+
);
757923
throw error;
758924
}
759925
}
@@ -780,15 +946,18 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis
780946
// Set up unified disconnect listener
781947
setupDisconnectListener(connectedPeripheral, deviceId, webContents);
782948

783-
// Discover services and characteristics
784-
discoverServicesAndCharacteristics(connectedPeripheral)
949+
// Discover services and characteristics with enhanced retry including fresh scan
950+
connectAndDiscoverWithFreshScan(deviceId)
785951
.then(characteristics => {
786952
deviceCharacteristics.set(deviceId, characteristics);
787953
logger?.info('[NobleBLE] Device ready for communication:', deviceId);
788954
resolve();
789955
})
790956
.catch(error => {
791-
logger?.error('[NobleBLE] Service/characteristic discovery failed:', error);
957+
logger?.error(
958+
'[NobleBLE] Service/characteristic discovery failed after all attempts:',
959+
error
960+
);
792961
// Disconnect on failure
793962
connectedPeripheral.disconnect();
794963
reject(error);
@@ -1053,6 +1222,7 @@ async function unsubscribeNotifications(deviceId: string): Promise<void> {
10531222
// Setup IPC handlers
10541223
export function setupNobleBleHandlers(webContents: WebContents): void {
10551224
try {
1225+
console.log('NOBLE_VERSION_771');
10561226
// @ts-ignore – electron-log is only available at runtime
10571227
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require
10581228
logger = require('electron-log') as Logger;

0 commit comments

Comments
 (0)