Skip to content

Commit d339675

Browse files
authored
Merge pull request #62 from PureSwift/feature/iso
Add ISO socket support
2 parents ca65b12 + ee040b5 commit d339675

8 files changed

Lines changed: 484 additions & 1 deletion

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// ISOFileDescriptor.swift
3+
// BluetoothLinux
4+
//
5+
6+
import Bluetooth
7+
import Socket
8+
9+
internal extension SocketDescriptor {
10+
11+
/// Creates an ISO socket binded to the specified address.
12+
@usableFromInline
13+
static func iso(
14+
_ address: ISOSocketAddress,
15+
_ flags: SocketFlags
16+
) throws(Errno) -> SocketDescriptor {
17+
try bluetooth(
18+
.iso,
19+
bind: address,
20+
flags: flags
21+
)
22+
}
23+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//
2+
// ISOQualityOfService.swift
3+
// BluetoothLinux
4+
//
5+
6+
import Bluetooth
7+
import SystemPackage
8+
import Socket
9+
10+
public extension BluetoothSocketOption {
11+
12+
/// Bluetooth Isochronous Channel Quality of Service socket option
13+
///
14+
/// Configures the connected isochronous stream parameters of an ISO socket
15+
/// (`bt_iso_qos`, unicast). Must be set before the connection is established.
16+
struct ISOQualityOfService: Equatable, Hashable, SocketOption, Sendable {
17+
18+
@_alwaysEmitIntoClient
19+
public static var id: BluetoothSocketOption { .isoQualityOfService }
20+
21+
/// The connected isochronous group identifier (`0xFF` for unset).
22+
public var group: UInt8 // cig
23+
24+
/// The connected isochronous stream identifier (`0xFF` for unset).
25+
public var stream: UInt8 // cis
26+
27+
/// The sleep clock accuracy.
28+
public var sleepClockAccuracy: UInt8 // sca
29+
30+
/// The preferred method of arranging subevents of multiple streams.
31+
public var packing: UInt8
32+
33+
/// The format of the sent data (unframed or framed).
34+
public var framing: UInt8
35+
36+
/// Input (receive) parameters.
37+
public var input: IO
38+
39+
/// Output (send) parameters.
40+
public var output: IO
41+
42+
public init(
43+
group: UInt8 = 0xFF,
44+
stream: UInt8 = 0xFF,
45+
sleepClockAccuracy: UInt8 = 0,
46+
packing: UInt8 = 0,
47+
framing: UInt8 = 0,
48+
input: IO = IO(),
49+
output: IO = IO()
50+
) {
51+
self.group = group
52+
self.stream = stream
53+
self.sleepClockAccuracy = sleepClockAccuracy
54+
self.packing = packing
55+
self.framing = framing
56+
self.input = input
57+
self.output = output
58+
}
59+
60+
public func withUnsafeBytes<Result, Error>(_ body: ((UnsafeRawBufferPointer) throws(Error) -> (Result))) rethrows -> Result where Error: Swift.Error {
61+
return try Swift.withUnsafeBytes(of: self) { bufferPointer in
62+
try body(bufferPointer)
63+
}
64+
}
65+
66+
public static func withUnsafeBytes<Error>(
67+
_ body: (UnsafeMutableRawBufferPointer) throws(Error) -> ()
68+
) rethrows -> Self where Error: Swift.Error {
69+
var value = self.init()
70+
try Swift.withUnsafeMutableBytes(of: &value, body)
71+
return value
72+
}
73+
}
74+
}
75+
76+
public extension BluetoothSocketOption.ISOQualityOfService {
77+
78+
/// Isochronous stream input/output parameters.
79+
///
80+
/// `bt_iso_io_qos`
81+
struct IO: Equatable, Hashable, Sendable {
82+
83+
/// SDU interval in microseconds.
84+
public var interval: UInt32
85+
86+
/// Maximum transport latency in milliseconds.
87+
public var latency: UInt16
88+
89+
/// Maximum SDU size in octets.
90+
public var sdu: UInt16
91+
92+
/// PHY to use.
93+
public var phy: UInt8
94+
95+
/// Retransmission number.
96+
public var retransmissionNumber: UInt8 // rtn
97+
98+
public init(
99+
interval: UInt32 = 0,
100+
latency: UInt16 = 0,
101+
sdu: UInt16 = 0,
102+
phy: UInt8 = 0,
103+
retransmissionNumber: UInt8 = 0
104+
) {
105+
self.interval = interval
106+
self.latency = latency
107+
self.sdu = sdu
108+
self.phy = phy
109+
self.retransmissionNumber = retransmissionNumber
110+
}
111+
}
112+
}
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
//
2+
// ISOSocket.swift
3+
// BluetoothLinux
4+
//
5+
6+
import Foundation
7+
import Bluetooth
8+
import SystemPackage
9+
import Socket
10+
11+
/// Bluetooth ISO Socket
12+
///
13+
/// Isochronous channels carry time-sensitive audio data between the host
14+
/// and Low Energy devices (LE Audio). Requires kernel 6.0 or later; on older
15+
/// kernels the ISO socket is gated behind an experimental feature toggled
16+
/// via the management interface.
17+
public struct ISOSocket: Sendable {
18+
19+
// MARK: - Properties
20+
21+
@usableFromInline
22+
internal let fileDescriptor: SocketDescriptor
23+
24+
/// Socket address.
25+
public let address: ISOSocketAddress
26+
27+
// MARK: - Initialization
28+
29+
internal init(
30+
fileDescriptor: SocketDescriptor,
31+
address: ISOSocketAddress
32+
) {
33+
self.fileDescriptor = fileDescriptor
34+
self.address = address
35+
}
36+
37+
/// Create a new ISO socket bound to the specified address.
38+
public init(address: ISOSocketAddress) throws(Errno) {
39+
self.fileDescriptor = try .iso(address, [.closeOnExec, .nonBlocking])
40+
self.address = address
41+
}
42+
43+
/// Creates a client socket connected to the specified remote device.
44+
///
45+
/// - Note: The quality of service must be configured before connecting
46+
/// when using non-default stream parameters.
47+
public static func client(
48+
address localAddress: ISOSocketAddress,
49+
destination destinationAddress: ISOSocketAddress,
50+
qualityOfService: BluetoothSocketOption.ISOQualityOfService? = nil
51+
) throws(Errno) -> Self {
52+
let fileDescriptor = try SocketDescriptor.iso(localAddress, [.closeOnExec, .nonBlocking])
53+
54+
// configure stream parameters before connecting
55+
if let qualityOfService {
56+
do {
57+
try fileDescriptor.setSocketOption(qualityOfService)
58+
} catch {
59+
try? fileDescriptor.close()
60+
throw error
61+
}
62+
}
63+
64+
// Start async connect - for non-blocking sockets this returns EINPROGRESS
65+
do {
66+
try fileDescriptor.connect(to: destinationAddress)
67+
} catch Errno.nowInProgress {
68+
// Expected for non-blocking socket - connection is in progress
69+
// Wait for socket to become writable (indicates connect completed)
70+
let timeout: Int = 30_000 // 30 seconds in milliseconds
71+
let events = try fileDescriptor.poll(for: [.write, .error, .hangup], timeout: timeout)
72+
73+
// Check for errors
74+
if events.contains(.error) || events.contains(.hangup) {
75+
try? fileDescriptor.close()
76+
throw Errno.connectionRefused
77+
}
78+
79+
// Check if we timed out (no events returned)
80+
if !events.contains(.write) {
81+
try? fileDescriptor.close()
82+
throw Errno.timedOut
83+
}
84+
} catch {
85+
// Other errors during connect
86+
try? fileDescriptor.close()
87+
throw error
88+
}
89+
90+
return Self.init(fileDescriptor: fileDescriptor, address: localAddress)
91+
}
92+
93+
/// Creates a server socket listening on the specified address.
94+
public static func server(
95+
address: ISOSocketAddress,
96+
backlog: Int = Socket.maxBacklog
97+
) throws(Errno) -> Self {
98+
let fileDescriptor = try SocketDescriptor.iso(address, [.closeOnExec, .nonBlocking])
99+
try fileDescriptor.closeIfThrows { () throws(Errno) -> () in
100+
try fileDescriptor.listen(backlog: backlog)
101+
}
102+
return Self.init(fileDescriptor: fileDescriptor, address: address)
103+
}
104+
105+
// MARK: - Methods
106+
107+
/// Close socket.
108+
public func close() {
109+
try? fileDescriptor.close()
110+
}
111+
112+
/// Attempt to accept an incoming connection.
113+
public func accept() throws(Errno) -> Self {
114+
let (fileDescriptor, address) = try self.fileDescriptor.accept(ISOSocketAddress.self)
115+
return Self.init(
116+
fileDescriptor: fileDescriptor,
117+
address: address
118+
)
119+
}
120+
121+
/// Write to the socket.
122+
public func send(_ data: Data) throws(Errno) -> Int {
123+
do {
124+
return try data.withUnsafeBytes { (bytes) throws(Errno) -> Int in
125+
try fileDescriptor.write(bytes)
126+
}
127+
}
128+
catch {
129+
throw error as! Errno // TODO: Foundation doesnt support typed error yet
130+
}
131+
}
132+
133+
/// Reads from the socket.
134+
public func receive(_ length: Int) throws(Errno) -> Data {
135+
do {
136+
var data = Data(count: length)
137+
let bytesRead = try data.withUnsafeMutableBytes { (bytes) throws(Errno) -> Int in
138+
try fileDescriptor.read(into: bytes)
139+
}
140+
if bytesRead < length {
141+
data = data.prefix(bytesRead)
142+
}
143+
return data
144+
}
145+
catch {
146+
throw error as! Errno // TODO: Foundation doesnt support typed error yet
147+
}
148+
}
149+
150+
// MARK: - Options
151+
152+
/// The quality of service of the socket.
153+
public var qualityOfService: BluetoothSocketOption.ISOQualityOfService {
154+
get throws(Errno) {
155+
try fileDescriptor.getSocketOption(BluetoothSocketOption.ISOQualityOfService.self)
156+
}
157+
}
158+
159+
/// Set the quality of service of the socket.
160+
///
161+
/// Must be configured before the connection is established.
162+
public func setQualityOfService(_ qualityOfService: BluetoothSocketOption.ISOQualityOfService) throws(Errno) {
163+
try fileDescriptor.setSocketOption(qualityOfService)
164+
}
165+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//
2+
// ISOSocketAddress.swift
3+
// BluetoothLinux
4+
//
5+
6+
import SystemPackage
7+
import Socket
8+
import Bluetooth
9+
10+
/// Bluetooth ISO Socket Address
11+
@frozen
12+
public struct ISOSocketAddress: Equatable, Hashable, Sendable {
13+
14+
// MARK: - Properties
15+
16+
/// Bluetooth address
17+
public var address: BluetoothAddress
18+
19+
/// Bluetooth address type
20+
public var addressType: AddressType
21+
22+
// MARK: - Initialization
23+
24+
public init(
25+
address: BluetoothAddress,
26+
addressType: AddressType = .lowEnergyPublic
27+
) {
28+
self.address = address
29+
self.addressType = addressType
30+
}
31+
}
32+
33+
extension ISOSocketAddress: BluetoothSocketAddress {
34+
35+
@_alwaysEmitIntoClient
36+
public static var protocolID: BluetoothSocketProtocol { .iso }
37+
38+
/// Unsafe pointer closure
39+
public func withUnsafePointer<Result, Error>(
40+
_ body: (UnsafePointer<CInterop.SocketAddress>, UInt32) throws(Error) -> Result
41+
) rethrows -> Result where Error: Swift.Error {
42+
var value = CInterop.ISOSocketAddress()
43+
value.address = address.littleEndian
44+
value.type = addressType.rawValue
45+
return try value.withUnsafePointer(body)
46+
}
47+
48+
public static func withUnsafePointer<Error>(
49+
_ body: (UnsafeMutablePointer<CInterop.SocketAddress>, UInt32) throws(Error) -> ()
50+
) rethrows -> Self where Error: Swift.Error {
51+
var value = CInterop.ISOSocketAddress()
52+
try value.withUnsafeMutablePointer(body)
53+
return Self.init(value)
54+
}
55+
56+
public static func withUnsafePointer(
57+
_ pointer: UnsafeMutablePointer<CInterop.SocketAddress>
58+
) -> Self {
59+
pointer.withMemoryRebound(to: CInterop.ISOSocketAddress.self, capacity: 1) { pointer in
60+
Self.init(pointer.pointee)
61+
}
62+
}
63+
}
64+
65+
internal extension ISOSocketAddress {
66+
67+
@usableFromInline
68+
init(_ bytes: CInterop.ISOSocketAddress) {
69+
self.init(
70+
address: .init(littleEndian: bytes.address),
71+
addressType: AddressType(rawValue: bytes.type) ?? .lowEnergyPublic
72+
)
73+
}
74+
}

0 commit comments

Comments
 (0)