From 20cc5a8cf5f62f8486c34c8cc1797cebe2b748d1 Mon Sep 17 00:00:00 2001 From: Jonas Tehler Date: Sat, 24 Jan 2026 17:46:06 +0100 Subject: [PATCH 1/2] Fix HCI event filter for events >= 32 (including LE Meta Event) The setEvent() method always set bits in eventMask.0, even for events with codes >= 32. The HCI filter structure uses two 32-bit event mask words: eventMask.0 for events 0-31 and eventMask.1 for events 32-63. For LE Meta Event (code 0x3E = 62), this meant bit 30 was incorrectly set in eventMask.0 instead of eventMask.1, causing the kernel to filter out all LE Meta events. This made BLE scanning non-functional - the stream would be created successfully but never yield any results. The fix routes events >= 32 to eventMask.1 with the appropriate bit offset. Tested on Linux 6.8 with Realtek USB Bluetooth adapter - scanning now correctly discovers BLE devices. --- Sources/BluetoothLinux/Internal/CInterop.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Sources/BluetoothLinux/Internal/CInterop.swift b/Sources/BluetoothLinux/Internal/CInterop.swift index b5ed39e..bddf54c 100644 --- a/Sources/BluetoothLinux/Internal/CInterop.swift +++ b/Sources/BluetoothLinux/Internal/CInterop.swift @@ -589,8 +589,15 @@ internal extension CInterop.HCIFilterSocketOption { @usableFromInline mutating func setEvent(_ event: UInt8) { - let bit = (CInt(event) & 63) - HCISetBit(bit, &eventMask.0) + // Events 0-31 use eventMask.0, events 32-63 use eventMask.1 + // Bug fix: was always using eventMask.0 even for events >= 32 + if event >= 32 { + let bit = CInt(event) - 32 + HCISetBit(bit, &eventMask.1) + } else { + let bit = CInt(event) + HCISetBit(bit, &eventMask.0) + } } @usableFromInline From aef4b14397cfc332253714bc5bd43caceccb8c23 Mon Sep 17 00:00:00 2001 From: Jonas Tehler Date: Sat, 24 Jan 2026 20:26:00 +0100 Subject: [PATCH 2/2] Fix L2CAP non-blocking connect to wait for connection completion The lowEnergyClient() method was returning immediately after starting a non-blocking connect, without waiting for the connection to actually complete. For non-blocking sockets, connect() returns EINPROGRESS and the caller must poll for writability to know when the connection is established. This caused GATT operations to fail because the socket wasn't actually connected when the GATT client tried to use it. The fix: 1. Catches EINPROGRESS from the non-blocking connect 2. Polls for writability with a 30 second timeout 3. Checks for error/hangup events indicating connection failure 4. Only returns the socket once the connection is confirmed --- .../BluetoothLinux/L2CAP/L2CAPSocket.swift | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/Sources/BluetoothLinux/L2CAP/L2CAPSocket.swift b/Sources/BluetoothLinux/L2CAP/L2CAPSocket.swift index 576682f..95de838 100755 --- a/Sources/BluetoothLinux/L2CAP/L2CAPSocket.swift +++ b/Sources/BluetoothLinux/L2CAP/L2CAPSocket.swift @@ -121,7 +121,33 @@ public struct L2CAPSocket: Sendable { channel: .att ) let fileDescriptor = try SocketDescriptor.l2cap(localSocketAddress, [.closeOnExec, .nonBlocking]) - try? fileDescriptor.connect(to: destinationSocketAddress) // ignore result, async socket always throws + + // Start async connect - for non-blocking sockets this returns EINPROGRESS + do { + try fileDescriptor.connect(to: destinationSocketAddress) + } catch Errno.nowInProgress { + // Expected for non-blocking socket - connection is in progress + // Wait for socket to become writable (indicates connect completed) + let timeout: Int = 30_000 // 30 seconds in milliseconds + let events = try fileDescriptor.poll(for: [.write, .error, .hangup], timeout: timeout) + + // Check for errors + if events.contains(.error) || events.contains(.hangup) { + try? fileDescriptor.close() + throw Errno.connectionRefused + } + + // Check if we timed out (no events returned) + if !events.contains(.write) { + try? fileDescriptor.close() + throw Errno.timedOut + } + } catch { + // Other errors during connect + try? fileDescriptor.close() + throw error + } + return Self.init(fileDescriptor: fileDescriptor, address: localSocketAddress) }