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

import Bluetooth
import Socket

internal extension SocketDescriptor {

/// Creates an ISO socket binded to the specified address.
@usableFromInline
static func iso(
_ address: ISOSocketAddress,
_ flags: SocketFlags
) throws(Errno) -> SocketDescriptor {
try bluetooth(
.iso,
bind: address,
flags: flags
)
}
}
112 changes: 112 additions & 0 deletions Sources/BluetoothLinux/ISO/ISOQualityOfService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
//
// ISOQualityOfService.swift
// BluetoothLinux
//

import Bluetooth
import SystemPackage
import Socket

public extension BluetoothSocketOption {

/// Bluetooth Isochronous Channel Quality of Service socket option
///
/// Configures the connected isochronous stream parameters of an ISO socket
/// (`bt_iso_qos`, unicast). Must be set before the connection is established.
struct ISOQualityOfService: Equatable, Hashable, SocketOption, Sendable {

@_alwaysEmitIntoClient
public static var id: BluetoothSocketOption { .isoQualityOfService }

/// The connected isochronous group identifier (`0xFF` for unset).
public var group: UInt8 // cig

/// The connected isochronous stream identifier (`0xFF` for unset).
public var stream: UInt8 // cis

/// The sleep clock accuracy.
public var sleepClockAccuracy: UInt8 // sca

/// The preferred method of arranging subevents of multiple streams.
public var packing: UInt8

/// The format of the sent data (unframed or framed).
public var framing: UInt8

/// Input (receive) parameters.
public var input: IO

/// Output (send) parameters.
public var output: IO

public init(
group: UInt8 = 0xFF,
stream: UInt8 = 0xFF,
sleepClockAccuracy: UInt8 = 0,
packing: UInt8 = 0,
framing: UInt8 = 0,
input: IO = IO(),
output: IO = IO()
) {
self.group = group
self.stream = stream
self.sleepClockAccuracy = sleepClockAccuracy
self.packing = packing
self.framing = framing
self.input = input
self.output = output
}

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

public extension BluetoothSocketOption.ISOQualityOfService {

/// Isochronous stream input/output parameters.
///
/// `bt_iso_io_qos`
struct IO: Equatable, Hashable, Sendable {

/// SDU interval in microseconds.
public var interval: UInt32

/// Maximum transport latency in milliseconds.
public var latency: UInt16

/// Maximum SDU size in octets.
public var sdu: UInt16

/// PHY to use.
public var phy: UInt8

/// Retransmission number.
public var retransmissionNumber: UInt8 // rtn

public init(
interval: UInt32 = 0,
latency: UInt16 = 0,
sdu: UInt16 = 0,
phy: UInt8 = 0,
retransmissionNumber: UInt8 = 0
) {
self.interval = interval
self.latency = latency
self.sdu = sdu
self.phy = phy
self.retransmissionNumber = retransmissionNumber
}
}
}
165 changes: 165 additions & 0 deletions Sources/BluetoothLinux/ISO/ISOSocket.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
//
// ISOSocket.swift
// BluetoothLinux
//

import Foundation
import Bluetooth
import SystemPackage
import Socket

/// Bluetooth ISO Socket
///
/// Isochronous channels carry time-sensitive audio data between the host
/// and Low Energy devices (LE Audio). Requires kernel 6.0 or later; on older
/// kernels the ISO socket is gated behind an experimental feature toggled
/// via the management interface.
public struct ISOSocket: Sendable {

// MARK: - Properties

@usableFromInline
internal let fileDescriptor: SocketDescriptor

/// Socket address.
public let address: ISOSocketAddress

// MARK: - Initialization

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

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

/// Creates a client socket connected to the specified remote device.
///
/// - Note: The quality of service must be configured before connecting
/// when using non-default stream parameters.
public static func client(
address localAddress: ISOSocketAddress,
destination destinationAddress: ISOSocketAddress,
qualityOfService: BluetoothSocketOption.ISOQualityOfService? = nil
) throws(Errno) -> Self {
let fileDescriptor = try SocketDescriptor.iso(localAddress, [.closeOnExec, .nonBlocking])

// configure stream parameters before connecting
if let qualityOfService {
do {
try fileDescriptor.setSocketOption(qualityOfService)
} catch {
try? fileDescriptor.close()
throw error
}
}

// Start async connect - for non-blocking sockets this returns EINPROGRESS
do {
try fileDescriptor.connect(to: destinationAddress)
} 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: localAddress)
}

/// Creates a server socket listening on the specified address.
public static func server(
address: ISOSocketAddress,
backlog: Int = Socket.maxBacklog
) throws(Errno) -> Self {
let fileDescriptor = try SocketDescriptor.iso(address, [.closeOnExec, .nonBlocking])
try fileDescriptor.closeIfThrows { () throws(Errno) -> () in
try fileDescriptor.listen(backlog: backlog)
}
return Self.init(fileDescriptor: fileDescriptor, address: address)
}

// 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(ISOSocketAddress.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

/// The quality of service of the socket.
public var qualityOfService: BluetoothSocketOption.ISOQualityOfService {
get throws(Errno) {
try fileDescriptor.getSocketOption(BluetoothSocketOption.ISOQualityOfService.self)
}
}

/// Set the quality of service of the socket.
///
/// Must be configured before the connection is established.
public func setQualityOfService(_ qualityOfService: BluetoothSocketOption.ISOQualityOfService) throws(Errno) {
try fileDescriptor.setSocketOption(qualityOfService)
}
}
74 changes: 74 additions & 0 deletions Sources/BluetoothLinux/ISO/ISOSocketAddress.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//
// ISOSocketAddress.swift
// BluetoothLinux
//

import SystemPackage
import Socket
import Bluetooth

/// Bluetooth ISO Socket Address
@frozen
public struct ISOSocketAddress: Equatable, Hashable, Sendable {

// MARK: - Properties

/// Bluetooth address
public var address: BluetoothAddress

/// Bluetooth address type
public var addressType: AddressType

// MARK: - Initialization

public init(
address: BluetoothAddress,
addressType: AddressType = .lowEnergyPublic
) {
self.address = address
self.addressType = addressType
}
}

extension ISOSocketAddress: BluetoothSocketAddress {

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

/// 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.ISOSocketAddress()
value.address = address.littleEndian
value.type = addressType.rawValue
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.ISOSocketAddress()
try value.withUnsafeMutablePointer(body)
return Self.init(value)
}

public static func withUnsafePointer(
_ pointer: UnsafeMutablePointer<CInterop.SocketAddress>
) -> Self {
pointer.withMemoryRebound(to: CInterop.ISOSocketAddress.self, capacity: 1) { pointer in
Self.init(pointer.pointee)
}
}
}

internal extension ISOSocketAddress {

@usableFromInline
init(_ bytes: CInterop.ISOSocketAddress) {
self.init(
address: .init(littleEndian: bytes.address),
addressType: AddressType(rawValue: bytes.type) ?? .lowEnergyPublic
)
}
}
Loading
Loading