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
23 changes: 23 additions & 0 deletions Sources/BluetoothLinux/RFCOMM/RFCOMMFileDescriptor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// RFCOMMFileDescriptor.swift
// BluetoothLinux
//

import Bluetooth
import Socket

internal extension SocketDescriptor {

/// Creates an RFCOMM socket binded to the specified address.
@usableFromInline
static func rfcomm(
_ address: RFCOMMSocketAddress,
_ flags: SocketFlags
) throws(Errno) -> SocketDescriptor {
try bluetooth(
.rfcomm,
bind: address,
flags: flags
)
}
}
184 changes: 177 additions & 7 deletions Sources/BluetoothLinux/RFCOMM/RFCOMMSocket.swift
Original file line number Diff line number Diff line change
@@ -1,14 +1,184 @@
//
// RFCOMM.swift
//
//
// Created by Alsey Coleman Miller on 16/10/21.
// RFCOMMSocket.swift
// BluetoothLinux
//

import Foundation
import Bluetooth
import SystemPackage
import Socket

/// Bluetooth RFCOMM Socket
public actor RFCOMMSocket {


///
/// Stream socket providing serial port emulation over L2CAP.
public struct RFCOMMSocket: Sendable {

// MARK: - Properties

@usableFromInline
internal let fileDescriptor: SocketDescriptor

/// Socket address.
public let address: RFCOMMSocketAddress

// MARK: - Initialization

internal init(
fileDescriptor: SocketDescriptor,
address: RFCOMMSocketAddress
) {
self.fileDescriptor = fileDescriptor
self.address = address
}

/// Create a new RFCOMM socket bound to the specified address.
public init(address: RFCOMMSocketAddress) throws(Errno) {
self.fileDescriptor = try .rfcomm(address, [.closeOnExec, .nonBlocking])
self.address = address
}

/// Creates a client socket connected to the specified remote device and channel.
///
/// - Parameter channel: The RFCOMM channel of the service on the remote device (1-30),
/// typically discovered via its service record.
public static func client(
address localAddress: BluetoothAddress,
destination destinationAddress: BluetoothAddress,
channel: UInt8
) throws(Errno) -> Self {
let localSocketAddress = RFCOMMSocketAddress(
address: localAddress,
channel: 0
)
let destinationSocketAddress = RFCOMMSocketAddress(
address: destinationAddress,
channel: channel
)
let fileDescriptor = try SocketDescriptor.rfcomm(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)
}

/// Creates a server socket listening on the specified address and channel.
///
/// - Parameter channel: The RFCOMM channel to listen on (1-30).
public static func server(
address: BluetoothAddress,
channel: UInt8,
backlog: Int = Socket.maxBacklog
) throws(Errno) -> Self {
let socketAddress = RFCOMMSocketAddress(
address: address,
channel: channel
)
let fileDescriptor = try SocketDescriptor.rfcomm(socketAddress, [.closeOnExec, .nonBlocking])
try fileDescriptor.closeIfThrows { () throws(Errno) -> () in
try fileDescriptor.listen(backlog: backlog)
}
return Self.init(fileDescriptor: fileDescriptor, address: socketAddress)
}

// MARK: - Methods

/// Close socket.
public func close() {
try? fileDescriptor.close()
}

/// Attempt to accept an incoming connection.
public func accept() throws(Errno) -> Self {
let (fileDescriptor, address) = try self.fileDescriptor.accept(RFCOMMSocketAddress.self)
return Self.init(
fileDescriptor: fileDescriptor,
address: address
)
}

/// Write to the socket.
public func send(_ data: Data) throws(Errno) -> Int {
do {
return try data.withUnsafeBytes { (bytes) throws(Errno) -> Int in
try fileDescriptor.write(bytes)
}
}
catch {
throw error as! Errno // TODO: Foundation doesnt support typed error yet
}
}

/// Reads from the socket.
public func receive(_ length: Int) throws(Errno) -> Data {
do {
var data = Data(count: length)
let bytesRead = try data.withUnsafeMutableBytes { (bytes) throws(Errno) -> Int in
try fileDescriptor.read(into: bytes)
}
if bytesRead < length {
data = data.prefix(bytesRead)
}
return data
}
catch {
throw error as! Errno // TODO: Foundation doesnt support typed error yet
}
}

// MARK: - Options

/// Connection information (handle and device class).
public var connectionInfo: RFCOMMSocketOption.ConnectionInfo {
get throws(Errno) {
try fileDescriptor.getSocketOption(RFCOMMSocketOption.ConnectionInfo.self)
}
}

/// The link mode of the socket.
public var linkMode: RFCOMMSocketOption.LinkMode {
get throws(Errno) {
try fileDescriptor.getSocketOption(RFCOMMSocketOption.LinkMode.self)
}
}

/// Set the link mode of the socket.
public func setLinkMode(_ linkMode: RFCOMMSocketOption.LinkMode) throws(Errno) {
try fileDescriptor.setSocketOption(linkMode)
}

/// The security level of the socket.
public var security: BluetoothSocketOption.Security {
get throws(Errno) {
try fileDescriptor.getSocketOption(BluetoothSocketOption.Security.self)
}
}

/// Set the security level of the socket.
public func setSecurity(_ security: BluetoothSocketOption.Security) throws(Errno) {
try fileDescriptor.setSocketOption(security)
}
}
50 changes: 50 additions & 0 deletions Tests/BluetoothLinuxTests/RFCOMMSocketTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// RFCOMMSocketTests.swift
// BluetoothLinuxTests
//

import Foundation
import XCTest
import Bluetooth
import SystemPackage
import Socket
@testable import BluetoothLinux

final class RFCOMMSocketTests: XCTestCase {

func testSocketOptionValues() {
XCTAssertEqual(RFCOMMSocketOption.optionLevel.rawValue, 18) // SOL_RFCOMM
XCTAssertEqual(RFCOMMSocketOption.connectionInfo.rawValue, 0x02)
XCTAssertEqual(RFCOMMSocketOption.linkMode.rawValue, 0x03)
}

func testLinkModeEncoding() {
// `RFCOMM_LM` is a 16-bit bitmask
let linkMode = RFCOMMSocketOption.LinkMode(linkMode: [.authenticated, .encrypted])
linkMode.withUnsafeBytes { buffer in
XCTAssertEqual(buffer.count, MemoryLayout<RFCOMMLinkMode.RawValue>.size)
XCTAssertEqual(buffer[0], 0x06)
XCTAssertEqual(buffer[1], 0x00)
}
let decoded = RFCOMMSocketOption.LinkMode.withUnsafeBytes { buffer in
buffer[0] = 0x24
buffer[1] = 0x00
}
XCTAssertEqual(decoded.linkMode, [.encrypted, .secure])
}

#if os(Linux)
func testSocketAddressEncoding() {
// socket address construction requires the Bluetooth address family (Linux only)
let address = RFCOMMSocketAddress(
address: BluetoothAddress(rawValue: "00:1A:7D:DA:71:13")!,
channel: 3
)
address.withUnsafePointer { pointer, size in
XCTAssertEqual(Int(size), MemoryLayout<CInterop.RFCOMMSocketAddress>.size)
let bytes = UnsafeRawPointer(pointer).loadUnaligned(as: CInterop.RFCOMMSocketAddress.self)
XCTAssertEqual(bytes.channel, 3)
}
}
#endif
}
Loading