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/SCO/SCOFileDescriptor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// SCOFileDescriptor.swift
// BluetoothLinux
//

import Bluetooth
import Socket

internal extension SocketDescriptor {

/// Creates an SCO socket binded to the specified address.
@usableFromInline
static func sco(
_ address: SCOSocketAddress,
_ flags: SocketFlags
) throws(Errno) -> SocketDescriptor {
try bluetooth(
.sco,
bind: address,
flags: flags
)
}
}
178 changes: 172 additions & 6 deletions Sources/BluetoothLinux/SCO/SCOSocket.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,181 @@
//
// SCOSocket.swift
//
//
// Created by Alsey Coleman Miller on 16/10/21.
// BluetoothLinux
//

import Foundation
import Bluetooth
import SystemPackage
import Socket

/// Bluetooth SCO Socket
public actor SCOSocket {


///
/// Synchronous Connection Oriented links carry voice audio
/// between the host and a connected classic (BR/EDR) device.
public struct SCOSocket: Sendable {

// MARK: - Properties

@usableFromInline
internal let fileDescriptor: SocketDescriptor

/// Socket address.
public let address: SCOSocketAddress

// MARK: - Initialization

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

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

/// Creates a client socket connected to the specified remote device.
///
/// - Note: An ACL connection to the remote device must already exist,
/// and a non-default air coding format (``BluetoothSocketOption/Voice``)
/// must be configured before connecting.
public static func client(
address localAddress: BluetoothAddress,
destination destinationAddress: BluetoothAddress,
voice: BluetoothSocketOption.Voice? = nil
) throws(Errno) -> Self {
let localSocketAddress = SCOSocketAddress(address: localAddress)
let destinationSocketAddress = SCOSocketAddress(address: destinationAddress)
let fileDescriptor = try SocketDescriptor.sco(localSocketAddress, [.closeOnExec, .nonBlocking])

// configure the air coding format before connecting
if let voice {
do {
try fileDescriptor.setSocketOption(voice)
} catch {
try? fileDescriptor.close()
throw error
}
}

// 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.
public static func server(
address: BluetoothAddress,
backlog: Int = Socket.maxBacklog
) throws(Errno) -> Self {
let socketAddress = SCOSocketAddress(address: address)
let fileDescriptor = try SocketDescriptor.sco(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(SCOSocketAddress.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

/// Socket options (maximum transmission unit).
public var options: SCOSocketOption.Options {
get throws(Errno) {
try fileDescriptor.getSocketOption(SCOSocketOption.Options.self)
}
}

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

/// The voice setting (air coding format) of the socket.
public var voice: BluetoothSocketOption.Voice {
get throws(Errno) {
try fileDescriptor.getSocketOption(BluetoothSocketOption.Voice.self)
}
}

/// Set the voice setting (air coding format) of the socket.
///
/// Must be configured before the connection is established.
public func setVoice(_ voice: BluetoothSocketOption.Voice) throws(Errno) {
try fileDescriptor.setSocketOption(voice)
}
}
55 changes: 55 additions & 0 deletions Sources/BluetoothLinux/SCO/SCOSocketAddress.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// SCOSocketAddress.swift
// BluetoothLinux
//

import SystemPackage
import Socket
import Bluetooth

/// Bluetooth SCO Socket Address
@frozen
public struct SCOSocketAddress: Equatable, Hashable, Sendable {

// MARK: - Properties

/// Bluetooth address
public var address: BluetoothAddress

// MARK: - Initialization

public init(address: BluetoothAddress) {
self.address = address
}
}

extension SCOSocketAddress: BluetoothSocketAddress {

@_alwaysEmitIntoClient
public static var protocolID: BluetoothSocketProtocol { .sco }

/// Unsafe pointer closure
public func withUnsafePointer<Result, Error>(
_ body: (UnsafePointer<CInterop.SocketAddress>, UInt32) throws(Error) -> Result
) rethrows -> Result where Error: Swift.Error {
var value = CInterop.SCOSocketAddress()
value.address = address.littleEndian
return try value.withUnsafePointer(body)
}

public static func withUnsafePointer<Error>(
_ body: (UnsafeMutablePointer<CInterop.SocketAddress>, UInt32) throws(Error) -> ()
) rethrows -> Self where Error: Swift.Error {
var value = CInterop.SCOSocketAddress()
try value.withUnsafeMutablePointer(body)
return Self.init(address: .init(littleEndian: value.address))
}

public static func withUnsafePointer(
_ pointer: UnsafeMutablePointer<CInterop.SocketAddress>
) -> Self {
pointer.withMemoryRebound(to: CInterop.SCOSocketAddress.self, capacity: 1) { pointer in
Self.init(address: .init(littleEndian: pointer.pointee.address))
}
}
}
103 changes: 98 additions & 5 deletions Sources/BluetoothLinux/SCO/SCOSocketOption.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,101 @@
//
// File.swift
//
//
// Created by Alsey Coleman Miller on 16/10/21.
// SCOSocketOption.swift
// BluetoothLinux
//

import Foundation
import SystemPackage
import Socket

/// SCO Socket Options
public enum SCOSocketOption: CInt, SocketOptionID {

public static var optionLevel: SocketOptionLevel { .sco }

/// SCO Socket Options
case options = 0x01

/// SCO Connection Info
case connectionInfo = 0x02
}

public extension SCOSocketOption {

/// SCO Socket Options
///
/// `sco_options`
struct Options: Equatable, Hashable, SocketOption, Sendable {

@_alwaysEmitIntoClient
public static var id: SCOSocketOption { .options }

/// Maximum transmission unit.
public var maximumTransmissionUnit: UInt16 // mtu

public init() {
self.maximumTransmissionUnit = 0
}

public func withUnsafeBytes<Result, Error>(_ body: ((UnsafeRawBufferPointer) throws(Error) -> (Result))) rethrows -> Result where Error: Swift.Error {
return try Swift.withUnsafeBytes(of: self) { bufferPointer in
try body(bufferPointer)
}
}

public static func withUnsafeBytes<Error>(
_ body: (UnsafeMutableRawBufferPointer) throws(Error) -> ()
) rethrows -> Self where Error: Swift.Error {
var value = self.init()
try Swift.withUnsafeMutableBytes(of: &value, body)
return value
}
}

/// SCO Connection Information
///
/// `sco_conninfo`
struct ConnectionInfo: SocketOption, Sendable {

@_alwaysEmitIntoClient
public static var id: SCOSocketOption { .connectionInfo }

/// Connection handle.
public var handle: UInt16

/// Device class.
public var deviceClass: (UInt8, UInt8, UInt8)

public init() {
self.handle = 0
self.deviceClass = (0, 0, 0)
}

public func withUnsafeBytes<Result, Error>(_ body: ((UnsafeRawBufferPointer) throws(Error) -> (Result))) rethrows -> Result where Error: Swift.Error {
return try Swift.withUnsafeBytes(of: self) { bufferPointer in
try body(bufferPointer)
}
}

public static func withUnsafeBytes<Error>(
_ body: (UnsafeMutableRawBufferPointer) throws(Error) -> ()
) rethrows -> Self where Error: Swift.Error {
var value = self.init()
try Swift.withUnsafeMutableBytes(of: &value, body)
return value
}
}
}

extension SCOSocketOption.ConnectionInfo: Equatable, Hashable {

public static func == (lhs: SCOSocketOption.ConnectionInfo, rhs: SCOSocketOption.ConnectionInfo) -> Bool {
lhs.handle == rhs.handle
&& lhs.deviceClass == rhs.deviceClass
}

public func hash(into hasher: inout Hasher) {
hasher.combine(handle)
hasher.combine(deviceClass.0)
hasher.combine(deviceClass.1)
hasher.combine(deviceClass.2)
}
}
Loading
Loading