Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
0ccaa0d
fix: drop stale peripheral cache on disconnect and keep BLE state lis…
originalix Aug 18, 2026
006f903
fix: bound BLE acquire with a desktop-scoped deadline, wire cancel in…
originalix Aug 19, 2026
d1fa81c
chore: bump version to 1.2.0-alpha.151
originalix Aug 19, 2026
f6ceaf5
fix: unsubscribe notifications before keep-alive disconnect and confi…
originalix Aug 19, 2026
6f1d6ce
chore: bump version to 1.2.0-alpha.153
originalix Aug 19, 2026
2544b0b
fix: declare warn on the transport Logger interface
originalix Aug 19, 2026
82a9616
fix: shorten keep-alive idle disconnect to 20s to stay inside the hot…
originalix Aug 19, 2026
0b81bf2
chore: bump version to 1.2.0-alpha.154
originalix Aug 19, 2026
f15c0e2
merge: onekey (report desktop USB and BLE device disconnects #891)
originalix Aug 19, 2026
881065c
chore: bump version to 1.2.0-alpha.156
originalix Aug 19, 2026
4eff565
chore: bump version to 1.2.0-alpha.158
originalix Aug 19, 2026
66fdd17
fix: reset the BLE link before protocol traffic on every cold setup
originalix Aug 20, 2026
0631611
chore: bump version to 1.2.0-alpha.161
originalix Aug 20, 2026
b1161c7
fix: scan for the advertisement before connecting a cold BLE link by id
originalix Aug 20, 2026
afed1c8
chore: bump version to 1.2.0-alpha.163
originalix Aug 20, 2026
17d6012
fix: pick the BLE cold-connect route per device family and cap the sc…
originalix Aug 20, 2026
8a6f829
chore: bump version to 1.2.0-alpha.164
originalix Aug 20, 2026
ceb65f7
merge: onekey (canonicalize Pro2 BLE names #890)
originalix Aug 20, 2026
dc82ee3
chore: bump version to 1.2.0-alpha.165
originalix Aug 20, 2026
d6be62d
fix: address BLE review findings on acquire timeout, preconnect bookk…
originalix Aug 20, 2026
3dd1b96
chore: bump version to 1.2.0-alpha.166
originalix Aug 20, 2026
72e137f
revert: defer BLE connection-path review fixes until field validation…
originalix Aug 20, 2026
dcbf3a7
chore: log BLE write, subscription and notify frames for field diagnosis
originalix Aug 20, 2026
aa45004
chore: bump version to 1.2.0-alpha.167
originalix Aug 20, 2026
25e3854
merge: onekey (#894)
originalix Aug 20, 2026
64dcbd5
fix: discover the OneKey BLE service by UUID so a stalled device is d…
originalix Aug 20, 2026
ce2d8e3
chore: bump version to 1.2.0-alpha.168
originalix Aug 20, 2026
b2fa03f
chore: bump version to 1.2.0-alpha.173
originalix Aug 20, 2026
25680af
fix: take a declared V1 protocol at face value on desktop BLE
originalix Aug 20, 2026
ea906ff
chore: bump version to 1.2.0-alpha.174
originalix Aug 20, 2026
b46341b
fix: take a declared V1 protocol at face value on mobile BLE too
originalix Aug 20, 2026
b2cb245
chore: bump version to 1.2.0-alpha.175
originalix Aug 20, 2026
f177b02
perf: shorten desktop BLE recovery by failing over to a cold reconnec…
originalix Aug 21, 2026
12d79d1
chore: bump version to 1.2.0-alpha.176
originalix Aug 21, 2026
be22a5d
merge: onekey (harden Protocol V2 firmware install polling #896)
originalix Aug 21, 2026
294d4c3
chore: bump version to 1.2.0-alpha.177
originalix Aug 21, 2026
081c18f
perf: skip the futile fresh scan while the link is still up
originalix Aug 21, 2026
5df24cf
chore: bump version to 1.2.0-alpha.178
originalix Aug 21, 2026
7148e31
test: assert a declared V1 acquire sends no probe frame
originalix Aug 21, 2026
dcdc438
chore: bump version to 1.2.0-alpha.179
originalix Aug 21, 2026
bfb070f
test: stop asserting an exact leftover timeout budget
originalix Aug 21, 2026
37976e1
chore: bump version to 1.2.0-alpha.180
originalix Aug 21, 2026
268ee94
fix: recover in-process when BLE service discovery fails
originalix Aug 21, 2026
cb79652
chore: bump version to 1.2.0-alpha.181
originalix Aug 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/connect-examples/electron-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "hardware-example",
"productName": "HardwareExample",
"executableName": "onekey-hardware-example",
"version": "1.2.0-alpha.150",
"version": "1.2.0-alpha.151",
"author": "OneKey",
"description": "OneKey Hardware SDK Electron BLE example",
"main": "dist/index.js",
Expand All @@ -21,7 +21,7 @@
"ts:check": "yarn tsc --noEmit"
},
"dependencies": {
"@onekeyfe/hd-transport-electron": "1.2.0-alpha.150",
"@onekeyfe/hd-transport-electron": "1.2.0-alpha.151",
"@stoprocent/noble": "2.3.16",
"debug": "4.3.4",
"electron-is-dev": "^3.0.1",
Expand Down
12 changes: 6 additions & 6 deletions packages/connect-examples/expo-example/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "expo-example",
"version": "1.2.0-alpha.150",
"version": "1.2.0-alpha.151",
"scripts": {
"start": "yarn expo start --dev-client",
"android": "yarn expo run:android",
Expand All @@ -19,11 +19,11 @@
"@noble/ed25519": "^2.1.0",
"@noble/hashes": "^1.3.3",
"@noble/secp256k1": "^1.7.1",
"@onekeyfe/hd-ble-sdk": "1.2.0-alpha.150",
"@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.150",
"@onekeyfe/hd-core": "1.2.0-alpha.150",
"@onekeyfe/hd-shared": "1.2.0-alpha.150",
"@onekeyfe/hd-transport": "1.2.0-alpha.150",
"@onekeyfe/hd-ble-sdk": "1.2.0-alpha.151",
"@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.151",
"@onekeyfe/hd-core": "1.2.0-alpha.151",
"@onekeyfe/hd-shared": "1.2.0-alpha.151",
"@onekeyfe/hd-transport": "1.2.0-alpha.151",
"@onekeyfe/react-native-ble-utils": "^0.1.3",
"@polkadot/util-crypto": "13.1.1",
"@react-native-async-storage/async-storage": "1.21.0",
Expand Down
10 changes: 5 additions & 5 deletions packages/connect-examples/expo-playground/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "onekey-hardware-playground",
"version": "1.2.0-alpha.150",
"version": "1.2.0-alpha.151",
"private": true,
"sideEffects": [
"app/utils/shim.js",
Expand All @@ -19,10 +19,10 @@
},
"dependencies": {
"@noble/hashes": "^1.8.0",
"@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.150",
"@onekeyfe/hd-core": "1.2.0-alpha.150",
"@onekeyfe/hd-shared": "1.2.0-alpha.150",
"@onekeyfe/hd-transport": "1.2.0-alpha.150",
"@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.151",
"@onekeyfe/hd-core": "1.2.0-alpha.151",
"@onekeyfe/hd-shared": "1.2.0-alpha.151",
"@onekeyfe/hd-transport": "1.2.0-alpha.151",
"@radix-ui/react-checkbox": "^1.3.2",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
Expand Down
2 changes: 1 addition & 1 deletion packages/connect-examples/hwk-demo/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hwk-demo",
"version": "1.2.0-alpha.150",
"version": "1.2.0-alpha.151",
"private": true,
"main": "index.js",
"scripts": {
Expand Down
6 changes: 3 additions & 3 deletions packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@onekeyfe/hd-core",
"version": "1.2.0-alpha.150",
"version": "1.2.0-alpha.151",
"description": "Core processes and APIs for communicating with OneKey hardware devices.",
"author": "OneKey",
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
Expand All @@ -25,8 +25,8 @@
"url": "https://github.com/OneKeyHQ/hardware-js-sdk/issues"
},
"dependencies": {
"@onekeyfe/hd-shared": "1.2.0-alpha.150",
"@onekeyfe/hd-transport": "1.2.0-alpha.150",
"@onekeyfe/hd-shared": "1.2.0-alpha.151",
"@onekeyfe/hd-transport": "1.2.0-alpha.151",
"axios": "1.15.2",
"bignumber.js": "^9.0.2",
"buffer": "^6.0.3",
Expand Down
103 changes: 97 additions & 6 deletions packages/core/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,10 @@ const onCallDevice = async (
if (method.payload?.onlyConnectBleDevice) {
Comment thread
originalix marked this conversation as resolved.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: [Connect-only success path still skips method disposal]

This early return now completes tracing and removes the request task, but it still bypasses the common cleanup block that calls method.dispose(). Repeated successful connect-only/preconnect calls can therefore retain method-owned resources or listeners.

Dispose the method before returning from this branch, or route the response through the same cleanup/finally path used by normal calls.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: [Connect-only success path still skips method disposal]

This early return now completes tracing and removes the request task, but it still bypasses the common cleanup block that calls method.dispose(). Repeated successful connect-only/preconnect calls can therefore retain method-owned resources or listeners.

Dispose the method before returning from this branch, or route the response through the same cleanup/finally path used by normal calls.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: [Connect-only success bypasses method disposal]

This early return now completes tracing and removes the request task, but it still bypasses the common cleanup path that calls method.dispose(). Repeated successful connect-only/preconnect calls can retain method-owned resources or listeners.

Please dispose the method before returning from this branch, or route the response through the same cleanup/finally path used by normal calls.

preWarmCallbackTask?.resolve();
Log.debug('Call API - only connect ble device: ', device?.mainId);
// This early return bypasses the normal-path releaseTask at the end of the
// call; without it the task leaks and haunts every later queue snapshot
// and cancel sweep (field log: a completed task lingered for 6 minutes).
requestQueue.releaseTask(method.responseID);
Comment thread
originalix marked this conversation as resolved.
Comment thread
originalix marked this conversation as resolved.
Comment thread
originalix marked this conversation as resolved.
Comment thread
originalix marked this conversation as resolved.
return createResponseMessage(method.responseID, true, null);
}

Expand Down Expand Up @@ -954,7 +958,60 @@ export function isMissingDetectedProtocolV2Error(method: BaseMethod, error: unkn
* If the Bluetooth connection times out, retry up to 6 times
* @param retryCount - Current retry count (default 0)
*/
async function connectDeviceForBle(method: BaseMethod, device: Device, retryCount = 0) {
// device.acquire awaits a transport reply with no deadline of its own; a
// transport that never settles (field case: Electron main lost an IPC reply,
// "reply was never sent" after 5 minutes) hangs the call forever and cancel()
// only takes effect at poll checkpoints. Race acquire against a deadline and
// the caller's abort signal so the hang is bounded and cancel is immediate.
const BLE_ACQUIRE_DEADLINE_MS = 60 * 1000;

function raceBleAcquire<T>(acquirePromise: Promise<T>, abortSignal?: AbortSignal): Promise<T> {
return new Promise<T>((resolve, reject) => {
let settled = false;
const settle = (fn: () => void) => {
if (settled) return;
settled = true;
clearTimeout(deadline);
abortSignal?.removeEventListener('abort', onAbort);
fn();
};
const onAbort = () =>
settle(() => reject(ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled)));
const deadline = setTimeout(
() =>
settle(() =>
reject(
ERRORS.TypedError(
HardwareErrorCode.BleTimeoutError,
`BLE acquire exceeded ${BLE_ACQUIRE_DEADLINE_MS}ms deadline`
)
)
),
BLE_ACQUIRE_DEADLINE_MS
);
// Attach before any early return so a late settlement of acquirePromise
// is always consumed — an abort or deadline must never leave the acquire
// rejection unhandled.
acquirePromise.then(
value => settle(() => resolve(value)),
error => settle(() => reject(error))
);
if (abortSignal) {
if (abortSignal.aborted) {
onAbort();
return;
}
abortSignal.addEventListener('abort', onAbort);
}
});
}

async function connectDeviceForBle(
method: BaseMethod,
device: Device,
abortSignal?: AbortSignal,
retryCount = 0
) {
try {
if (device.wasInterruptedByUser()) {
throw ERRORS.TypedError(HardwareErrorCode.DeviceInterruptedFromUser);
Expand All @@ -968,9 +1025,43 @@ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCoun
!device.commands ||
device.commands.disposed;
if (shouldAcquire) {
await device.acquire(method.payload.connectProtocol, {
forceProtocolDetection: method.payload.forceProtocolDetection,
});
// The deadline/abort guards are scoped to the desktop electron
// transport: its IPC acquire is the only path with a proven
// never-settling failure mode, while react-native/lowlevel acquire may
// legitimately block on a user-driven system bonding prompt for longer
// than any sane deadline. Other envs keep the plain acquire unchanged.
const useAcquireGuards = DataManager.getSettings('env') === 'desktop-web-ble';
// A cancel landing during the retry backoff must not start a new acquire.
if (useAcquireGuards && abortSignal?.aborted) {
throw ERRORS.TypedError(HardwareErrorCode.CallQueueActionCancelled);
}
if (!useAcquireGuards) {
await device.acquire(method.payload.connectProtocol, {
forceProtocolDetection: method.payload.forceProtocolDetection,
});
} else {
try {
await raceBleAcquire(
Comment thread
originalix marked this conversation as resolved.
Comment thread
originalix marked this conversation as resolved.
Comment thread
originalix marked this conversation as resolved.
Comment thread
originalix marked this conversation as resolved.
Comment thread
originalix marked this conversation as resolved.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: [Timed-out acquire can mutate a newer connection attempt]

When the deadline or abort wins, raceBleAcquire rejects but the underlying Device.acquire keeps running. The timeout cleanup only disconnects the current link and marks transport state; it does not invalidate the acquire generation. If the old acquire resolves after the retry starts, Device.acquire can still commit mainId, deviceAcquired, commands, and protocol state for the stale link, corrupting the newer session.

Invalidate the abandoned acquire with the same interruption or generation mechanism used for cancellation, and guard the state-commit section so a late completion cannot overwrite the active connection.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3dd1b964d8 (published as 1.2.0-alpha.166).

connectDeviceForBle now calls device.beginConnectionAttempt() in the deadline/abort catch, before the disconnect attempt. That supersedes the abandoned acquire, so when it settles later the generation guard already in Device.acquire (interruptedAttempt === attempt || connectionAttempt !== attempt) drops the link it built and throws DeviceInterruptedFromUser instead of committing mainId, deviceAcquired, commands or protocol state over the newer attempt. The bump also covers the case you called out where mainId is still unset, so the existing disconnect guard is skipped.

Your stronger suggestion — moving the deadline into the Electron transport so it aborts the underlying IPC — is the better end state and I have not done it here; this keeps the fix inside Core where the retry lives. Happy to follow up with the transport-level abort in a separate change.

Duplicate threads for this same finding are being resolved together; this one carries the resolution.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction: this fix is deferred, not shipped. It landed in d6be62d0f (published as 1.2.0-alpha.166) and has now been reverted in 72e137fca.

Reason: this file's connection path is currently under field validation against a 6.5.0 control build — the cold-connect route, the forced reset and the cache eviction were each derived from device logs, and changing them mid-validation would invalidate the comparison. The finding itself stands and is queued to land once the root cause is confirmed.

Nothing under test was affected: the app PR pins 1.2.0-alpha.165, which never contained these changes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: [Timed-out acquire can mutate a newer connection attempt]

When the deadline or abort wins, raceBleAcquire rejects but the underlying Device.acquire keeps running. The timeout cleanup only disconnects the current link when device.mainId is already set; on a first acquire there may be no session to disconnect, and the acquire generation is not invalidated. If the old acquire resolves after the retry starts, Device.acquire can still commit mainId, deviceAcquired, commands, and protocol state for the stale link, corrupting the newer session.

Invalidate the abandoned acquire with the same interruption or generation mechanism used for cancellation, and guard the state-commit section so a late completion cannot overwrite the active connection.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: [Acquire deadline allows overlapping native acquires]

If the deadline wins, only raceBleAcquire rejects; the underlying device.acquire() keeps running. device.mainId is normally assigned only after that promise resolves, so the timeout cleanup often cannot disconnect it, and the recursive retry starts another acquire while the expired operation can still complete and mutate Device state.

Please invalidate the expired connection-attempt generation and disconnect by the known BLE connect ID before starting another acquire.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: [Acquire deadline allows overlapping native acquires]

If the deadline wins, only raceBleAcquire rejects; the underlying device.acquire() keeps running. device.mainId is normally assigned only after that promise resolves, so the timeout cleanup often cannot disconnect it, and the recursive retry starts another acquire while the expired operation can still complete and mutate Device state.

Please invalidate the expired connection-attempt generation and disconnect by the known BLE connect ID before starting another acquire.

device.acquire(method.payload.connectProtocol, {
forceProtocolDetection: method.payload.forceProtocolDetection,
}),
abortSignal
);
} catch (err) {
// A deadline hit means the transport is wedged mid-acquire; drop the
// link before the retry so it cold-connects instead of stacking a
// second connect onto the half-open one.
if (
Comment thread
originalix marked this conversation as resolved.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: [Acquire timeout retries while the original BLE acquire is still in flight]

When the 60-second race expires, only the wrapper promise is rejected; the underlying device.acquire() continues running. Device.mainId is assigned after that promise resolves, so this guard normally has no session to disconnect and the recursive retry starts a second acquire on the same device. A late response from the first acquire can then recreate commands or mark the device acquired after the retry has moved on.

Please invalidate/cancel the underlying acquire (or advance the connection-attempt generation so late completion is discarded) and wait for that operation to be safely quarantined before starting a retry.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: [Acquire timeout retries while the original BLE acquire is still in flight]

When the 60-second race expires, only the wrapper promise is rejected; the underlying device.acquire() continues running. Device.mainId is assigned after that promise resolves, so this guard normally has no session to disconnect and the recursive retry starts a second acquire on the same device. A late response from the first acquire can then recreate commands or mark the device acquired after the retry has moved on.

Please invalidate or cancel the underlying acquire, or advance the connection-attempt generation so late completion is discarded, and quarantine that operation before starting a retry.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: [Acquire timeout retries while the original BLE acquire is still in flight]

When the deadline or cancellation fires, only the wrapper promise is rejected; the underlying device.acquire() continues running. Device.mainId is usually assigned after that promise resolves, so this cleanup often has no session to disconnect and the retry starts a second acquire on the same device. A late response from the first acquire can then recreate commands or mark the device acquired after the retry has moved on.

Please cancel or invalidate the underlying acquire and quarantine late completion with the connection-attempt generation before starting a retry.

err.errorCode === HardwareErrorCode.BleTimeoutError &&
Comment thread
originalix marked this conversation as resolved.
Comment thread
originalix marked this conversation as resolved.
Comment thread
originalix marked this conversation as resolved.
device.mainId &&
device.deviceConnector
) {
await device.deviceConnector.disconnect(device.mainId).catch(() => undefined);
device.markTransportDisconnected();
}
throw err;
}
}
}
if (method.payload?.onlyConnectBleDevice) {
if (shouldAcquire) {
Expand Down Expand Up @@ -1010,7 +1101,7 @@ async function connectDeviceForBle(method: BaseMethod, device: Device, retryCoun
const nextRetry = retryCount + 1;
Log.debug(`Bluetooth connection will retry, retry count: ${nextRetry}`);
await wait(3000);
await connectDeviceForBle(method, device, nextRetry);
await connectDeviceForBle(method, device, abortSignal, nextRetry);
} else {
throw err;
}
Expand Down Expand Up @@ -1120,7 +1211,7 @@ const ensureConnected = async (
if (tryCount === 1) {
device.beginConnectionAttempt();
}
await connectDeviceForBle(method, device);
await connectDeviceForBle(method, device, abortSignal);
}
resolve(device);
return;
Expand Down
8 changes: 4 additions & 4 deletions packages/hd-ble-sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@onekeyfe/hd-ble-sdk",
"version": "1.2.0-alpha.150",
"version": "1.2.0-alpha.151",
"author": "OneKey",
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
"license": "ISC",
Expand All @@ -20,8 +20,8 @@
"lint:fix": "eslint . --fix"
},
"dependencies": {
"@onekeyfe/hd-core": "1.2.0-alpha.150",
"@onekeyfe/hd-shared": "1.2.0-alpha.150",
"@onekeyfe/hd-transport-react-native": "1.2.0-alpha.150"
"@onekeyfe/hd-core": "1.2.0-alpha.151",
"@onekeyfe/hd-shared": "1.2.0-alpha.151",
"@onekeyfe/hd-transport-react-native": "1.2.0-alpha.151"
}
}
10 changes: 5 additions & 5 deletions packages/hd-cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@onekeyfe/hardware-cli",
"version": "1.2.0-alpha.150",
"version": "1.2.0-alpha.151",
"description": "OneKey hardware wallet CLI for testing device communication",
"author": "OneKey",
"license": "Apache-2.0",
Expand Down Expand Up @@ -31,10 +31,10 @@
"test": "jest"
},
"dependencies": {
"@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.150",
"@onekeyfe/hd-core": "1.2.0-alpha.150",
"@onekeyfe/hd-shared": "1.2.0-alpha.150",
"@onekeyfe/hd-transport-usb": "1.2.0-alpha.150",
"@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.151",
"@onekeyfe/hd-core": "1.2.0-alpha.151",
"@onekeyfe/hd-shared": "1.2.0-alpha.151",
"@onekeyfe/hd-transport-usb": "1.2.0-alpha.151",
"@stoprocent/noble": "2.3.16",
"commander": "^12.0.0"
}
Expand Down
16 changes: 8 additions & 8 deletions packages/hd-common-connect-sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@onekeyfe/hd-common-connect-sdk",
"version": "1.2.0-alpha.150",
"version": "1.2.0-alpha.151",
"author": "OneKey",
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
"license": "ISC",
Expand All @@ -20,12 +20,12 @@
"lint:fix": "eslint . --fix"
},
"dependencies": {
"@onekeyfe/hd-core": "1.2.0-alpha.150",
"@onekeyfe/hd-shared": "1.2.0-alpha.150",
"@onekeyfe/hd-transport-emulator": "1.2.0-alpha.150",
"@onekeyfe/hd-transport-http": "1.2.0-alpha.150",
"@onekeyfe/hd-transport-lowlevel": "1.2.0-alpha.150",
"@onekeyfe/hd-transport-usb": "1.2.0-alpha.150",
"@onekeyfe/hd-transport-web-device": "1.2.0-alpha.150"
"@onekeyfe/hd-core": "1.2.0-alpha.151",
"@onekeyfe/hd-shared": "1.2.0-alpha.151",
"@onekeyfe/hd-transport-emulator": "1.2.0-alpha.151",
"@onekeyfe/hd-transport-http": "1.2.0-alpha.151",
"@onekeyfe/hd-transport-lowlevel": "1.2.0-alpha.151",
"@onekeyfe/hd-transport-usb": "1.2.0-alpha.151",
"@onekeyfe/hd-transport-web-device": "1.2.0-alpha.151"
}
}
8 changes: 4 additions & 4 deletions packages/hd-transport-electron/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@onekeyfe/hd-transport-electron",
"version": "1.2.0-alpha.150",
"version": "1.2.0-alpha.151",
"author": "OneKey",
"homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme",
"license": "MIT",
Expand All @@ -25,9 +25,9 @@
"electron-log": ">=4.0.0"
},
"dependencies": {
"@onekeyfe/hd-core": "1.2.0-alpha.150",
"@onekeyfe/hd-shared": "1.2.0-alpha.150",
"@onekeyfe/hd-transport": "1.2.0-alpha.150",
"@onekeyfe/hd-core": "1.2.0-alpha.151",
"@onekeyfe/hd-shared": "1.2.0-alpha.151",
"@onekeyfe/hd-transport": "1.2.0-alpha.151",
"@stoprocent/noble": "2.3.16",
"p-retry": "^4.6.2"
},
Expand Down
25 changes: 18 additions & 7 deletions packages/hd-transport-electron/src/noble-ble-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,11 @@ async function initializeNoble(): Promise<void> {
noble = require('@stoprocent/noble') as NobleModule;
logger?.info('[NobleBLE] Noble library loaded');

// Register the process-lifetime state listener before any early return:
// the poweredOn fast path below would otherwise skip it for the whole
// session, leaving poweredOff cache/state reconciliation dead.
setupPersistentStateListener();

// Wait for Bluetooth to be ready
await new Promise<void>((resolve, reject) => {
if (!noble) {
Expand All @@ -268,9 +273,6 @@ async function initializeNoble(): Promise<void> {
return;
}

// Setup persistent state listener before initialization
setupPersistentStateListener();

const timeout = setTimeout(() => {
reject(
ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Bluetooth initialization timeout')
Expand Down Expand Up @@ -494,6 +496,9 @@ function handleDeviceDisconnect(deviceId: string, webContents: WebContents): voi

cleanupDevice(deviceId, webContents, {
cleanupConnection: true,
// Same stale-object hazard as manual disconnect: an externally dropped
// link invalidates the cached peripheral for the next reconnect.
cleanupDiscoveredCache: true,
sendDisconnectEvent: true,
cancelOperations: true,
reason: 'auto-disconnect',
Expand Down Expand Up @@ -1588,6 +1593,10 @@ async function disconnectDevice(deviceId: string): Promise<void> {
});
cleanupDevice(deviceId, undefined, {
cleanupConnection: true,
// macOS returns zero GATT services when a previously-disconnected
// peripheral object is reconnected; drop the discovery cache so the next
// connect resolves a fresh peripheral (direct connect by id, or scan).
cleanupDiscoveredCache: true,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: [Clearing the discovered peripheral cache on every disconnect breaks the non-advertising fallback]

This new cleanupDiscoveredCache: true runs on every manual or idle disconnect, so the next reconnect no longer has the previous Peripheral object as a fallback. After that, connectDevice() can only recover through tryDirectConnectById() or performTargetedScan().

That is not equivalent. tryDirectConnectById() returns undefined whenever connectAsync is unavailable or during its 15-second cooldown, and performTargetedScan() only succeeds if the device advertises again. A bonded device that stays silent between calls can now regress from reconnecting successfully to DeviceNotFound.

Please keep the last discovered peripheral until a replacement peripheral has been resolved successfully, or limit cache eviction to the backend cases where direct-connect is guaranteed to cover the reconnect path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3dd1b964d8 (1.2.0-alpha.166), close to your first suggestion. cleanupDevice no longer drops the evicted Peripheral outright: it moves it into a process-lifetime stalePeripherals map, and connectDevice uses it only as the last resort, after both a targeted scan and tryDirectConnectById come up empty (and clears it as soon as a fresh peripheral is resolved). A bonded device that stays silent — or a connect-by-id sitting in its 15s cooldown — therefore keeps the old reconnect path instead of regressing to DeviceNotFound, while a stale object is never preferred again, which is what the eviction was for: reconnecting a retrieved/cached peripheral is what produces the link where GATT resolves from cache but the device answers no protocol traffic.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction: this fix is deferred, not shipped. It landed in d6be62d0f (published as 1.2.0-alpha.166) and has now been reverted in 72e137fca.

Reason: this file's connection path is currently under field validation against a 6.5.0 control build — the cold-connect route, the forced reset and the cache eviction were each derived from device logs, and changing them mid-validation would invalidate the comparison. The finding itself stands and is queued to land once the root cause is confirmed.

Nothing under test was affected: the app PR pins 1.2.0-alpha.165, which never contained these changes.

sendDisconnectEvent: false,
cancelOperations: true,
reason: 'manual-disconnect',
Expand Down Expand Up @@ -1913,10 +1922,12 @@ export function setupNobleBleHandlers(webContents: WebContents): void {
}

await stopScanning().catch(() => undefined);
if (noble && persistentStateListener) {
noble.removeListener('stateChange', persistentStateListener);
persistentStateListener = null;
}
// persistentStateListener is process-lifetime, NOT per-window: this
// destroy handler also fires on a renderer soft restart, and removing
// the listener here permanently killed poweredOff/poweredOn
// reconciliation for the rest of the process (nothing re-registers it:
// initializeNoble early-returns once noble is loaded). The null-guard
// in setupPersistentStateListener keeps it deduped to one instance.
cleanupNobleListeners();
discoveredDevices.clear();
safeLog(logger, 'info', 'Noble BLE cleanup completed');
Expand Down
Loading
Loading