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
93 changes: 91 additions & 2 deletions Sources/BluetoothLinux/HIDP/HIDP.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,99 @@
//
// HIDP.swift
//
// BluetoothLinux
//
// Created by Alsey Coleman Miller on 16/10/21.
// Human Interface Device Protocol control socket.
//

import Foundation
import Bluetooth
import SystemPackage
import Socket

/// HIDP control socket.
///
/// Manages kernel HID protocol sessions, which bridge connected L2CAP
/// control (PSM 17) and interrupt (PSM 19) sockets into kernel input devices.
public struct HIDPSocket: Sendable {

// MARK: - Properties

@usableFromInline
internal let fileDescriptor: SocketDescriptor

// MARK: - Initialization

/// Open a HIDP control socket.
public init() throws(Errno) {
self.fileDescriptor = try .bluetooth(.hidp, flags: [.closeOnExec])
}

// MARK: - Methods

/// Close the control socket.
///
/// Established sessions are not affected.
public func close() {
try? fileDescriptor.close()
}

/// Bridge connected L2CAP control and interrupt sockets into a kernel input device.
///
/// The L2CAP sockets must be connected to the remote device on the HID control (PSM 17)
/// and HID interrupt (PSM 19) channels. The report descriptor and device identity are
/// typically read from the device's service record.
public func addConnection(
control: L2CAPSocket,
interrupt: L2CAPSocket,
flags: HIDPConnectionFlag = [],
parser: UInt16 = 0x0100,
country: UInt8 = 0,
subclass: UInt8 = 0,
vendor: UInt16 = 0,
product: UInt16 = 0,
version: UInt16 = 0,
name: String = "",
reportDescriptor: Data = Data(),
idleTimeout: UInt32 = 0
) throws {
try fileDescriptor.hidpAddConnection(
controlSocket: control.fileDescriptor,
interruptSocket: interrupt.fileDescriptor,
flags: flags,
parser: parser,
country: country,
subclass: subclass,
vendor: vendor,
product: product,
version: version,
name: name,
reportDescriptor: reportDescriptor,
idleTimeout: idleTimeout
)
}

/// Destroy the session (and input device) for the specified remote device.
public func removeConnection(
destination: BluetoothAddress,
flags: HIDPConnectionFlag = []
) throws {
try fileDescriptor.hidpRemoveConnection(
destination: destination,
flags: flags
)
}

/// List the active sessions.
public func connections(
limit: Int = HIDPIO.GetConnectionList.maxLimit
) throws -> [HIDPConnection] {
try fileDescriptor.hidpConnectionList(limit: limit)
}

/// Read information for the session with the specified remote device.
public func connectionInformation(
for destination: BluetoothAddress
) throws -> HIDPConnection {
try fileDescriptor.hidpConnectionInformation(for: destination)
}
}
71 changes: 71 additions & 0 deletions Sources/BluetoothLinux/HIDP/HIDPConnection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//
// HIDPConnection.swift
// BluetoothLinux
//

import Foundation
import Bluetooth
import SystemPackage
import Socket

/// HIDP connection information.
public struct HIDPConnection: Equatable, Hashable, Sendable {

/// Address of the remote device.
public let address: BluetoothAddress

/// Connection flags.
public let flags: HIDPConnectionFlag

/// Connection state.
public let state: HIDPConnectionState

/// Vendor identifier.
public let vendor: UInt16

/// Product identifier.
public let product: UInt16

/// Version number.
public let version: UInt16

/// Name of the device.
public let name: String
}

internal extension HIDPConnection {

init(_ bytes: CInterop.HIDPConnectionInformation) {
self.address = BluetoothAddress(littleEndian: bytes.address)
self.flags = HIDPConnectionFlag(rawValue: bytes.flags)
self.state = HIDPConnectionState(rawValue: bytes.state) ?? .unknown
self.vendor = bytes.vendor
self.product = bytes.product
self.version = bytes.version
self.name = String(hidpDeviceName: bytes.name)
}
}

internal extension String {

/// Decode from a fixed-size null-terminated device name buffer.
@usableFromInline
init(hidpDeviceName: CInterop.HIDPDeviceName) {
self = withUnsafeBytes(of: hidpDeviceName) { buffer in
String(decoding: buffer.prefix(while: { $0 != 0 }), as: UTF8.self)
}
}

/// Encode as a fixed-size null-terminated device name buffer.
@usableFromInline
var hidpDeviceName: CInterop.HIDPDeviceName {
var name: CInterop.HIDPDeviceName = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
let utf8 = Array(self.utf8.prefix(127))
withUnsafeMutableBytes(of: &name) { buffer in
for (index, byte) in utf8.enumerated() {
buffer[index] = byte
}
}
return name
}
}
24 changes: 24 additions & 0 deletions Sources/BluetoothLinux/HIDP/HIDPConnectionFlag.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// HIDPConnectionFlag.swift
// BluetoothLinux
//

