Skip to content
26 changes: 26 additions & 0 deletions Sources/BluetoothLinux/Extensions/Integer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// Integer.swift
// BluetoothLinux
//

internal extension UInt16 {

init(bytes: (UInt8, UInt8)) {
self = unsafeBitCast(bytes, to: UInt16.self)
}

var bytes: (UInt8, UInt8) {
unsafeBitCast(self, to: (UInt8, UInt8).self)
}
}

internal extension UInt32 {

init(bytes: (UInt8, UInt8, UInt8, UInt8)) {
self = unsafeBitCast(bytes, to: UInt32.self)
}

var bytes: (UInt8, UInt8, UInt8, UInt8) {
unsafeBitCast(self, to: (UInt8, UInt8, UInt8, UInt8).self)
}
}
191 changes: 191 additions & 0 deletions Sources/BluetoothLinux/Management/ManagementCommands.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
//
// ManagementCommands.swift
// BluetoothLinux
//

import Foundation
import Bluetooth
import BluetoothHCI

/// Bluetooth Management interface version information.
public struct ManagementVersion: Equatable, Hashable, Sendable {

/// Interface version.
public let version: UInt8

/// Interface revision.
public let revision: UInt16
}

/// Discoverable mode.
public enum ManagementDiscoverableMode: UInt8, Sendable, CaseIterable {

case disabled = 0x00
case general = 0x01
case limited = 0x02
}

/// Advertising mode.
public enum ManagementAdvertisingMode: UInt8, Sendable, CaseIterable {

case disabled = 0x00
case enabled = 0x01

/// Advertise as connectable even when not connectable.
case connectable = 0x02
}

