Skip to content

Commit 706cd38

Browse files
authored
Merge pull request #61 from PureSwift/feature/hidp
Add HIDP support
2 parents d339675 + 9153b59 commit 706cd38

10 files changed

Lines changed: 876 additions & 2 deletions
Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,99 @@
11
//
22
// HIDP.swift
3-
//
3+
// BluetoothLinux
44
//
5-
// Created by Alsey Coleman Miller on 16/10/21.
5+
// Human Interface Device Protocol control socket.
66
//
77

8+
import Foundation
9+
import Bluetooth
810
import SystemPackage
11+
import Socket
912

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

19+
// MARK: - Properties
20+
21+
@usableFromInline
22+
internal let fileDescriptor: SocketDescriptor
23+
24+
// MARK: - Initialization
25+
26+
/// Open a HIDP control socket.
27+
public init() throws(Errno) {
28+
self.fileDescriptor = try .bluetooth(.hidp, flags: [.closeOnExec])
29+
}
30+
31+
// MARK: - Methods
32+
33+
/// Close the control socket.
34+
///
35+
/// Established sessions are not affected.
36+
public func close() {
37+
try? fileDescriptor.close()
38+
}
39+
40+
/// Bridge connected L2CAP control and interrupt sockets into a kernel input device.
41+
///
42+
/// The L2CAP sockets must be connected to the remote device on the HID control (PSM 17)
43+
/// and HID interrupt (PSM 19) channels. The report descriptor and device identity are
44+
/// typically read from the device's service record.
45+
public func addConnection(
46+
control: L2CAPSocket,
47+
interrupt: L2CAPSocket,
48+
flags: HIDPConnectionFlag = [],
49+
parser: UInt16 = 0x0100,
50+
country: UInt8 = 0,
51+
subclass: UInt8 = 0,
52+
vendor: UInt16 = 0,
53+
product: UInt16 = 0,
54+
version: UInt16 = 0,
55+
name: String = "",
56+
reportDescriptor: Data = Data(),
57+
idleTimeout: UInt32 = 0
58+
) throws {
59+
try fileDescriptor.hidpAddConnection(
60+
controlSocket: control.fileDescriptor,
61+
interruptSocket: interrupt.fileDescriptor,
62+
flags: flags,
63+
parser: parser,
64+
country: country,
65+
subclass: subclass,
66+
vendor: vendor,
67+
product: product,
68+
version: version,
69+
name: name,
70+
reportDescriptor: reportDescriptor,
71+
idleTimeout: idleTimeout
72+
)
73+
}
74+
75+
/// Destroy the session (and input device) for the specified remote device.
76+
public func removeConnection(
77+
destination: BluetoothAddress,
78+
flags: HIDPConnectionFlag = []
79+
) throws {
80+
try fileDescriptor.hidpRemoveConnection(
81+
destination: destination,
82+
flags: flags
83+
)
84+
}
85+
86+
/// List the active sessions.
87+
public func connections(
88+
limit: Int = HIDPIO.GetConnectionList.maxLimit
89+
) throws -> [HIDPConnection] {
90+
try fileDescriptor.hidpConnectionList(limit: limit)
91+
}
92+
93+
/// Read information for the session with the specified remote device.
94+
public func connectionInformation(
95+
for destination: BluetoothAddress
96+
) throws -> HIDPConnection {
97+
try fileDescriptor.hidpConnectionInformation(for: destination)
98+
}
99+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//
2+
// HIDPConnection.swift
3+
// BluetoothLinux
4+
//
5+
6+
import Foundation
7+
import Bluetooth
8+
import SystemPackage
9+
import Socket
10+
11+
/// HIDP connection information.
12+
public struct HIDPConnection: Equatable, Hashable, Sendable {
13+
14+
/// Address of the remote device.
15+
public let address: BluetoothAddress
16+
17+
/// Connection flags.
18+
public let flags: HIDPConnectionFlag
19+
20+
/// Connection state.
21+
public let state: HIDPConnectionState
22+
23+
/// Vendor identifier.
24+
public let vendor: UInt16
25+
26+
/// Product identifier.
27+
public let product: UInt16
28+
29+
/// Version number.
30+
public let version: UInt16
31+
32+
/// Name of the device.
33+
public let name: String
34+
}
35+
36+
internal extension HIDPConnection {
37+
38+
init(_ bytes: CInterop.HIDPConnectionInformation) {
39+
self.address = BluetoothAddress(littleEndian: bytes.address)
40+
self.flags = HIDPConnectionFlag(rawValue: bytes.flags)
41+
self.state = HIDPConnectionState(rawValue: bytes.state) ?? .unknown
42+
self.vendor = bytes.vendor
43+
self.product = bytes.product
44+
self.version = bytes.version
45+
self.name = String(hidpDeviceName: bytes.name)
46+
}
47+
}
48+
49+
internal extension String {
50+
51+
/// Decode from a fixed-size null-terminated device name buffer.
52+
@usableFromInline
53+
init(hidpDeviceName: CInterop.HIDPDeviceName) {
54+
self = withUnsafeBytes(of: hidpDeviceName) { buffer in
55+
String(decoding: buffer.prefix(while: { $0 != 0 }), as: UTF8.self)
56+
}
57+
}
58+
59+
/// Encode as a fixed-size null-terminated device name buffer.
60+
@usableFromInline
61+
var hidpDeviceName: CInterop.HIDPDeviceName {
62+
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)
63+
let utf8 = Array(self.utf8.prefix(127))
64+
withUnsafeMutableBytes(of: &name) { buffer in
65+
for (index, byte) in utf8.enumerated() {
66+
buffer[index] = byte
67+
}
68+
}
69+
return name
70+
}
71+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//
2+
// HIDPConnectionFlag.swift
3+
// BluetoothLinux
4+
//
5+
6+
/// HIDP connection flags.
7+
@frozen
8+
public struct HIDPConnectionFlag: OptionSet, Equatable, Hashable, Sendable {
9+
10+
public let rawValue: UInt32
11+
12+
public init(rawValue: UInt32) {
13+
self.rawValue = rawValue
14+
}
15+
}
16+
17+
public extension HIDPConnectionFlag {
18+
19+
/// Delete the session when the device sends a virtual cable unplug.
20+
static var virtualCableUnplug: HIDPConnectionFlag { HIDPConnectionFlag(rawValue: 1 << 0) }
21+
22+
/// Use the boot protocol instead of the report protocol.
23+
static var bootProtocolMode: HIDPConnectionFlag { HIDPConnectionFlag(rawValue: 1 << 1) }
24+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//
2+
// HIDPConnectionState.swift
3+
// BluetoothLinux
4+
//
5+
6+
/// HIDP connection state.
7+
public enum HIDPConnectionState: UInt16, CaseIterable, Sendable {
8+
9+
case unknown = 0x00
10+
case connected = 0x01
11+
case open = 0x02
12+
case bound = 0x03
13+
case listening = 0x04
14+
case connecting = 0x05
15+
case connecting2 = 0x06
16+
case config = 0x07
17+
case disconnecting = 0x08
18+
case closed = 0x09
19+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
//
2+
// HIDPAddConnection.swift
3+
// BluetoothLinux
4+
//
5+
6+
import Foundation
7+
import Bluetooth
8+
import SystemPackage
9+
import Socket
10+
11+
public extension HIDPIO {
12+
13+
/// HIDP Add Connection
14+
///
15+
/// Bridges connected L2CAP control (PSM 17) and interrupt (PSM 19) sockets
16+
/// into a kernel input device.
17+
struct AddConnection: IOControlValue {
18+
19+
@_alwaysEmitIntoClient
20+
public static var id: HIDPIO { .addConnection }
21+
22+
@usableFromInline
23+
internal private(set) var bytes: CInterop.HIDPConnectionAddRequest
24+
25+
/// HID report descriptor.
26+
public let reportDescriptor: Data
27+
28+
public init(
29+
controlSocket: SocketDescriptor,
30+
interruptSocket: SocketDescriptor,
31+
flags: HIDPConnectionFlag = [],
32+
parser: UInt16 = 0x0100,
33+
country: UInt8 = 0,
34+
subclass: UInt8 = 0,
35+
vendor: UInt16 = 0,
36+
product: UInt16 = 0,
37+
version: UInt16 = 0,
38+
name: String = "",
39+
reportDescriptor: Data = Data(),
40+
idleTimeout: UInt32 = 0
41+
) {
42+
precondition(reportDescriptor.count <= UInt16.max)
43+
self.reportDescriptor = reportDescriptor
44+
self.bytes = CInterop.HIDPConnectionAddRequest(
45+
controlSocket: controlSocket.rawValue,
46+
interruptSocket: interruptSocket.rawValue,
47+
parser: parser,
48+
reportDescriptorSize: UInt16(reportDescriptor.count),
49+
reportDescriptor: nil,
50+
country: country,
51+
subclass: subclass,
52+
vendor: vendor,
53+
product: product,
54+
version: version,
55+
flags: flags.rawValue,
56+
idleTimeout: idleTimeout,
57+
name: name.hidpDeviceName
58+
)
59+
}
60+
61+
public mutating func withUnsafeMutablePointer<Result>(_ body: (UnsafeMutableRawPointer) throws -> (Result)) rethrows -> Result {
62+
// keep the report descriptor buffer alive for the duration of the call
63+
var descriptor = [UInt8](reportDescriptor)
64+
var bytes = self.bytes
65+
let result: Result = try descriptor.withUnsafeMutableBufferPointer { buffer in
66+
bytes.reportDescriptor = buffer.baseAddress
67+
return try Swift.withUnsafeMutableBytes(of: &bytes) { requestBuffer in
68+
try body(requestBuffer.baseAddress!)
69+
}
70+
}
71+
bytes.reportDescriptor = nil
72+
self.bytes = bytes
73+
return result
74+
}
75+
}
76+
}
77+
78+
public extension HIDPIO.AddConnection {
79+
80+
@_alwaysEmitIntoClient
81+
var controlSocket: SocketDescriptor {
82+
return .init(rawValue: bytes.controlSocket)
83+
}
84+
85+
@_alwaysEmitIntoClient
86+
var interruptSocket: SocketDescriptor {
87+
return .init(rawValue: bytes.interruptSocket)
88+
}
89+
90+
@_alwaysEmitIntoClient
91+
var flags: HIDPConnectionFlag {
92+
return .init(rawValue: bytes.flags)
93+
}
94+
95+
/// Name of the device.
96+
var name: String {
97+
return String(hidpDeviceName: bytes.name)
98+
}
99+
}
100+
101+
// MARK: - File Descriptor
102+
103+
internal extension SocketDescriptor {
104+
105+
@usableFromInline
106+
func hidpAddConnection(
107+
controlSocket: SocketDescriptor,
108+
interruptSocket: SocketDescriptor,
109+
flags: HIDPConnectionFlag = [],
110+
parser: UInt16 = 0x0100,
111+
country: UInt8 = 0,
112+
subclass: UInt8 = 0,
113+
vendor: UInt16 = 0,
114+
product: UInt16 = 0,
115+
version: UInt16 = 0,
116+
name: String = "",
117+
reportDescriptor: Data = Data(),
118+
idleTimeout: UInt32 = 0
119+
) throws {
120+
var request = HIDPIO.AddConnection(
121+
controlSocket: controlSocket,
122+
interruptSocket: interruptSocket,
123+
flags: flags,
124+
parser: parser,
125+
country: country,
126+
subclass: subclass,
127+
vendor: vendor,
128+
product: product,
129+
version: version,
130+
name: name,
131+
reportDescriptor: reportDescriptor,
132+
idleTimeout: idleTimeout
133+
)
134+
try inputOutput(&request)
135+
}
136+
}

0 commit comments

Comments
 (0)