/// HIDP connection flags.
@frozen
public struct HIDPConnectionFlag: OptionSet, Equatable, Hashable, Sendable {

public let rawValue: UInt32

public init(rawValue: UInt32) {
self.rawValue = rawValue
}
}

public extension HIDPConnectionFlag {

/// Delete the session when the device sends a virtual cable unplug.
static var virtualCableUnplug: HIDPConnectionFlag { HIDPConnectionFlag(rawValue: 1 << 0) }

/// Use the boot protocol instead of the report protocol.
static var bootProtocolMode: HIDPConnectionFlag { HIDPConnectionFlag(rawValue: 1 << 1) }
}
19 changes: 19 additions & 0 deletions Sources/BluetoothLinux/HIDP/HIDPConnectionState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// HIDPConnectionState.swift
// BluetoothLinux
//

/// HIDP connection state.
public enum HIDPConnectionState: UInt16, CaseIterable, Sendable {

case unknown = 0x00
case connected = 0x01
case open = 0x02
case bound = 0x03
case listening = 0x04
case connecting = 0x05
case connecting2 = 0x06
case config = 0x07
case disconnecting = 0x08
case closed = 0x09
}
136 changes: 136 additions & 0 deletions Sources/BluetoothLinux/HIDP/IOCTL/HIDPAddConnection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
//
// HIDPAddConnection.swift
// BluetoothLinux
//

import Foundation
import Bluetooth
import SystemPackage
import Socket

public extension HIDPIO {

/// HIDP Add Connection
///
/// Bridges connected L2CAP control (PSM 17) and interrupt (PSM 19) sockets
/// into a kernel input device.
struct AddConnection: IOControlValue {

@_alwaysEmitIntoClient
public static var id: HIDPIO { .addConnection }

@usableFromInline
internal private(set) var bytes: CInterop.HIDPConnectionAddRequest

/// HID report descriptor.
public let reportDescriptor: Data

public init(
controlSocket: SocketDescriptor,
interruptSocket: SocketDescriptor,
flags: HIDPConnectionFlag = [],
parser: UInt16 = 0x0100,
country: UInt8 = 0,
subclass: UInt8 = 0,
vendor: UInt16 = 0,
product: UInt16 = 0,
version: UInt16 = 0,
name: String = "",
reportDescriptor: Data = Data(),
idleTimeout: UInt32 = 0
) {
precondition(reportDescriptor.count <= UInt16.max)
self.reportDescriptor = reportDescriptor
self.bytes = CInterop.HIDPConnectionAddRequest(
controlSocket: controlSocket.rawValue,
interruptSocket: interruptSocket.rawValue,
parser: parser,
reportDescriptorSize: UInt16(reportDescriptor.count),
reportDescriptor: nil,
country: country,
subclass: subclass,
vendor: vendor,
product: product,
version: version,
flags: flags.rawValue,
idleTimeout: idleTimeout,
name: name.hidpDeviceName
)
}

public mutating func withUnsafeMutablePointer<Result>(_ body: (UnsafeMutableRawPointer) throws -> (Result)) rethrows -> Result {
// keep the report descriptor buffer alive for the duration of the call
var descriptor = [UInt8](reportDescriptor)
var bytes = self.bytes
let result: Result = try descriptor.withUnsafeMutableBufferPointer { buffer in
bytes.reportDescriptor = buffer.baseAddress
return try Swift.withUnsafeMutableBytes(of: &bytes) { requestBuffer in
try body(requestBuffer.baseAddress!)
}
}
bytes.reportDescriptor = nil
self.bytes = bytes
return result
}
}
}

public extension HIDPIO.AddConnection {

@_alwaysEmitIntoClient
var controlSocket: SocketDescriptor {
return .init(rawValue: bytes.controlSocket)
}

@_alwaysEmitIntoClient
var interruptSocket: SocketDescriptor {
return .init(rawValue: bytes.interruptSocket)
}

@_alwaysEmitIntoClient
var flags: HIDPConnectionFlag {
return .init(rawValue: bytes.flags)
}

/// Name of the device.
var name: String {
return String(hidpDeviceName: bytes.name)
}
}

// MARK: - File Descriptor

internal extension SocketDescriptor {

@usableFromInline
func hidpAddConnection(
controlSocket: SocketDescriptor,
interruptSocket: SocketDescriptor,
flags: HIDPConnectionFlag = [],
parser: UInt16 = 0x0100,
country: UInt8 = 0,
subclass: UInt8 = 0,
vendor: UInt16 = 0,
product: UInt16 = 0,
version: UInt16 = 0,
name: String = "",
reportDescriptor: Data = Data(),
idleTimeout: UInt32 = 0
) throws {
var request = HIDPIO.AddConnection(
controlSocket: controlSocket,
interruptSocket: interruptSocket,
flags: flags,
parser: parser,
country: country,
subclass: subclass,
vendor: vendor,
product: product,
version: version,
name: name,
reportDescriptor: reportDescriptor,
idleTimeout: idleTimeout
)
try inputOutput(&request)
}
}
Loading
Loading