diff --git a/packages/connect-examples/electron-example/package.json b/packages/connect-examples/electron-example/package.json index 891feb72d..33625bbe7 100644 --- a/packages/connect-examples/electron-example/package.json +++ b/packages/connect-examples/electron-example/package.json @@ -2,7 +2,7 @@ "name": "hardware-example", "productName": "HardwareExample", "executableName": "onekey-hardware-example", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "author": "OneKey", "description": "OneKey Hardware SDK Electron BLE example", "main": "dist/index.js", @@ -21,7 +21,7 @@ "ts:check": "yarn tsc --noEmit" }, "dependencies": { - "@onekeyfe/hd-transport-electron": "1.2.0-alpha.176", + "@onekeyfe/hd-transport-electron": "1.2.0-alpha.181", "@stoprocent/noble": "2.3.16", "debug": "4.3.4", "electron-is-dev": "^3.0.1", diff --git a/packages/connect-examples/expo-example/package.json b/packages/connect-examples/expo-example/package.json index 9d9960335..2dff9f9d2 100644 --- a/packages/connect-examples/expo-example/package.json +++ b/packages/connect-examples/expo-example/package.json @@ -1,6 +1,6 @@ { "name": "expo-example", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "scripts": { "start": "yarn expo start --dev-client", "android": "yarn expo run:android", @@ -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.176", - "@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.176", - "@onekeyfe/hd-core": "1.2.0-alpha.176", - "@onekeyfe/hd-shared": "1.2.0-alpha.176", - "@onekeyfe/hd-transport": "1.2.0-alpha.176", + "@onekeyfe/hd-ble-sdk": "1.2.0-alpha.181", + "@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.181", + "@onekeyfe/hd-core": "1.2.0-alpha.181", + "@onekeyfe/hd-shared": "1.2.0-alpha.181", + "@onekeyfe/hd-transport": "1.2.0-alpha.181", "@onekeyfe/react-native-ble-utils": "^0.1.3", "@polkadot/util-crypto": "13.1.1", "@react-native-async-storage/async-storage": "1.21.0", diff --git a/packages/connect-examples/expo-playground/package.json b/packages/connect-examples/expo-playground/package.json index a777d33c7..4992bd12d 100644 --- a/packages/connect-examples/expo-playground/package.json +++ b/packages/connect-examples/expo-playground/package.json @@ -1,6 +1,6 @@ { "name": "onekey-hardware-playground", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "private": true, "sideEffects": [ "app/utils/shim.js", @@ -19,10 +19,10 @@ }, "dependencies": { "@noble/hashes": "^1.8.0", - "@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.176", - "@onekeyfe/hd-core": "1.2.0-alpha.176", - "@onekeyfe/hd-shared": "1.2.0-alpha.176", - "@onekeyfe/hd-transport": "1.2.0-alpha.176", + "@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.181", + "@onekeyfe/hd-core": "1.2.0-alpha.181", + "@onekeyfe/hd-shared": "1.2.0-alpha.181", + "@onekeyfe/hd-transport": "1.2.0-alpha.181", "@radix-ui/react-checkbox": "^1.3.2", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", diff --git a/packages/connect-examples/hwk-demo/package.json b/packages/connect-examples/hwk-demo/package.json index 60e0491a1..a64422739 100644 --- a/packages/connect-examples/hwk-demo/package.json +++ b/packages/connect-examples/hwk-demo/package.json @@ -1,6 +1,6 @@ { "name": "hwk-demo", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "private": true, "main": "index.js", "scripts": { diff --git a/packages/core/package.json b/packages/core/package.json index 482603afb..be8427238 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-core", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "description": "Core processes and APIs for communicating with OneKey hardware devices.", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", @@ -25,8 +25,8 @@ "url": "https://github.com/OneKeyHQ/hardware-js-sdk/issues" }, "dependencies": { - "@onekeyfe/hd-shared": "1.2.0-alpha.176", - "@onekeyfe/hd-transport": "1.2.0-alpha.176", + "@onekeyfe/hd-shared": "1.2.0-alpha.181", + "@onekeyfe/hd-transport": "1.2.0-alpha.181", "axios": "1.15.2", "bignumber.js": "^9.0.2", "buffer": "^6.0.3", diff --git a/packages/core/src/core/index.ts b/packages/core/src/core/index.ts index 67710f302..b5209b8b0 100644 --- a/packages/core/src/core/index.ts +++ b/packages/core/src/core/index.ts @@ -415,6 +415,13 @@ const onCallDevice = async ( if (method.payload?.onlyConnectBleDevice) { preWarmCallbackTask?.resolve(); Log.debug('Call API - only connect ble device: ', device?.mainId); + // This early return bypasses the normal-path bookkeeping 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), + // and the request stays in the active maps, so repeated preconnects pile + // up phantom work in diagnostics. + completeMethodRequestContext(method); + requestQueue.releaseTask(method.responseID); return createResponseMessage(method.responseID, true, null); } @@ -954,7 +961,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(acquirePromise: Promise, abortSignal?: AbortSignal): Promise { + return new Promise((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); @@ -968,9 +1028,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( + 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 ( + err.errorCode === HardwareErrorCode.BleTimeoutError && + device.mainId && + device.deviceConnector + ) { + await device.deviceConnector.disconnect(device.mainId).catch(() => undefined); + device.markTransportDisconnected(); + } + throw err; + } + } } if (method.payload?.onlyConnectBleDevice) { if (shouldAcquire) { @@ -1010,7 +1104,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; } @@ -1120,7 +1214,7 @@ const ensureConnected = async ( if (tryCount === 1) { device.beginConnectionAttempt(); } - await connectDeviceForBle(method, device); + await connectDeviceForBle(method, device, abortSignal); } resolve(device); return; diff --git a/packages/hd-ble-sdk/package.json b/packages/hd-ble-sdk/package.json index 804d35d1f..ace3d8781 100644 --- a/packages/hd-ble-sdk/package.json +++ b/packages/hd-ble-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-ble-sdk", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", "license": "ISC", @@ -20,8 +20,8 @@ "lint:fix": "eslint . --fix" }, "dependencies": { - "@onekeyfe/hd-core": "1.2.0-alpha.176", - "@onekeyfe/hd-shared": "1.2.0-alpha.176", - "@onekeyfe/hd-transport-react-native": "1.2.0-alpha.176" + "@onekeyfe/hd-core": "1.2.0-alpha.181", + "@onekeyfe/hd-shared": "1.2.0-alpha.181", + "@onekeyfe/hd-transport-react-native": "1.2.0-alpha.181" } } diff --git a/packages/hd-cli/package.json b/packages/hd-cli/package.json index 72fe6ef69..6557938da 100644 --- a/packages/hd-cli/package.json +++ b/packages/hd-cli/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hardware-cli", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "description": "OneKey hardware wallet CLI for testing device communication", "author": "OneKey", "license": "Apache-2.0", @@ -31,10 +31,10 @@ "test": "jest" }, "dependencies": { - "@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.176", - "@onekeyfe/hd-core": "1.2.0-alpha.176", - "@onekeyfe/hd-shared": "1.2.0-alpha.176", - "@onekeyfe/hd-transport-usb": "1.2.0-alpha.176", + "@onekeyfe/hd-common-connect-sdk": "1.2.0-alpha.181", + "@onekeyfe/hd-core": "1.2.0-alpha.181", + "@onekeyfe/hd-shared": "1.2.0-alpha.181", + "@onekeyfe/hd-transport-usb": "1.2.0-alpha.181", "@stoprocent/noble": "2.3.16", "commander": "^12.0.0" } diff --git a/packages/hd-common-connect-sdk/package.json b/packages/hd-common-connect-sdk/package.json index 2e38da01e..be8562656 100644 --- a/packages/hd-common-connect-sdk/package.json +++ b/packages/hd-common-connect-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-common-connect-sdk", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", "license": "ISC", @@ -20,12 +20,12 @@ "lint:fix": "eslint . --fix" }, "dependencies": { - "@onekeyfe/hd-core": "1.2.0-alpha.176", - "@onekeyfe/hd-shared": "1.2.0-alpha.176", - "@onekeyfe/hd-transport-emulator": "1.2.0-alpha.176", - "@onekeyfe/hd-transport-http": "1.2.0-alpha.176", - "@onekeyfe/hd-transport-lowlevel": "1.2.0-alpha.176", - "@onekeyfe/hd-transport-usb": "1.2.0-alpha.176", - "@onekeyfe/hd-transport-web-device": "1.2.0-alpha.176" + "@onekeyfe/hd-core": "1.2.0-alpha.181", + "@onekeyfe/hd-shared": "1.2.0-alpha.181", + "@onekeyfe/hd-transport-emulator": "1.2.0-alpha.181", + "@onekeyfe/hd-transport-http": "1.2.0-alpha.181", + "@onekeyfe/hd-transport-lowlevel": "1.2.0-alpha.181", + "@onekeyfe/hd-transport-usb": "1.2.0-alpha.181", + "@onekeyfe/hd-transport-web-device": "1.2.0-alpha.181" } } diff --git a/packages/hd-transport-electron/package.json b/packages/hd-transport-electron/package.json index 921c28013..9f7a6f045 100644 --- a/packages/hd-transport-electron/package.json +++ b/packages/hd-transport-electron/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-transport-electron", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", "license": "MIT", @@ -25,9 +25,9 @@ "electron-log": ">=4.0.0" }, "dependencies": { - "@onekeyfe/hd-core": "1.2.0-alpha.176", - "@onekeyfe/hd-shared": "1.2.0-alpha.176", - "@onekeyfe/hd-transport": "1.2.0-alpha.176", + "@onekeyfe/hd-core": "1.2.0-alpha.181", + "@onekeyfe/hd-shared": "1.2.0-alpha.181", + "@onekeyfe/hd-transport": "1.2.0-alpha.181", "@stoprocent/noble": "2.3.16", "p-retry": "^4.6.2" }, diff --git a/packages/hd-transport-electron/src/noble-ble-handler.ts b/packages/hd-transport-electron/src/noble-ble-handler.ts index 27d3a4ec6..53434bbe9 100644 --- a/packages/hd-transport-electron/src/noble-ble-handler.ts +++ b/packages/hd-transport-electron/src/noble-ble-handler.ts @@ -16,6 +16,7 @@ import { createKnownBleUuidAliases, hasOnekeyCommunicationService, isOnekeyBluetoothDevice, + isPro2FamilyBleName, matchesKnownBleUuid, wait, } from '@onekeyfe/hd-shared'; @@ -90,8 +91,18 @@ const DEVICE_SCAN_TIMEOUT = 5000; // 5 seconds for device scanning const DEVICE_CHECK_INTERVAL = 500; // 500ms interval for periodic device checks const SERVICE_DISCOVERY_TIMEOUT = 10000; // 10 seconds for service discovery const BLE_CLEANUP_TIMEOUT = 250; +// A physical teardown must actually reach the OS before the device can return +// to a clean state; 250ms routinely declared success while CoreBluetooth was +// still disconnecting, leaving no trace when the teardown never completed. +const BLE_DISCONNECT_CONFIRM_TIMEOUT_MS = 3000; // Renderer release is logical only; this timer physically frees the device. -const BLE_IDLE_DISCONNECT_MS = 3 * 60_000; +// Keep it SHORT: the Classic 1S goes protocol-deaf when a link is dropped +// after sitting idle for minutes (field logs 2026-08-19: every deaf window +// followed a 3-minute-idle disconnect, while disconnects right after traffic +// have never produced one across 6.5.0's per-call teardown history). A hot +// disconnect ~20s after the last operation stays inside the proven-safe +// pattern and also shrinks the window in which phones cannot see the device. +const BLE_IDLE_DISCONNECT_MS = 20_000; // Ceiling while a call is in flight: no outstanding write, but not forever. const BLE_BUSY_BACKSTOP_MS = 10 * 60_000; @@ -257,6 +268,11 @@ async function initializeNoble(): Promise { 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((resolve, reject) => { if (!noble) { @@ -269,9 +285,6 @@ async function initializeNoble(): Promise { return; } - // Setup persistent state listener before initialization - setupPersistentStateListener(); - const timeout = setTimeout(() => { reject( ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Bluetooth initialization timeout') @@ -398,7 +411,21 @@ function armIdleDisconnect( logger?.info('[NobleBLE] Keep-alive timeout, disconnecting device', { deviceId, reason }); const peripheral = connectedDevices.get(deviceId); const deviceName = peripheral?.advertisement?.localName || 'Unknown Device'; - const pending = disconnectDevice(deviceId) + // Unsubscribe CCCD before dropping the link, matching every other + // teardown path: the 1S leaves its notify session half-open when the + // link drops without an unsubscribe and then ignores application + // protocol traffic on NEW links for tens of minutes (field log + // 2026-08-19: the lone unsubscribe-skipping idle teardown caused a + // 6-attempt reconnect loop; unsubscribe-first teardowns reconnected + // instantly). + const pending = unsubscribeNotifications(deviceId) + .catch(error => { + logger?.warn('[NobleBLE] Keep-alive unsubscribe failed, disconnecting anyway', { + deviceId, + error: error instanceof Error ? error.message : String(error), + }); + }) + .then(() => disconnectDevice(deviceId)) .then(() => { // A call is still in flight here; it must reject, not hang. // Both 'idle' and 'busy-backstop' report IdleKeepAlive on purpose: @@ -508,6 +535,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', @@ -703,6 +733,17 @@ async function transmitHexDataToDevice( ); const pacingDelayMs = resolveNobleBleWritePacingDelay(options); + // Diagnostics: the frame header alone (report id + magic + message type + + // length) identifies which protocol frame went out, without logging payload. + logger?.info('[NobleBLE] Write frame', { + deviceId, + bytes: toBuffer.length, + head: toBuffer.subarray(0, 8).toString('hex'), + packetCapacity, + mtu: peripheral.mtu ?? null, + pacingDelayMs, + }); + if (!IS_WINDOWS || pairedDevices.has(deviceId)) { // macOS / Linux or already paired on Windows: direct write const writeCharacteristic = doGetWriteCharacteristic(); @@ -761,6 +802,44 @@ async function transmitHexDataToDevice( } // Handle discovered device (for general enumeration only) +// A scan that finds nothing costs this much before the direct-connect +// fallback runs. Every advertisement in the 6.5.0 control logs arrived within +// 631ms, so this leaves ~2x headroom while keeping a miss cheap. +const BLE_COLD_CONNECT_SCAN_TIMEOUT_MS = 1500; + +// Attempts before a link is written off as unable to serve the OneKey service. +const SERVICE_DISCOVERY_MAX_ATTEMPTS = 2; + +// Advertised names, kept for the life of the process: device caches are purged +// on every disconnect, but the family a device belongs to never changes and +// decides which connection strategy is safe for it. +const bleNamesById = new Map(); + +function rememberBleName(deviceId: string, name?: string | null): void { + const trimmed = name?.trim(); + if (trimmed) { + bleNamesById.set(deviceId, trimmed); + } +} + +function resolveBleName(deviceId: string): string | undefined { + const live = + connectedDevices.get(deviceId)?.advertisement?.localName?.trim() || + discoveredDevices.get(deviceId)?.advertisement?.localName?.trim(); + return live || bleNamesById.get(deviceId); +} + +/** + * Pro2/Neo connect by id first: they are unaffected by the stale-session + * problem and advertise under Find My names a OneKey-filtered scan cannot + * match. Everything else — Classic family, and any device whose name is not + * known yet — scans first, because connecting by id can resolve a session the + * device no longer serves (see setupConnectionAndDiscoverServices). + */ +function shouldConnectByIdFirst(deviceId: string): boolean { + return isPro2FamilyBleName(resolveBleName(deviceId)); +} + function handleDeviceDiscovered(peripheral: Peripheral): void { // Only process OneKey candidates for general discovery. Avoid logging every // ambient BLE peripheral; it makes Pro2 debugging hard to read. @@ -770,6 +849,7 @@ function handleDeviceDiscovered(peripheral: Peripheral): void { const isNewDevice = !discoveredDevices.has(peripheral.id); discoveredDevices.set(peripheral.id, peripheral); + rememberBleName(peripheral.id, peripheral.advertisement?.localName); if (isNewDevice) { logger?.debug('[NobleBLE] OneKey BLE device discovered', { deviceId: peripheral.id, @@ -804,7 +884,10 @@ async function waitForNobleScanStop(nobleInstance: NobleModule): Promise { // Perform targeted scan for a specific device ID // Uses self-contained local listener pattern - no global state needed -async function performTargetedScan(targetDeviceId: string): Promise { +async function performTargetedScan( + targetDeviceId: string, + timeoutMs: number = NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS +): Promise { if (!noble) { throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not available'); } @@ -849,7 +932,7 @@ async function performTargetedScan(targetDeviceId: string): Promise { logger?.info('[NobleBLE] Targeted scan timeout for device:', targetDeviceId); finish(null).catch(reject); - }, NOBLE_BLE_TARGETED_SCAN_TIMEOUT_MS); + }, timeoutMs); // Add local listener for this scan nobleInstance.on('discover', onDiscover); @@ -1051,9 +1134,17 @@ async function discoverServicesAndCharacteristics( // Main discovery logic as async function const discoveryPromise = (async (): Promise => { - // Step 1: Discover ALL services (no filter — Pro2 may use different service UUID) + // Step 1: Discover the OneKey service by UUID. Filtering is not just a + // narrowing: an unfiltered discovery is answered from the OS GATT cache, so + // a device whose stack still advertises and accepts links but no longer + // serves its application layer still looks healthy — the link comes up, the + // cached services resolve, the protocol frame goes out and nothing ever + // answers. A targeted query returns nothing in that state, which is what + // 6.5.0 relied on to trip the recovery path below (retry, reset, fresh + // scan) — the sequence that brings such a device back. Pro2/Neo expose the + // same service UUID, and the selection below only ever accepts that one. const services = await new Promise((resolve, reject) => { - peripheral.discoverServices([], (error, svc) => { + peripheral.discoverServices(ONEKEY_SERVICE_UUIDS, (error, svc) => { if (error) { logger?.error('[NobleBLE] Service discovery failed:', error); reject(ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, error.message)); @@ -1064,7 +1155,7 @@ async function discoverServicesAndCharacteristics( }); if (!services || services.length === 0) { - throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No services found'); + throw ERRORS.TypedError(HardwareErrorCode.BleServiceNotFound, 'No OneKey services found'); } logger?.debug('[NobleBLE] services discovered', { @@ -1248,12 +1339,14 @@ async function discoverServicesAndCharacteristicsWithRetry( deviceId, peripheralState: peripheral.state, attempt: attemptNumber, - maxRetries: 5, + maxRetries: SERVICE_DISCOVERY_MAX_ATTEMPTS, targetUUIDs: ONEKEY_SERVICE_UUIDS, }); if (attemptNumber > 1) { - logger?.info(`[NobleBLE] Service discovery retry attempt ${attemptNumber}/5`); + logger?.info( + `[NobleBLE] Service discovery retry attempt ${attemptNumber}/${SERVICE_DISCOVERY_MAX_ATTEMPTS}` + ); } // Verify connection state before attempting service discovery @@ -1267,20 +1360,32 @@ async function discoverServicesAndCharacteristicsWithRetry( try { return await discoverServicesAndCharacteristics(peripheral); } catch (error) { - logger?.error(`[NobleBLE] No services found (attempt ${attemptNumber}/5)`); + logger?.error( + `[NobleBLE] No services found (attempt ${attemptNumber}/${SERVICE_DISCOVERY_MAX_ATTEMPTS})` + ); - if (attemptNumber < 5) { - logger?.error(`[NobleBLE] Will retry service discovery (attempt ${attemptNumber + 1}/5)`); + if (attemptNumber < SERVICE_DISCOVERY_MAX_ATTEMPTS) { + logger?.error( + `[NobleBLE] Will retry service discovery (attempt ${ + attemptNumber + 1 + }/${SERVICE_DISCOVERY_MAX_ATTEMPTS})` + ); } throw error; // p-retry will handle the retry logic } }, { - retries: 4, // Total 5 attempts (initial + 4 retries) - factor: 1.5, // Exponential backoff: 1000ms → 1500ms → 2250ms → 3000ms - minTimeout: 1000, // Start with 1 second delay - maxTimeout: 3000, // Maximum 3 seconds delay + // One retry, not four. An empty result here is the signature of a device + // whose stack no longer serves its application layer, and retrying the + // same link has never recovered it — every observed run failed all the + // way through. The recovery is the teardown and cold reconnect the caller + // performs afterwards, so reaching it sooner is what shortens the wait. + // The single retry stays for a genuinely transient miss. + retries: SERVICE_DISCOVERY_MAX_ATTEMPTS - 1, + factor: 1.5, + minTimeout: 500, + maxTimeout: 3000, onFailedAttempt: error => { // This runs after each failed attempt logger?.error(`[NobleBLE] Service discovery attempt ${error.attemptNumber} failed:`, { @@ -1305,26 +1410,45 @@ async function setupConnectionAndDiscoverServices( deviceId: string, webContents: WebContents ): Promise { - setupDisconnectListener(peripheral, deviceId, webContents); - + // Reset the link before any protocol traffic reaches the device. 6.5.0 did + // this on every cold setup and never produced a protocol-deaf Classic 1S + // (control log 2026-08-20: 16/16 links answered within ~1s, including after + // a 15-minute idle). Demoting it to a discovery-failure fallback removed the + // reset entirely for this device, because discovery always succeeds: the link + // comes up, GATT resolves from cache, and the device then answers nothing on + // a session it no longer serves. Costs ~1.3s per cold setup; a kept-alive + // link with an intact subscription returns before reaching here, so reuse + // inside a workflow is unaffected. try { - return await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId); - } catch (initialError) { - logger?.info('[NobleBLE] Direct service discovery failed, retrying after reconnect', { - deviceId, - error: initialError, - }); + await forceReconnectPeripheral(peripheral, deviceId); + } catch (resetError) { + // A failed reset must not abort the attempt: discovery on the existing + // link, then the fresh-scan fallback, still have a chance to recover. + logger?.error('[NobleBLE] Connection reset before discovery failed, continuing', resetError); } - - await forceReconnectPeripheral(peripheral, deviceId); setupDisconnectListener(peripheral, deviceId, webContents); + try { return await discoverServicesAndCharacteristicsWithRetry(peripheral, deviceId); - } catch (reconnectError) { - logger?.error( - '[NobleBLE] Service discovery failed after reconnect, attempting fresh scan...', - reconnectError - ); + } catch (discoveryError) { + // A connected peripheral does not advertise, so the fresh scan below would + // only run out its timeout. Drop the link first, then rescan: the cold + // reconnect is what recovers a device whose stack stopped serving its + // application layer. Recover here rather than throwing to the caller — + // one acquire pays ~2s for the rescan, while a thrown error costs the + // renderer a full retry round trip. + if (peripheral.state === 'connected') { + logger?.error( + '[NobleBLE] Service discovery failed on a live link, dropping it before the fresh scan', + discoveryError + ); + await disconnectDevice(deviceId).catch(() => undefined); + } else { + logger?.error( + '[NobleBLE] Service discovery failed, attempting fresh scan...', + discoveryError + ); + } return freshScanAndDiscover(deviceId, webContents); } } @@ -1413,10 +1537,7 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis // enumerate clears the discovery map; a kept-alive link outlives it. let peripheral = discoveredDevices.get(deviceId) ?? connectedDevices.get(deviceId); - // If device not discovered, try a targeted scan for this specific device if (!peripheral) { - logger?.info('[NobleBLE] Device not discovered, attempting targeted scan for:', deviceId); - // Initialize Noble if not already done if (!noble) { await initializeNoble(); @@ -1426,27 +1547,46 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis throw ERRORS.TypedError(HardwareErrorCode.RuntimeError, 'Noble not available'); } - // Reaches a bonded device that is not advertising; else falls to the scan. - peripheral = await tryDirectConnectById(deviceId); - if (peripheral) { - discoveredDevices.set(deviceId, peripheral); - } - } + // A live advertisement proves the device holds no link and will open a + // fresh session; connecting by id resolves through the OS cache instead + // and can hand back a session the device no longer serves (see + // setupConnectionAndDiscoverServices). Scanning is also the faster of the + // two when the device does advertise: ~96ms median against ~784ms for + // connect-by-id across the field logs. Pro2/Neo keep connect-by-id first + // because they are unaffected and may advertise under an unmatchable Find + // My name. + const byIdFirst = shouldConnectByIdFirst(deviceId); + logger?.info('[NobleBLE] Resolving device for cold connect', { + deviceId, + name: resolveBleName(deviceId) ?? 'unknown', + strategy: byIdFirst ? 'connect-by-id-first' : 'scan-first', + }); - if (!peripheral) { - // Perform a targeted scan to find the specific device - try { - const foundPeripheral = await performTargetedScan(deviceId); - if (!foundPeripheral) { - throw ERRORS.TypedError( - HardwareErrorCode.DeviceNotFound, - `Device ${deviceId} not found even after targeted scan` - ); + const scanForPeripheral = async () => { + try { + return (await performTargetedScan(deviceId, BLE_COLD_CONNECT_SCAN_TIMEOUT_MS)) ?? undefined; + } catch (scanError) { + logger?.info('[NobleBLE] Targeted scan failed', { + deviceId, + error: scanError instanceof Error ? scanError.message : String(scanError), + }); + return undefined; } - peripheral = foundPeripheral; - } catch (error) { - logger?.error('[NobleBLE] Targeted scan failed:', error); - throw error; + }; + + const connectById = async () => { + const found = await tryDirectConnectById(deviceId); + if (found) { + discoveredDevices.set(deviceId, found); + } + return found; + }; + + peripheral = byIdFirst ? await connectById() : await scanForPeripheral(); + if (!peripheral) { + // The preferred route came up empty: not advertising (held elsewhere, or + // silent), or not reachable by id. Try the other one before giving up. + peripheral = byIdFirst ? await scanForPeripheral() : await connectById(); } } @@ -1455,6 +1595,10 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis throw ERRORS.TypedError(HardwareErrorCode.DeviceNotFound, `Device ${deviceId} not found`); } + // Also covers the connect-by-id route, whose simulated discovery carries no + // service UUIDs and therefore never reaches handleDeviceDiscovered. + rememberBleName(deviceId, peripheral.advertisement?.localName); + logger?.info('[NobleBLE] Connecting to device:', deviceId); // Check if device is already connected @@ -1575,9 +1719,14 @@ async function connectDevice(deviceId: string, webContents: WebContents): Promis resolve(); } catch (setupError) { logger?.error('[NobleBLE] Connection setup failed:', setupError); - connectedPeripheral.disconnect(() => { - reject(setupError); - }); + // Never reject from inside a raw disconnect callback: noble only fires + // it on a real 'disconnect' event, so a peripheral that is already + // down leaves this promise — and the renderer acquire awaiting it — + // pending forever. disconnectDevice always settles (it no-ops on an + // unknown peripheral and caps the confirm wait). + disconnectDevice(deviceId) + .catch(() => undefined) + .then(() => reject(setupError)); } }); }); @@ -1597,11 +1746,18 @@ async function disconnectDevice(deviceId: string): Promise { } await runBleCallbackOperation(callback => peripheral.disconnect(() => callback()), { - timeoutMs: BLE_CLEANUP_TIMEOUT, + timeoutMs: BLE_DISCONNECT_CONFIRM_TIMEOUT_MS, timeoutBehavior: 'resolve', }); + if (peripheral.state === 'connected') { + logger?.warn('[NobleBLE] Physical disconnect did not confirm in time', { deviceId }); + } 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, sendDisconnectEvent: false, cancelOperations: true, reason: 'manual-disconnect', @@ -1732,6 +1888,11 @@ async function subscribeNotifications( }); notifyCharacteristic.on('data', (data: Buffer) => { + logger?.debug('[NobleBLE] Notify frame', { + deviceId, + bytes: data.length, + head: data.subarray(0, 8).toString('hex'), + }); // Windows BLE pairing detection: receiving any data means device is paired if (!pairedDevices.has(deviceId)) { pairedDevices.add(deviceId); @@ -1742,9 +1903,14 @@ async function subscribeNotifications( }); } + const subscribeStartedAt = Date.now(); try { await rebuildAppSubscription(deviceId, notifyCharacteristic); subscribedDevices.set(deviceId, true); + logger?.info('[NobleBLE] Notification subscription active', { + deviceId, + ms: Date.now() - subscribeStartedAt, + }); } finally { // 🔒 CRITICAL: Always clear operation state (even on error) subscriptionOperations.set(deviceId, 'idle'); @@ -1927,10 +2093,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'); diff --git a/packages/hd-transport-electron/src/types/noble-extended.ts b/packages/hd-transport-electron/src/types/noble-extended.ts index f9f7c410a..f86b4366b 100644 --- a/packages/hd-transport-electron/src/types/noble-extended.ts +++ b/packages/hd-transport-electron/src/types/noble-extended.ts @@ -39,6 +39,7 @@ export interface NobleModule { export interface Logger { info(message: string, ...args: any[]): void; debug(message: string, ...args: any[]): void; + warn(message: string, ...args: any[]): void; error(message: string, ...args: any[]): void; } diff --git a/packages/hd-transport-emulator/package.json b/packages/hd-transport-emulator/package.json index 207bf1d61..958769b70 100644 --- a/packages/hd-transport-emulator/package.json +++ b/packages/hd-transport-emulator/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-transport-emulator", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "description": "hardware emulator transport", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", @@ -24,8 +24,8 @@ "url": "https://github.com/OneKeyHQ/hardware-js-sdk/issues" }, "dependencies": { - "@onekeyfe/hd-shared": "1.2.0-alpha.176", - "@onekeyfe/hd-transport": "1.2.0-alpha.176", + "@onekeyfe/hd-shared": "1.2.0-alpha.181", + "@onekeyfe/hd-transport": "1.2.0-alpha.181", "axios": "1.15.2", "secure-json-parse": "^4.0.0" } diff --git a/packages/hd-transport-http/package.json b/packages/hd-transport-http/package.json index fbb77d09d..d6149e123 100644 --- a/packages/hd-transport-http/package.json +++ b/packages/hd-transport-http/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-transport-http", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "description": "hardware http transport", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", @@ -24,8 +24,8 @@ "url": "https://github.com/OneKeyHQ/hardware-js-sdk/issues" }, "dependencies": { - "@onekeyfe/hd-shared": "1.2.0-alpha.176", - "@onekeyfe/hd-transport": "1.2.0-alpha.176", + "@onekeyfe/hd-shared": "1.2.0-alpha.181", + "@onekeyfe/hd-transport": "1.2.0-alpha.181", "axios": "1.15.2", "secure-json-parse": "^4.0.0" } diff --git a/packages/hd-transport-lowlevel/package.json b/packages/hd-transport-lowlevel/package.json index 80a861700..8211cc6da 100644 --- a/packages/hd-transport-lowlevel/package.json +++ b/packages/hd-transport-lowlevel/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-transport-lowlevel", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", "license": "MIT", "main": "dist/index.js", @@ -20,7 +20,7 @@ "lint:fix": "eslint . --fix" }, "dependencies": { - "@onekeyfe/hd-shared": "1.2.0-alpha.176", - "@onekeyfe/hd-transport": "1.2.0-alpha.176" + "@onekeyfe/hd-shared": "1.2.0-alpha.181", + "@onekeyfe/hd-transport": "1.2.0-alpha.181" } } diff --git a/packages/hd-transport-react-native/package.json b/packages/hd-transport-react-native/package.json index 2369405b5..cfa86b5d5 100644 --- a/packages/hd-transport-react-native/package.json +++ b/packages/hd-transport-react-native/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-transport-react-native", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", "license": "MIT", "main": "dist/index.js", @@ -20,9 +20,9 @@ "lint:fix": "eslint . --fix" }, "dependencies": { - "@onekeyfe/hd-core": "1.2.0-alpha.176", - "@onekeyfe/hd-shared": "1.2.0-alpha.176", - "@onekeyfe/hd-transport": "1.2.0-alpha.176", + "@onekeyfe/hd-core": "1.2.0-alpha.181", + "@onekeyfe/hd-shared": "1.2.0-alpha.181", + "@onekeyfe/hd-transport": "1.2.0-alpha.181", "@onekeyfe/react-native-ble-utils": "^0.1.6", "react-native-ble-plx": "3.5.1" } diff --git a/packages/hd-transport-react-native/src/index.ts b/packages/hd-transport-react-native/src/index.ts index dff366395..4a4a36566 100644 --- a/packages/hd-transport-react-native/src/index.ts +++ b/packages/hd-transport-react-native/src/index.ts @@ -2120,31 +2120,23 @@ export default class ReactNativeBleTransport { protocolHint?: ProtocolType, rebuildTransport?: () => Promise ): Promise { - // iOS still skips an extra V1 Initialize during acquire. Expected V2 must - // Ping so USB-priority `link disabled` can surface instead of a later - // unmapped RuntimeError. - if (Platform.OS === 'ios' && expectedProtocol === 'V1') { - this.deviceProtocol.set(uuid, expectedProtocol); + // A declared V1 is taken at face value on every platform, as iOS has done + // since protocol probing arrived: the caller reads the protocol off its own + // device record, so the probe re-asks a question that is already answered + // and costs a round trip on every acquire. Expected V2 must still Ping so + // USB-priority `link disabled` surfaces here instead of as a later unmapped + // RuntimeError. sessionProtocols is stamped too — the iOS path skipped it, + // which left the firmware-install reconnect (skipProtocolProbe) without the + // confirmed protocol it requires. + if (expectedProtocol === 'V1') { + this.deviceProtocol.set(uuid, 'V1'); + this.sessionProtocols.set(uuid, 'V1'); Log?.debug('[ReactNativeBleTransport] protocol selected', { deviceId: uuid, - protocol: expectedProtocol, + protocol: 'V1', source: 'expected', }); - return expectedProtocol; - } - - if (expectedProtocol === 'V1') { - if (await this.probeProtocolV1(uuid)) { - this.deviceProtocol.set(uuid, 'V1'); - this.sessionProtocols.set(uuid, 'V1'); - Log?.debug('[ReactNativeBleTransport] protocol detected', { - deviceId: uuid, - protocol: 'V1', - source: 'expected', - }); - return 'V1'; - } - throw this.createProtocolMismatchError(expectedProtocol, uuid); + return 'V1'; } if (expectedProtocol === 'V2') { diff --git a/packages/hd-transport-usb/package.json b/packages/hd-transport-usb/package.json index 959b2cf6f..1913c574d 100644 --- a/packages/hd-transport-usb/package.json +++ b/packages/hd-transport-usb/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-transport-usb", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "description": "OneKey hardware wallet direct USB transport plugin (libusb)", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", "license": "MIT", @@ -21,8 +21,8 @@ "lint:fix": "eslint . --fix" }, "dependencies": { - "@onekeyfe/hd-shared": "1.2.0-alpha.176", - "@onekeyfe/hd-transport": "1.2.0-alpha.176", + "@onekeyfe/hd-shared": "1.2.0-alpha.181", + "@onekeyfe/hd-transport": "1.2.0-alpha.181", "bytebuffer": "^5.0.1", "usb": "^2.14.0" } diff --git a/packages/hd-transport-web-device/__tests__/electron-ble-transport.test.ts b/packages/hd-transport-web-device/__tests__/electron-ble-transport.test.ts index 8cffd8932..799d44c8d 100644 --- a/packages/hd-transport-web-device/__tests__/electron-ble-transport.test.ts +++ b/packages/hd-transport-web-device/__tests__/electron-ble-transport.test.ts @@ -327,7 +327,7 @@ describe('ElectronBleTransport protocol detection', () => { } }); - test('reconnects Protocol V1 with a non-destructive GetFeatures probe', async () => { + test('reconnects a declared Protocol V1 device without probing again', async () => { const device = { id: 'classic-id', name: 'OneKey Classic' }; const nobleBle = createNobleBle(device); let notificationHandler: ((deviceId: string, data: string) => void) | undefined; @@ -360,7 +360,10 @@ describe('ElectronBleTransport protocol detection', () => { uuid: device.id, }) ); - expect(nobleBle.write).toHaveBeenCalledTimes(2); + // The first acquire probes because the protocol is unknown; the second + // declares V1 and must send nothing at all, leaving the first frame on + // the link to Core — which is what carries the wallet session. + expect(nobleBle.write).toHaveBeenCalledTimes(1); expect(nobleBle.write.mock.calls.every(([, hex]) => /^3f23230037/.test(hex))).toBe(true); expect(protocolV2Writer).not.toHaveBeenCalled(); } finally { @@ -373,19 +376,13 @@ describe('ElectronBleTransport protocol detection', () => { const nobleBle = createNobleBle(device); let notificationHandler: ((deviceId: string, data: string) => void) | undefined; const v1ResponseHex = '3f23230002000000040a026f6b'; - let writeCount = 0; - nobleBle.onNotification.mockImplementation(handler => { notificationHandler = handler; return jest.fn(); }); - nobleBle.write.mockImplementation(() => { - writeCount += 1; - if (writeCount === 1) { - setTimeout(() => notificationHandler?.(device.id, v1ResponseHex), 0); - } - return Promise.resolve(); - }); + // A declared V1 acquire writes nothing, so every write here belongs to the + // call under test and none of them is answered. + nobleBle.write.mockImplementation(() => Promise.resolve()); const bleTransport = configureTransport(nobleBle); await bleTransport.acquire({ uuid: device.id, expectedProtocol: 'V1' }); diff --git a/packages/hd-transport-web-device/__tests__/webusb-protocol-v2-timeout.test.ts b/packages/hd-transport-web-device/__tests__/webusb-protocol-v2-timeout.test.ts index 000af1d7f..0bce78a58 100644 --- a/packages/hd-transport-web-device/__tests__/webusb-protocol-v2-timeout.test.ts +++ b/packages/hd-transport-web-device/__tests__/webusb-protocol-v2-timeout.test.ts @@ -387,6 +387,13 @@ describe('WebUsbTransport Protocol V2 timeout recovery', () => { webusb.callProtocolV2(path, 'Ping', { message: 'short' }, { timeoutMs: 25 }), ]); - expect(readTimeouts).toEqual([1_000, 25]); + // Each call keeps its own budget instead of inheriting the other's. The + // queued one is a deadline, so it arrives with whatever the first call left + // of its 25ms — asserting the exact remainder makes this fail on a loaded + // machine, which is timing, not behaviour. + expect(readTimeouts).toHaveLength(2); + expect(readTimeouts[0]).toBe(1_000); + expect(readTimeouts[1]).toBeGreaterThan(0); + expect(readTimeouts[1]).toBeLessThanOrEqual(25); }); }); diff --git a/packages/hd-transport-web-device/package.json b/packages/hd-transport-web-device/package.json index 35d8493d2..ec97c0605 100644 --- a/packages/hd-transport-web-device/package.json +++ b/packages/hd-transport-web-device/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-transport-web-device", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", "license": "MIT", @@ -21,11 +21,11 @@ "lint:fix": "eslint . --fix" }, "dependencies": { - "@onekeyfe/hd-shared": "1.2.0-alpha.176", - "@onekeyfe/hd-transport": "1.2.0-alpha.176" + "@onekeyfe/hd-shared": "1.2.0-alpha.181", + "@onekeyfe/hd-transport": "1.2.0-alpha.181" }, "devDependencies": { - "@onekeyfe/hd-transport-electron": "1.2.0-alpha.176", + "@onekeyfe/hd-transport-electron": "1.2.0-alpha.181", "@types/w3c-web-usb": "^1.0.6", "@types/web-bluetooth": "^0.0.17" } diff --git a/packages/hd-transport-web-device/src/electron-ble-transport.ts b/packages/hd-transport-web-device/src/electron-ble-transport.ts index b3e4861e0..6aa15ade9 100644 --- a/packages/hd-transport-web-device/src/electron-ble-transport.ts +++ b/packages/hd-transport-web-device/src/electron-ble-transport.ts @@ -487,13 +487,20 @@ export default class ElectronBleTransport { expectedProtocol?: ProtocolType, protocolHint?: ProtocolType ): Promise { + // A declared V1 is taken at face value, as the React Native transport + // already does on iOS: the caller reads the protocol off its own device + // record, so probing re-asks a question that is already answered and adds a + // round trip to every cold connect. It also fails in a way that costs the + // session: a device whose protocol session has stalled ignores the probe + // frame, and the timeout became a protocol-mismatch error that stopped Core + // from ever sending Initialize — the one frame such a device still answers, + // and how it gets revived. A declared V2 keeps probing, matching iOS, so a + // USB-priority "link disabled" surfaces here rather than as an unmapped + // error later. An undeclared protocol still goes through full detection. if (expectedProtocol === 'V1') { - if (await this.probeProtocolV1(uuid)) { - this.deviceProtocol.set(uuid, 'V1'); - this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V1 (expected)`); - return 'V1'; - } - throw this.createProtocolMismatchError(expectedProtocol, uuid); + this.deviceProtocol.set(uuid, 'V1'); + this.Log?.debug(`[Electron BLE] detectProtocol: uuid=${uuid} -> V1 (expected, no probe)`); + return 'V1'; } if (expectedProtocol === 'V2') { diff --git a/packages/hd-transport/package.json b/packages/hd-transport/package.json index 9a507d49b..156312fbc 100644 --- a/packages/hd-transport/package.json +++ b/packages/hd-transport/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-transport", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "description": "Transport layer abstractions and utilities for OneKey hardware SDK.", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", diff --git a/packages/hd-web-sdk/package.json b/packages/hd-web-sdk/package.json index 9d625488e..349625cde 100644 --- a/packages/hd-web-sdk/package.json +++ b/packages/hd-web-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-web-sdk", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "author": "OneKey", "homepage": "https://github.com/OneKeyHQ/hardware-js-sdk#readme", "license": "ISC", @@ -23,10 +23,10 @@ }, "dependencies": { "@onekeyfe/cross-inpage-provider-core": "2.2.67", - "@onekeyfe/hd-core": "1.2.0-alpha.176", - "@onekeyfe/hd-shared": "1.2.0-alpha.176", - "@onekeyfe/hd-transport-http": "1.2.0-alpha.176", - "@onekeyfe/hd-transport-web-device": "1.2.0-alpha.176" + "@onekeyfe/hd-core": "1.2.0-alpha.181", + "@onekeyfe/hd-shared": "1.2.0-alpha.181", + "@onekeyfe/hd-transport-http": "1.2.0-alpha.181", + "@onekeyfe/hd-transport-web-device": "1.2.0-alpha.181" }, "devDependencies": { "@babel/plugin-proposal-optional-chaining": "^7.17.12", diff --git a/packages/hwk-adapter-core/package.json b/packages/hwk-adapter-core/package.json index 82a93be0e..ceffbbddc 100644 --- a/packages/hwk-adapter-core/package.json +++ b/packages/hwk-adapter-core/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-adapter-core", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "description": "Shared types and utilities for OneKey hardware wallet kit", "author": "OneKey", "license": "MIT", diff --git a/packages/hwk-ledger-adapter/package.json b/packages/hwk-ledger-adapter/package.json index 2103d80a3..937a6995c 100644 --- a/packages/hwk-ledger-adapter/package.json +++ b/packages/hwk-ledger-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-ledger-adapter", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "description": "Ledger hardware wallet adapter for OneKey", "author": "OneKey", "license": "MIT", @@ -50,7 +50,7 @@ "@ledgerhq/device-signer-kit-solana": "^1.7.0", "@ledgerhq/hw-app-trx": "^6.34.1", "@ledgerhq/hw-transport": "^6.34.1", - "@onekeyfe/hwk-adapter-core": "1.2.0-alpha.176", + "@onekeyfe/hwk-adapter-core": "1.2.0-alpha.181", "bitcoinjs-lib": "npm:@onekeyfe/bitcoinjs-lib@7.0.1" }, "devDependencies": { diff --git a/packages/hwk-ledger-connector-ble/package.json b/packages/hwk-ledger-connector-ble/package.json index 424559ddd..00725c12f 100644 --- a/packages/hwk-ledger-connector-ble/package.json +++ b/packages/hwk-ledger-connector-ble/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-ledger-connector-ble", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "description": "IConnector implementation for Ledger hardware wallets via React Native BLE", "author": "OneKey", "license": "MIT", @@ -51,8 +51,8 @@ "@ledgerhq/device-signer-kit-ethereum": "^1.9.0", "@ledgerhq/device-signer-kit-solana": "^1.7.0", "@ledgerhq/device-transport-kit-react-native-ble": "^1.0.0", - "@onekeyfe/hwk-adapter-core": "1.2.0-alpha.176", - "@onekeyfe/hwk-ledger-adapter": "1.2.0-alpha.176" + "@onekeyfe/hwk-adapter-core": "1.2.0-alpha.181", + "@onekeyfe/hwk-ledger-adapter": "1.2.0-alpha.181" }, "peerDependencies": { "react-native": "*" diff --git a/packages/hwk-ledger-connector-webhid/package.json b/packages/hwk-ledger-connector-webhid/package.json index d61a9af2f..761991155 100644 --- a/packages/hwk-ledger-connector-webhid/package.json +++ b/packages/hwk-ledger-connector-webhid/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-ledger-connector-webhid", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "description": "IConnector implementation for Ledger hardware wallets via WebHID (DMK)", "author": "OneKey", "license": "MIT", @@ -49,8 +49,8 @@ "@ledgerhq/device-signer-kit-ethereum": "^1.9.0", "@ledgerhq/device-signer-kit-solana": "^1.7.0", "@ledgerhq/device-transport-kit-web-hid": "^1.0.0", - "@onekeyfe/hwk-adapter-core": "1.2.0-alpha.176", - "@onekeyfe/hwk-ledger-adapter": "1.2.0-alpha.176" + "@onekeyfe/hwk-adapter-core": "1.2.0-alpha.181", + "@onekeyfe/hwk-ledger-adapter": "1.2.0-alpha.181" }, "devDependencies": { "rimraf": "^5.0.0", diff --git a/packages/hwk-trezor-adapter/package.json b/packages/hwk-trezor-adapter/package.json index e11ca83f7..357c82992 100644 --- a/packages/hwk-trezor-adapter/package.json +++ b/packages/hwk-trezor-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-adapter", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "description": "Trezor hardware wallet adapter primitives for OneKey", "author": "OneKey", "license": "MIT", @@ -43,8 +43,8 @@ "crypto" ], "dependencies": { - "@onekeyfe/hwk-adapter-core": "1.2.0-alpha.176", - "@onekeyfe/hwk-trezor-core": "1.2.0-alpha.176", + "@onekeyfe/hwk-adapter-core": "1.2.0-alpha.181", + "@onekeyfe/hwk-trezor-core": "1.2.0-alpha.181", "tslib": "^2.6.2" }, "devDependencies": { diff --git a/packages/hwk-trezor-connector-electron-ble/package.json b/packages/hwk-trezor-connector-electron-ble/package.json index 95fbc0bbe..f5728f6df 100644 --- a/packages/hwk-trezor-connector-electron-ble/package.json +++ b/packages/hwk-trezor-connector-electron-ble/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-connector-electron-ble", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "description": "IConnector implementation for Trezor hardware wallets via Electron BLE (noble in main process)", "author": "OneKey", "license": "MIT", @@ -58,9 +58,9 @@ "connector" ], "dependencies": { - "@onekeyfe/hwk-adapter-core": "1.2.0-alpha.176", - "@onekeyfe/hwk-trezor-adapter": "1.2.0-alpha.176", - "@onekeyfe/hwk-trezor-connector": "1.2.0-alpha.176", + "@onekeyfe/hwk-adapter-core": "1.2.0-alpha.181", + "@onekeyfe/hwk-trezor-adapter": "1.2.0-alpha.181", + "@onekeyfe/hwk-trezor-connector": "1.2.0-alpha.181", "buffer": "^6.0.3" }, "peerDependencies": { diff --git a/packages/hwk-trezor-connector-rn-ble/package.json b/packages/hwk-trezor-connector-rn-ble/package.json index ebba7e5d4..1f86a676c 100644 --- a/packages/hwk-trezor-connector-rn-ble/package.json +++ b/packages/hwk-trezor-connector-rn-ble/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-connector-rn-ble", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "description": "IConnector implementation for Trezor hardware wallets via React Native BLE", "author": "OneKey", "license": "MIT", @@ -46,9 +46,9 @@ "connector" ], "dependencies": { - "@onekeyfe/hwk-adapter-core": "1.2.0-alpha.176", - "@onekeyfe/hwk-trezor-adapter": "1.2.0-alpha.176", - "@onekeyfe/hwk-trezor-connector": "1.2.0-alpha.176", + "@onekeyfe/hwk-adapter-core": "1.2.0-alpha.181", + "@onekeyfe/hwk-trezor-adapter": "1.2.0-alpha.181", + "@onekeyfe/hwk-trezor-connector": "1.2.0-alpha.181", "buffer": "^6.0.3", "react-native-ble-plx": "^3.5.1" }, diff --git a/packages/hwk-trezor-connector-webusb/package.json b/packages/hwk-trezor-connector-webusb/package.json index b52844381..2c1f5b9a3 100644 --- a/packages/hwk-trezor-connector-webusb/package.json +++ b/packages/hwk-trezor-connector-webusb/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-connector-webusb", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "description": "IConnector implementation for Trezor hardware wallets via WebUSB", "author": "OneKey", "license": "MIT", @@ -55,8 +55,8 @@ "connector" ], "dependencies": { - "@onekeyfe/hwk-adapter-core": "1.2.0-alpha.176", - "@onekeyfe/hwk-trezor-connector": "1.2.0-alpha.176", + "@onekeyfe/hwk-adapter-core": "1.2.0-alpha.181", + "@onekeyfe/hwk-trezor-connector": "1.2.0-alpha.181", "buffer": "^6.0.3" }, "devDependencies": { diff --git a/packages/hwk-trezor-connector/package.json b/packages/hwk-trezor-connector/package.json index fe49922a3..4985a94a0 100644 --- a/packages/hwk-trezor-connector/package.json +++ b/packages/hwk-trezor-connector/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-connector", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "description": "Shared Trezor connector base for Hardware Wallet Kit", "author": "OneKey", "license": "MIT", @@ -38,13 +38,13 @@ }, "sideEffects": false, "dependencies": { - "@onekeyfe/hwk-adapter-core": "1.2.0-alpha.176", - "@onekeyfe/hwk-trezor-core": "1.2.0-alpha.176" + "@onekeyfe/hwk-adapter-core": "1.2.0-alpha.181", + "@onekeyfe/hwk-trezor-core": "1.2.0-alpha.181" }, "devDependencies": { - "@onekeyfe/hwk-trezor-protobuf": "1.2.0-alpha.176", - "@onekeyfe/hwk-trezor-protocol": "1.2.0-alpha.176", - "@onekeyfe/hwk-trezor-transport": "1.2.0-alpha.176", + "@onekeyfe/hwk-trezor-protobuf": "1.2.0-alpha.181", + "@onekeyfe/hwk-trezor-protocol": "1.2.0-alpha.181", + "@onekeyfe/hwk-trezor-transport": "1.2.0-alpha.181", "rimraf": "^5.0.0", "tsup": "^8.0.0", "typescript": "5.1.6" diff --git a/packages/hwk-trezor-core/package.json b/packages/hwk-trezor-core/package.json index 4f97791f5..1ca766eae 100644 --- a/packages/hwk-trezor-core/package.json +++ b/packages/hwk-trezor-core/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-core", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "description": "Minimal Trezor core call layer for Hardware Wallet Kit", "author": "OneKey", "license": "MIT", @@ -41,9 +41,9 @@ "devDependencies": { "@noble/ciphers": "^1.3.0", "@noble/hashes": "^1.8.0", - "@onekeyfe/hwk-trezor-protobuf": "1.2.0-alpha.176", - "@onekeyfe/hwk-trezor-protocol": "1.2.0-alpha.176", - "@onekeyfe/hwk-trezor-transport": "1.2.0-alpha.176", + "@onekeyfe/hwk-trezor-protobuf": "1.2.0-alpha.181", + "@onekeyfe/hwk-trezor-protocol": "1.2.0-alpha.181", + "@onekeyfe/hwk-trezor-transport": "1.2.0-alpha.181", "buffer": "^6.0.3", "rimraf": "^5.0.0", "tsup": "^8.0.0", diff --git a/packages/hwk-trezor-protobuf/package.json b/packages/hwk-trezor-protobuf/package.json index 25f665e2c..187967fea 100644 --- a/packages/hwk-trezor-protobuf/package.json +++ b/packages/hwk-trezor-protobuf/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-protobuf", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "private": true, "description": "Trezor protobuf helpers vendored from Trezor Suite MIT sources", "author": "OneKey", @@ -50,7 +50,7 @@ "sideEffects": false, "dependencies": { "@bufbuild/protobuf": "^2.11.0", - "@onekeyfe/hwk-trezor-schema-utils": "1.2.0-alpha.176" + "@onekeyfe/hwk-trezor-schema-utils": "1.2.0-alpha.181" }, "devDependencies": { "rimraf": "^5.0.0", diff --git a/packages/hwk-trezor-protocol/package.json b/packages/hwk-trezor-protocol/package.json index 45ea3b15b..e17908c22 100644 --- a/packages/hwk-trezor-protocol/package.json +++ b/packages/hwk-trezor-protocol/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-protocol", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "private": true, "description": "Trezor transport protocol helpers vendored from Trezor Suite MIT sources", "author": "OneKey", diff --git a/packages/hwk-trezor-schema-utils/package.json b/packages/hwk-trezor-schema-utils/package.json index 97a46066b..f2bff0613 100644 --- a/packages/hwk-trezor-schema-utils/package.json +++ b/packages/hwk-trezor-schema-utils/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-schema-utils", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "private": true, "description": "Schema helpers vendored from Trezor Suite MIT sources for HWK Trezor support", "author": "OneKey", @@ -44,7 +44,7 @@ }, "sideEffects": false, "dependencies": { - "@onekeyfe/hwk-trezor-type-utils": "1.2.0-alpha.176", + "@onekeyfe/hwk-trezor-type-utils": "1.2.0-alpha.181", "@sinclair/typebox": "^0.34.49", "ts-mixer": "^6.0.4" }, diff --git a/packages/hwk-trezor-transport/package.json b/packages/hwk-trezor-transport/package.json index 5c9e1b272..4557f499d 100644 --- a/packages/hwk-trezor-transport/package.json +++ b/packages/hwk-trezor-transport/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-transport", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "private": true, "description": "Low-level Trezor transport helpers vendored from Trezor Suite MIT sources", "author": "OneKey", @@ -49,10 +49,10 @@ }, "sideEffects": false, "dependencies": { - "@onekeyfe/hwk-trezor-protobuf": "1.2.0-alpha.176", - "@onekeyfe/hwk-trezor-protocol": "1.2.0-alpha.176", - "@onekeyfe/hwk-trezor-type-utils": "1.2.0-alpha.176", - "@onekeyfe/hwk-trezor-utils": "1.2.0-alpha.176", + "@onekeyfe/hwk-trezor-protobuf": "1.2.0-alpha.181", + "@onekeyfe/hwk-trezor-protocol": "1.2.0-alpha.181", + "@onekeyfe/hwk-trezor-type-utils": "1.2.0-alpha.181", + "@onekeyfe/hwk-trezor-utils": "1.2.0-alpha.181", "cross-fetch": "^4.1.0", "usb": "^2.17.0" }, diff --git a/packages/hwk-trezor-type-utils/package.json b/packages/hwk-trezor-type-utils/package.json index 69e00d082..5df106628 100644 --- a/packages/hwk-trezor-type-utils/package.json +++ b/packages/hwk-trezor-type-utils/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-type-utils", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "private": true, "description": "Type helpers vendored from Trezor Suite MIT sources for HWK Trezor support", "author": "OneKey", diff --git a/packages/hwk-trezor-utils/package.json b/packages/hwk-trezor-utils/package.json index 495d19413..479aef87a 100644 --- a/packages/hwk-trezor-utils/package.json +++ b/packages/hwk-trezor-utils/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hwk-trezor-utils", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "private": true, "description": "Utility helpers vendored from Trezor Suite MIT sources for HWK Trezor support", "author": "OneKey", @@ -39,7 +39,7 @@ }, "sideEffects": false, "dependencies": { - "@onekeyfe/hwk-trezor-type-utils": "1.2.0-alpha.176", + "@onekeyfe/hwk-trezor-type-utils": "1.2.0-alpha.181", "bignumber.js": "^9.3.1", "events": "^3.3.0" }, diff --git a/packages/shared/package.json b/packages/shared/package.json index 3c5f809bc..782b3f539 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@onekeyfe/hd-shared", - "version": "1.2.0-alpha.176", + "version": "1.2.0-alpha.181", "description": "Hardware SDK's shared tool library", "keywords": [ "Hardware-SDK", diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index e05284728..82f74b17b 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -344,6 +344,30 @@ export const hasOnekeyCommunicationService = ( matchesKnownBleUuid(uuid, ONEKEY_COMMUNICATION_SERVICE_ALIASES) ); +/** + * Protocol V2 family (Pro2 / Neo) by advertised BLE name. Callers use it to + * pick a connection strategy: this family also advertises under Find My names + * that carry no OneKey service UUID, so a scan cannot always see it. + */ +export const isPro2FamilyBleName = (value?: string | null): boolean => { + if (!value) { + return false; + } + const normalized = value.trim().toLowerCase(); + if (!normalized) { + return false; + } + if (normalized.startsWith('pro2') || normalized.startsWith('neo')) { + return true; + } + const compact = compactBleName(normalized); + return ( + PRO2_COMPACT_NAME_PATTERN.test(compact) || + NEO_COMPACT_NAME_PATTERN.test(compact) || + isPro2FindMyAdvertisementName(value) + ); +}; + export const isOnekeyBluetoothDevice = ({ id, name,