Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
164 changes: 164 additions & 0 deletions Sources/BluetoothLinux/L2CAP/L2CAPSocket+Classic.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
//
// L2CAPSocket+Classic.swift
// BluetoothLinux
//
// Classic (BR/EDR) L2CAP connection conveniences.
//

import Foundation
import Bluetooth
import BluetoothHCI
import SystemPackage
import Socket

public extension L2CAPSocket {

/// Creates a client socket for a classic (BR/EDR) L2CAP connection.
///
/// - Parameter protocolServiceMultiplexer: The protocol/service multiplexer (PSM)
/// of the service on the remote device (e.g. ``ProtocolServiceMultiplexer/bnep``).
static func classicClient(
address localAddress: BluetoothAddress,
destination destinationAddress: BluetoothAddress,
protocolServiceMultiplexer: ProtocolServiceMultiplexer
) throws(Errno) -> Self {
let localSocketAddress = L2CAPSocketAddress(
address: localAddress,
addressType: nil,
protocolServiceMultiplexer: nil,
channel: 0
)
let destinationSocketAddress = L2CAPSocketAddress(
address: destinationAddress,
addressType: nil,
protocolServiceMultiplexer: protocolServiceMultiplexer,
channel: 0
)
return try connect(
local: localSocketAddress,
destination: destinationSocketAddress
)
}

/// Creates a client socket for a classic (BR/EDR) L2CAP connection.
static func classicClient(
hostController: HostController,
destination: BluetoothAddress,
protocolServiceMultiplexer: ProtocolServiceMultiplexer
) async throws -> Self {
let localAddress = try await hostController.readDeviceAddress()
return try classicClient(
address: localAddress,
destination: destination,
protocolServiceMultiplexer: protocolServiceMultiplexer
)
}

/// Creates a server socket for a classic (BR/EDR) L2CAP connection.
static func classicServer(
address: BluetoothAddress,
protocolServiceMultiplexer: ProtocolServiceMultiplexer,
backlog: Int = Socket.maxBacklog
) throws(Errno) -> Self {
let address = L2CAPSocketAddress(
address: address,
addressType: nil,
protocolServiceMultiplexer: protocolServiceMultiplexer,
channel: 0
)
let fileDescriptor = try SocketDescriptor.l2cap(address, [.closeOnExec, .nonBlocking])
try fileDescriptor.closeIfThrows { () throws(Errno) -> () in
try fileDescriptor.listen(backlog: backlog)
}
return Self.init(fileDescriptor: fileDescriptor, address: address)
}

/// Creates a server socket for a classic (BR/EDR) L2CAP connection.
static func classicServer(
hostController: HostController,
protocolServiceMultiplexer: ProtocolServiceMultiplexer,
backlog: Int = Socket.maxBacklog
) async throws -> Self {
let address = try await hostController.readDeviceAddress()
return try classicServer(
address: address,
protocolServiceMultiplexer: protocolServiceMultiplexer,
backlog: backlog
)
}

/// Creates a socket and connects to the destination, waiting until the
/// non-blocking connection completes.
internal static func connect(
local localSocketAddress: L2CAPSocketAddress,
destination destinationSocketAddress: L2CAPSocketAddress
) throws(Errno) -> Self {
let fileDescriptor = try SocketDescriptor.l2cap(localSocketAddress, [.closeOnExec, .nonBlocking])

// 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)
}
}

// MARK: - Server

public extension L2CAPSocket.Server {

/// Creates a server socket for a classic (BR/EDR) L2CAP connection.
static func classicServer(
address: BluetoothAddress,
protocolServiceMultiplexer: ProtocolServiceMultiplexer,
backlog: Int = Socket.maxBacklog
) throws(Errno) -> L2CAPSocket.Server {
let socket = try L2CAPSocket.classicServer(
address: address,
protocolServiceMultiplexer: protocolServiceMultiplexer,
backlog: backlog
)
return Self.init(socket: socket)
}
}

// MARK: - Connection

public extension L2CAPSocket.Connection {

/// Creates a client socket for a classic (BR/EDR) L2CAP connection.
static func classicClient(
address: BluetoothAddress,
destination: BluetoothAddress,
protocolServiceMultiplexer: ProtocolServiceMultiplexer
) throws(Errno) -> Self {
let socket = try L2CAPSocket.classicClient(
address: address,
destination: destination,
protocolServiceMultiplexer: protocolServiceMultiplexer
)
return .init(socket: socket, destination: destination)
}
}
60 changes: 60 additions & 0 deletions Tests/BluetoothLinuxTests/L2CAPSocketAddressTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// L2CAPSocketAddressTests.swift
// BluetoothLinuxTests
//
// Socket address construction requires the Bluetooth address family,
// which is only available on Linux (these run in CI).
//

#if os(Linux)
import Foundation
import XCTest
import Bluetooth
import BluetoothHCI
import SystemPackage
import Socket
@testable import BluetoothLinux

final class L2CAPSocketAddressTests: XCTestCase {

func testClassicAddressEncoding() {
let address = L2CAPSocketAddress(
address: BluetoothAddress(rawValue: "00:1A:7D:DA:71:13")!,
addressType: nil,
protocolServiceMultiplexer: .bnep,
channel: 0
)
address.withUnsafePointer { pointer, size in
XCTAssertEqual(Int(size), MemoryLayout<CInterop.L2CAPSocketAddress>.size)
let bytes = UnsafeRawPointer(pointer).loadUnaligned(as: CInterop.L2CAPSocketAddress.self)
XCTAssertEqual(UInt16(littleEndian: bytes.l2_psm), 0x000F)
XCTAssertEqual(bytes.l2_cid, 0)
XCTAssertEqual(bytes.l2_bdaddr_type, 0)
XCTAssertEqual(BluetoothAddress(littleEndian: bytes.l2_bdaddr).rawValue, "00:1A:7D:DA:71:13")
}
}

func testClassicAddressDecoding() {
var bytes = CInterop.L2CAPSocketAddress()
bytes.l2_psm = UInt16(0x0011).littleEndian // HID Control
bytes.l2_bdaddr = BluetoothAddress(rawValue: "00:1A:7D:DA:71:13")!.littleEndian
let address = L2CAPSocketAddress(bytes)
XCTAssertEqual(address.protocolServiceMultiplexer, .hidc)
XCTAssertEqual(address.addressType, nil)
XCTAssertEqual(address.channel.rawValue, 0)
XCTAssertEqual(address.address.rawValue, "00:1A:7D:DA:71:13")
}

func testLowEnergyAddressEncoding() {
let address = L2CAPSocketAddress(
lowEnergy: BluetoothAddress(rawValue: "00:1A:7D:DA:71:13")!,
isRandom: false
)
address.withUnsafePointer { pointer, size in
let bytes = UnsafeRawPointer(pointer).loadUnaligned(as: CInterop.L2CAPSocketAddress.self)
XCTAssertEqual(UInt16(littleEndian: bytes.l2_cid), 0x0004) // ATT
XCTAssertEqual(bytes.l2_bdaddr_type, AddressType.lowEnergyPublic.rawValue)
}
}
}
#endif
Loading