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
2 changes: 1 addition & 1 deletion Sources/BluetoothLinux/HCI/HCIPacketType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import BluetoothHCI

/// HCI Packet types
public enum HCIPacketType: UInt8 {
public enum HCIPacketType: UInt8, Sendable {

case command = 0x01
case acl = 0x02
Expand Down
193 changes: 193 additions & 0 deletions Sources/BluetoothLinux/HCI/HCIUserChannel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
//
// HCIUserChannel.swift
// BluetoothLinux
//

import Foundation
import Bluetooth
import BluetoothHCI
import SystemPackage
import Socket

/// HCI User Channel
///
/// Provides exclusive access to a Bluetooth controller (`HCI_CHANNEL_USER`),
/// bypassing the kernel's host stack. While the channel is open, all HCI traffic
/// (commands, events, ACL and SCO data) flows through this socket and the kernel
/// does not process packets itself.
///
/// The controller must be powered off before the channel can be opened
/// (use ``open(device:)`` to power it off automatically), and the
/// `CAP_NET_ADMIN` capability (root) is required.
public actor HCIUserChannel {

// MARK: - Properties

/// Maximum size of an HCI packet in bytes, including the packet type prefix.
public static var maximumPacketLength: Int { 1 + 4 + Int(UInt16.max) }

/// Controller identifier.
public let id: HostController.ID

@usableFromInline
internal let socket: Socket

// MARK: - Initialization

deinit {
let socket = self.socket
Task(priority: .high) {
await socket.close()
}
}

/// Open a user channel for the specified controller.
///
/// The controller must already be powered off.
public init(id: HostController.ID) async throws {
let address = HCISocketAddress(
device: id,
channel: .user
)
let fileDescriptor = try SocketDescriptor.hci(address, flags: [.closeOnExec, .nonBlocking])
self.id = id
self.socket = await Socket(fileDescriptor: fileDescriptor)
}

/// Power off the specified controller and open a user channel for it.
public static func open(device id: HostController.ID) async throws -> HCIUserChannel {
try HostController.disable(device: id)
return try await HCIUserChannel(id: id)
}

// MARK: - Methods

/// Send a raw HCI packet.
public func send(_ packet: Packet) async throws {
var data = Data(capacity: 1 + packet.data.count)
data.append(packet.type.rawValue)
data.append(packet.data)
_ = try await socket.write(data)
}

/// Send an HCI command with parameters, without waiting for a response.
public func send<Command: HCICommand & Sendable>(
_ command: Command,
parameter: Data = Data()
) async throws {
try await socket.sendCommand(command, parameter: parameter)
}

/// Receive the next HCI packet.
public func receive() async throws -> Packet {
let data = try await socket.read(Self.maximumPacketLength)
guard let packet = Packet(data: data) else {
throw BluetoothHostControllerError.garbageResponse(data)
}
return packet
}

/// Send an HCI command and wait for its response, ignoring unrelated packets.
///
/// - Returns: The return parameters of the Command Complete event,
/// or empty data if the command was acknowledged with a successful Command Status event.
///
/// - Throws: ``HCIError`` if the command fails.
@discardableResult
public func request<Command: HCICommand & Sendable>(
_ command: Command,
parameter: Data = Data()
) async throws -> Data {
try await socket.sendCommand(command, parameter: parameter)
while true {
let packet = try await receive()
guard let response = CommandResponse(packet), response.opcode == command.opcode else {
continue
}
if let status = response.status, status != 0 {
throw HCIError(rawValue: status) ?? BluetoothHostControllerError.garbageResponse(packet.data)
}
return response.parameters
}
}
}

// MARK: - Supporting Types

public extension HCIUserChannel {

/// HCI packet.
struct Packet: Equatable, Hashable, Sendable {

/// Packet type.
public let type: HCIPacketType

/// Packet payload, without the packet type prefix.
public let data: Data

public init(type: HCIPacketType, data: Data) {
self.type = type
self.data = data
}
}
}

public extension HCIUserChannel.Packet {

/// Decode a packet from its wire representation (packet type prefix followed by payload).
init?(data: Data) {
guard data.isEmpty == false,
let type = HCIPacketType(rawValue: data[data.startIndex]) else {
return nil
}
self.init(type: type, data: Data(data.dropFirst()))
}
}

internal extension HCIUserChannel {

/// Parsed Command Complete or Command Status event.
struct CommandResponse {

/// Opcode of the command this event responds to.
let opcode: UInt16

/// Command status, if this is a Command Status event.
let status: UInt8?

/// Return parameters, if this is a Command Complete event.
let parameters: Data

init?(_ packet: HCIUserChannel.Packet) {
guard packet.type == .event,
packet.data.count >= HCIEventHeader.length,
let header = HCIEventHeader(data: Data(packet.data.prefix(HCIEventHeader.length))) else {
return nil
}
let parameters = Data(packet.data.dropFirst(HCIEventHeader.length))
guard parameters.count == Int(header.parameterLength) else {
return nil
}
switch header.event {
case .commandComplete:
// number of packets, opcode, return parameters
guard parameters.count >= 3 else {
return nil
}
self.opcode = UInt16(littleEndian: UInt16(bytes: (parameters[1], parameters[2])))
self.status = nil
self.parameters = Data(parameters.dropFirst(3))
case .commandStatus:
// status, number of packets, opcode
guard parameters.count >= 4 else {
return nil
}
self.opcode = UInt16(littleEndian: UInt16(bytes: (parameters[2], parameters[3])))
self.status = parameters[0]
self.parameters = Data()
default:
return nil
}
}
}
}
90 changes: 90 additions & 0 deletions Tests/BluetoothLinuxTests/UserChannelTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//
// UserChannelTests.swift
// BluetoothLinux
//

import Foundation
import XCTest
import Bluetooth
import BluetoothHCI
@testable import BluetoothLinux

final class UserChannelTests: XCTestCase {

func testPacket() {
// HCI Reset command packet
let data = Data([0x01, 0x03, 0x0C, 0x00])
guard let packet = HCIUserChannel.Packet(data: data) else {
XCTFail("Could not parse packet")
return
}
XCTAssertEqual(packet.type, .command)
XCTAssertEqual(packet.data, Data([0x03, 0x0C, 0x00]))
}

func testPacketInvalid() {
XCTAssertNil(HCIUserChannel.Packet(data: Data()))
XCTAssertNil(HCIUserChannel.Packet(data: Data([0x05])))
}

func testCommandCompleteResponse() {
// Command Complete for HCI Reset (0x0C03), status success
let packet = HCIUserChannel.Packet(
type: .event,
data: Data([
0x0E, // Command Complete
0x04, // parameter length
0x01, // number of packets
0x03, 0x0C, // opcode
0x00 // status
])
)
guard let response = HCIUserChannel.CommandResponse(packet) else {
XCTFail("Could not parse response")
return
}
XCTAssertEqual(response.opcode, 0x0C03)
XCTAssertNil(response.status)
XCTAssertEqual(response.parameters, Data([0x00]))
}

func testCommandStatusResponse() {
// Command Status for LE Create Connection (0x200D), command disallowed
let packet = HCIUserChannel.Packet(
type: .event,
data: Data([
0x0F, // Command Status
0x04, // parameter length
0x0C, // status (command disallowed)
0x01, // number of packets
0x0D, 0x20 // opcode
])
)
guard let response = HCIUserChannel.CommandResponse(packet) else {
XCTFail("Could not parse response")
return
}
XCTAssertEqual(response.opcode, 0x200D)
XCTAssertEqual(response.status, 0x0C)
XCTAssertEqual(HCIError(rawValue: 0x0C), .commandDisallowed)
XCTAssertEqual(response.parameters, Data())
}

func testResponseIgnoresOtherPackets() {
// ACL data is not a command response
let acl = HCIUserChannel.Packet(type: .acl, data: Data([0x01, 0x00, 0x02, 0x00, 0xAA, 0xBB]))
XCTAssertNil(HCIUserChannel.CommandResponse(acl))
// LE Meta event is not a command response
let meta = HCIUserChannel.Packet(type: .event, data: Data([0x3E, 0x01, 0x00]))
XCTAssertNil(HCIUserChannel.CommandResponse(meta))
}

func testResponseInvalidLength() {
// header declares more parameters than present
let truncated = HCIUserChannel.Packet(type: .event, data: Data([0x0E, 0x0A, 0x01]))
XCTAssertNil(HCIUserChannel.CommandResponse(truncated))
// command complete too short for opcode
let short = HCIUserChannel.Packet(type: .event, data: Data([0x0E, 0x01, 0x01]))
XCTAssertNil(HCIUserChannel.CommandResponse(short))
}
}
Loading