public extension ManagementSocket {

/// Read Management Version Information
func readVersion() async throws -> ManagementVersion {
let response = try await send(.readVersion)
guard response.count >= 3 else {
throw BluetoothHostControllerError.garbageResponse(response)
}
let bytes = Array(response)
return ManagementVersion(
version: bytes[0],
revision: UInt16(littleEndian: UInt16(bytes: (bytes[1], bytes[2])))
)
}

/// Read Controller Index List
func readControllerIndexList() async throws -> [HostController.ID] {
let response = try await send(.readIndexList)
guard response.count >= 2 else {
throw BluetoothHostControllerError.garbageResponse(response)
}
let bytes = Array(response)
let count = Int(UInt16(littleEndian: UInt16(bytes: (bytes[0], bytes[1]))))
guard response.count >= 2 + (count * 2) else {
throw BluetoothHostControllerError.garbageResponse(response)
}
return (0 ..< count).map { index in
let offset = 2 + (index * 2)
return HostController.ID(
rawValue: UInt16(littleEndian: UInt16(bytes: (bytes[offset], bytes[offset + 1])))
)
}
}

/// Read Controller Information
func readControllerInformation(for index: HostController.ID) async throws -> ManagementControllerInformation {
let response = try await send(.readControllerInformation, index: index)
guard let information = ManagementControllerInformation(data: response) else {
throw BluetoothHostControllerError.garbageResponse(response)
}
return information
}

/// Set Powered
@discardableResult
func setPowered(_ isEnabled: Bool, for index: HostController.ID) async throws -> ManagementSettings {
try await setMode(.setPowered, isEnabled, for: index)
}

/// Set Connectable
@discardableResult
func setConnectable(_ isEnabled: Bool, for index: HostController.ID) async throws -> ManagementSettings {
try await setMode(.setConnectable, isEnabled, for: index)
}

/// Set Fast Connectable
@discardableResult
func setFastConnectable(_ isEnabled: Bool, for index: HostController.ID) async throws -> ManagementSettings {
try await setMode(.setFastConnectable, isEnabled, for: index)
}

/// Set Bondable
@discardableResult
func setBondable(_ isEnabled: Bool, for index: HostController.ID) async throws -> ManagementSettings {
try await setMode(.setBondable, isEnabled, for: index)
}

/// Set Link Security
@discardableResult
func setLinkSecurity(_ isEnabled: Bool, for index: HostController.ID) async throws -> ManagementSettings {
try await setMode(.setLinkSecurity, isEnabled, for: index)
}

/// Set Secure Simple Pairing
@discardableResult
func setSecureSimplePairing(_ isEnabled: Bool, for index: HostController.ID) async throws -> ManagementSettings {
try await setMode(.setSecureSimplePairing, isEnabled, for: index)
}

/// Set Low Energy
@discardableResult
func setLowEnergy(_ isEnabled: Bool, for index: HostController.ID) async throws -> ManagementSettings {
try await setMode(.setLowEnergy, isEnabled, for: index)
}

/// Set Discoverable
///
/// - Parameter mode: The discoverable mode to set.
/// - Parameter timeout: Duration in seconds before discoverable mode is disabled again,
/// or `0` to remain discoverable indefinitely.
@discardableResult
func setDiscoverable(
_ mode: ManagementDiscoverableMode,
timeout: UInt16 = 0,
for index: HostController.ID
) async throws -> ManagementSettings {
let timeoutBytes = timeout.littleEndian.bytes
let parameters = Data([mode.rawValue, timeoutBytes.0, timeoutBytes.1])
let response = try await send(.setDiscoverable, index: index, parameters: parameters)
return try Self.settings(from: response)
}

/// Set Advertising
@discardableResult
func setAdvertising(
_ mode: ManagementAdvertisingMode,
for index: HostController.ID
) async throws -> ManagementSettings {
let response = try await send(.setAdvertising, index: index, parameters: Data([mode.rawValue]))
return try Self.settings(from: response)
}

/// Set Local Name
///
/// - Parameter name: The controller name (truncated to fit).
/// - Parameter shortName: The short name used when advertising space is limited (truncated to fit).
func setLocalName(
_ name: String,
shortName: String = "",
for index: HostController.ID
) async throws {
var parameters = Data(capacity: ManagementControllerInformation.maximumNameLength + ManagementControllerInformation.maximumShortNameLength)
parameters.append(Self.fixedLengthString(name, length: ManagementControllerInformation.maximumNameLength))
parameters.append(Self.fixedLengthString(shortName, length: ManagementControllerInformation.maximumShortNameLength))
try await send(.setLocalName, index: index, parameters: parameters)
}

// MARK: - Internal

internal func setMode(
_ opcode: ManagementOpcode,
_ isEnabled: Bool,
for index: HostController.ID
) async throws -> ManagementSettings {
let response = try await send(opcode, index: index, parameters: Data([isEnabled ? 0x01 : 0x00]))
return try Self.settings(from: response)
}

internal static func settings(from response: Data) throws -> ManagementSettings {
guard response.count >= 4 else {
throw BluetoothHostControllerError.garbageResponse(response)
}
let bytes = Array(response)
return ManagementSettings(
rawValue: UInt32(littleEndian: UInt32(bytes: (bytes[0], bytes[1], bytes[2], bytes[3])))
)
}

internal static func fixedLengthString(_ string: String, length: Int) -> Data {
var data = Data(string.utf8.prefix(length - 1))
data.append(contentsOf: repeatElement(0, count: length - data.count))
return data
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//
// ManagementControllerInformation.swift
// BluetoothLinux
//

import Foundation
import Bluetooth

/// Bluetooth Management interface controller information.
///
/// Return parameters of the Read Controller Information command.
public struct ManagementControllerInformation: Equatable, Hashable, Sendable {

/// Length of the encoded structure in bytes.
public static var length: Int { 6 + 1 + 2 + 4 + 4 + 3 + Self.maximumNameLength + Self.maximumShortNameLength }

/// Maximum length of the controller name in bytes, including the null terminator.
public static var maximumNameLength: Int { 249 }

/// Maximum length of the controller short name in bytes, including the null terminator.
public static var maximumShortNameLength: Int { 11 }

/// Controller address.
public let address: BluetoothAddress

/// Bluetooth core specification version.
public let version: UInt8

/// Manufacturer identifier.
public let manufacturer: UInt16

/// Settings the controller supports.
public let supportedSettings: ManagementSettings

/// Settings currently active on the controller.
public let currentSettings: ManagementSettings

/// Class of device.
public let classOfDevice: (UInt8, UInt8, UInt8)

/// Controller name.
public let name: String

/// Controller short name.
public let shortName: String

public static func == (lhs: ManagementControllerInformation, rhs: ManagementControllerInformation) -> Bool {
lhs.address == rhs.address
&& lhs.version == rhs.version
&& lhs.manufacturer == rhs.manufacturer
&& lhs.supportedSettings == rhs.supportedSettings
&& lhs.currentSettings == rhs.currentSettings
&& lhs.classOfDevice == rhs.classOfDevice
&& lhs.name == rhs.name
&& lhs.shortName == rhs.shortName
}

public func hash(into hasher: inout Hasher) {
hasher.combine(address)
hasher.combine(version)
hasher.combine(manufacturer)
hasher.combine(supportedSettings)
hasher.combine(currentSettings)
hasher.combine(classOfDevice.0)
hasher.combine(classOfDevice.1)
hasher.combine(classOfDevice.2)
hasher.combine(name)
hasher.combine(shortName)
}
}

public extension ManagementControllerInformation {

init?(data: Data) {
guard data.count >= Self.length else {
return nil
}
let bytes = Array(data)
self.address = BluetoothAddress(
littleEndian: BluetoothAddress(
bytes: (bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5])
)
)
self.version = bytes[6]
self.manufacturer = UInt16(littleEndian: UInt16(bytes: (bytes[7], bytes[8])))
self.supportedSettings = ManagementSettings(
rawValue: UInt32(littleEndian: UInt32(bytes: (bytes[9], bytes[10], bytes[11], bytes[12])))
)
self.currentSettings = ManagementSettings(
rawValue: UInt32(littleEndian: UInt32(bytes: (bytes[13], bytes[14], bytes[15], bytes[16])))
)
self.classOfDevice = (bytes[17], bytes[18], bytes[19])
let nameBytes = bytes[20 ..< 20 + Self.maximumNameLength]
let shortNameBytes = bytes[20 + Self.maximumNameLength ..< Self.length]
self.name = String(cString: nameBytes)
self.shortName = String(cString: shortNameBytes)
}
}

internal extension String {

/// Initialize from a fixed-size null-terminated C string buffer.
init<C: Collection>(cString bytes: C) where C.Element == UInt8 {
let characters = bytes.prefix(while: { $0 != 0 })
self.init(decoding: characters, as: UTF8.self)
}
}
Loading
Loading