macOS 上使用 Swift 和 IOKit 开发 HID 设备通信应用
目录
在现代计算机系统中,人机接口设备(HID)扮演着至关重要的角色,为用户提供与计算机交互的途径。本文将指导您如何使用 Swift 编程语言和 macOS 的 IOKit 框架来开发一个可以与 HID 设备进行通信的应用程序。无论您是想开发自定义键盘、游戏控制器还是其他 USB 设备的驱动程序,这篇教程都将为您提供坚实的基础。
凌顺实验室(lingshunlab.com)将分享逐步介绍如何设置项目、获取必要的权限、列出系统中的 HID 设备、打开特定设备、读取和发送 HID 报告,以及正确关闭设备。通过实际的代码示例和详细的解释,您将学会如何:
- 创建 macOS 应用程序项目
- 配置 USB 设备访问权限
- 使用 IOKit 枚举 HID 设备
- 打开和关闭 HID 设备
- 与 HID 设备进行数据交换
这个教程适合于有 Swift 基础的 macOS 开发者,特别是那些对底层硬件通信感兴趣的开发者。通过学习本教程,您将能够开发出能够与各种 HID 设备无缝交互的强大应用程序。
关键词:macOS 开发, Swift, IOKit, HID 设备, USB 通信, 硬件接口, 设备驱动, 人机接口
创建项目
新建一个项目,这里名称为「easyHID」
添加APP访问USB的权限
要让App可以访问到USB,我们需要添加USB的权限,不然的话是无法打开任何的串口或HID设备。
通过以下方法,即可为App添加USB的访问权限:
编写程序
添加AppDelegate
创建AppDelegate文件,并且会使用IOKit的相关库,代码如下:
import SwiftUI
import IOKit
import IOKit.usb
import IOKit.hid
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
print("Application started")
}
}
在easyHIDApp的代码中添加以下代码:
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
修改成如下图所示即可:
列出所有HID设备
创建HID设备信息的结构体
struct HIDDeviceInfo {
let Transport: String // 设备的传输方式(如USB、Bluetooth等)
let VendorID: Int // 设备厂商ID
let ProductID: Int // 设备产品ID
let Product: String // 设备产品名称
let PrimaryUsagePage: Int // 设备主要用途页
let PrimaryUsage: Int // 设备主要用途
let ReportInterval: Int // 设备报告间隔
}
创建HID的类
class HIDManager {
// 单例模式
static let shared = HIDManager()
var manager: IOHIDManager!
@Published var hidDevices: [HIDDeviceInfo] // 可观察的设备列表
// 私有初始化方法,确保只能通过shared访问
private init() {
self.hidDevices = []
}
// 列出所有HID设备
func listHIDDevices() {
self.hidDevices = []
// 创建IOHIDManager实例
manager = IOHIDManagerCreate(kCFAllocatorDefault, IOOptionBits(kIOHIDOptionsTypeNone))
// 打开HIDManager
let result = IOHIDManagerOpen(manager, IOOptionBits(kIOHIDOptionsTypeNone))
print(result)
if result != kIOReturnSuccess {
print("Failed to open HID Manager")
}
// 设置设备匹配条件为nil,匹配所有HID设备
IOHIDManagerSetDeviceMatching(manager, nil)
// 获取所有匹配的设备
if let deviceSet = IOHIDManagerCopyDevices(manager) as? Set<IOHIDDevice> {
for device in deviceSet {
if let deviceInfo = getDeviceInfo(device) {
// 把信息添加到hidDevices
hidDevices.append(deviceInfo)
}
}
}
}
// 从IOHIDDevice获取设备信息
private func getDeviceInfo(_ device: IOHIDDevice) -> HIDDeviceInfo? {
// 尝试获取设备的各种属性
guard let transport = IOHIDDeviceGetProperty(device, kIOHIDTransportKey as CFString) as? String,
let vendorID = IOHIDDeviceGetProperty(device, kIOHIDVendorIDKey as CFString) as? Int,
let productID = IOHIDDeviceGetProperty(device, kIOHIDProductIDKey as CFString) as? Int,
let product = IOHIDDeviceGetProperty(device, kIOHIDProductKey as CFString) as? String,
let primaryUsagePage = IOHIDDeviceGetProperty(device, kIOHIDPrimaryUsagePageKey as CFString) as? Int,
let primaryUsage = IOHIDDeviceGetProperty(device, kIOHIDPrimaryUsageKey as CFString) as? Int,
let reportInterval = IOHIDDeviceGetProperty(device, kIOHIDReportIntervalKey as CFString) as? Int else {
return nil // 如果任何必要属性缺失,返回nil
}
// 创建并返回HIDDeviceInfo实例
return HIDDeviceInfo(
Transport: transport,
VendorID: vendorID,
ProductID: productID,
Product: product,
PrimaryUsagePage: primaryUsagePage,
PrimaryUsage: primaryUsage,
ReportInterval: reportInterval
)
}
}
然后把AppDelegate,改成如下:
class AppDelegate: NSObject, NSApplicationDelegate {
let hid = HIDManager.shared
func applicationDidFinishLaunching(_ aNotification: Notification) {
print("Application started")
// 列出所有HID设备
hid.listHIDDevices()
// 打印每个设备的基本信息
for hid in hid.hidDevices {
print(hid.Product)
print(hid.VendorID)
print(hid.ProductID)
}
}
}
可以看到输出如下信息(这是我当前macbook上所有的HID设备信息):
打开指定HID设备
添加isOpen和device属性:
class HIDManager {
// 单例模式
static let shared = HIDManager()
var manager: IOHIDManager!
@Published var device: IOHIDDevice? // 当前打开的HID设备
@Published var isOpen: Bool? // 设备是否打开
@Published var hidDevices: [HIDDeviceInfo] // 所有发现的HID设备列表
...
}
然后添加一个openHID的函数:
// 打开指定的HID设备
func openHID(vid: Int, pid: Int) {
// 创建HID管理器
manager = IOHIDManagerCreate(kCFAllocatorDefault, IOOptionBits(kIOHIDOptionsTypeNone))
// 设置设备匹配条件
let deviceMatching: [String: Any] = [
kIOHIDVendorIDKey: vid,
kIOHIDProductIDKey: pid
]
IOHIDManagerSetDeviceMatching(manager, deviceMatching as CFDictionary)
// 打开 HID Manager
let result = IOHIDManagerOpen(manager, IOOptionBits(kIOHIDOptionsTypeNone))
if result != kIOReturnSuccess {
print("Failed to open HID Manager")
return
}
// 获取匹配的设备
if let deviceSet = IOHIDManagerCopyDevices(manager) as? Set<IOHIDDevice>, let matchedDevice = deviceSet.first {
// 尝试打开设备
let openResult = IOHIDDeviceOpen(matchedDevice, IOOptionBits(kIOHIDOptionsTypeNone))
if openResult == kIOReturnSuccess {
self.device = matchedDevice
self.isOpen = true
} else {
self.isOpen = false
}
} else {
self.isOpen = nil
}
}
关闭HID设备
然后添加一个closeHID的函数:
// 关闭HID设备
func closeHID() {
// 关闭 HID Device
if let device = self.device {
IOHIDDeviceClose(device, IOOptionBits(kIOHIDOptionsTypeNone))
}
// 关闭 HID Manager
IOHIDManagerClose(manager, IOOptionBits(kIOHIDOptionsTypeNone))
print("HID Manager closed")
self.isOpen = false
}
发送数据到HID设备
然后添加一个sendHIDReport的函数:
// 向设备发送 HID 报告
func sendHIDReport(report: [UInt8]) {
guard let device = self.device else { return }
var report = report
// 向设备发送输出报告
let result = IOHIDDeviceSetReport(device, kIOHIDReportTypeOutput, CFIndex(0), &report, report.count)
if result == kIOReturnSuccess {
print("HID Report sent: \(report)")
} else {
print("Failed to send HID Report")
}
}
读取HID设备的数据
然后添加一个readHIDReport的函数:
// 读取 HID 报告
func readHIDReport() {
guard let device = self.device else { return }
var report = [UInt8](repeating: 0, count: 9) // 创建一个9字节的缓冲区
var reportLength = report.count
// 从设备读取输入报告
let result = IOHIDDeviceGetReport(device, kIOHIDReportTypeInput, CFIndex(0), &report, &reportLength)
if result == kIOReturnSuccess {
print("HID Report read: \(report)")
} else {
print("Failed to read HID Report")
}
}
至此,基本的HID的功能已经实现。
完整代码
完整的AppDelegate代码如下:
import SwiftUI
import IOKit
import IOKit.usb
import IOKit.hid
class AppDelegate: NSObject, NSApplicationDelegate {
// 创建HIDManager的共享实例
let hid = HIDManager.shared
func applicationDidFinishLaunching(_ aNotification: Notification) {
print("Application started")
// 列出所有HID设备
hid.listHIDDevices()
// 打印每个HID设备的基本信息
for hid in hid.hidDevices {
print(hid.Product)
print(hid.VendorID)
print(hid.ProductID)
}
print("========================")
// 打开特定的HID设备(VID: 21325, PID: 8457)
hid.openHID(vid: 21325, pid: 8457)
// 打印设备打开状态和设备对象
print(hid.isOpen ?? "nil")
print(hid.device ?? "nil")
// 读取HID报告
hid.readHIDReport()
print("⌛️⌛️⌛️⌛️⌛️⌛️⌛️⌛️⌛️⌛️⌛️⌛️")
// 发送HID报告
hid.sendHIDReport(report: [181, 223, 0, 1, 0, 0, 0, 0, 0])
print("⌛️⌛️⌛️⌛️⌛️⌛️⌛️⌛️⌛️⌛️⌛️⌛️")
// 再次读取HID报告
hid.readHIDReport()
// 关闭HID设备
hid.closeHID()
}
}
// 定义HID设备信息结构体
struct HIDDeviceInfo {
let Transport: String // 传输方式
let VendorID: Int // 厂商ID
let ProductID: Int // 产品ID
let Product: String // 产品名称
let PrimaryUsagePage: Int // 主要用途页
let PrimaryUsage: Int // 主要用途
let ReportInterval: Int // 报告间隔
}
class HIDManager {
// 单例模式
static let shared = HIDManager()
var manager: IOHIDManager!
@Published var device: IOHIDDevice? // 当前打开的HID设备
@Published var isOpen: Bool? // 设备是否打开
@Published var hidDevices: [HIDDeviceInfo] // 所有发现的HID设备列表
// 私有初始化方法
private init() {
self.hidDevices = []
}
// 列出所有HID设备
func listHIDDevices() {
self.hidDevices = []
// 创建HID管理器
manager = IOHIDManagerCreate(kCFAllocatorDefault, IOOptionBits(kIOHIDOptionsTypeNone))
// 打开HID管理器
let result = IOHIDManagerOpen(manager, IOOptionBits(kIOHIDOptionsTypeNone))
print(result)
if result != kIOReturnSuccess {
print("Failed to open HID Manager")
}
// 设置设备匹配条件为nil,匹配所有HID设备
IOHIDManagerSetDeviceMatching(manager, nil)
// 获取所有匹配的设备
if let deviceSet = IOHIDManagerCopyDevices(manager) as? Set<IOHIDDevice> {
for device in deviceSet {
if let deviceInfo = getDeviceInfo(device) {
hidDevices.append(deviceInfo)
}
}
}
}
// 获取单个HID设备的信息
private func getDeviceInfo(_ device: IOHIDDevice) -> HIDDeviceInfo? {
// 尝试获取设备的各种属性
guard let transport = IOHIDDeviceGetProperty(device, kIOHIDTransportKey as CFString) as? String,
let vendorID = IOHIDDeviceGetProperty(device, kIOHIDVendorIDKey as CFString) as? Int,
let productID = IOHIDDeviceGetProperty(device, kIOHIDProductIDKey as CFString) as? Int,
let product = IOHIDDeviceGetProperty(device, kIOHIDProductKey as CFString) as? String,
let primaryUsagePage = IOHIDDeviceGetProperty(device, kIOHIDPrimaryUsagePageKey as CFString) as? Int,
let primaryUsage = IOHIDDeviceGetProperty(device, kIOHIDPrimaryUsageKey as CFString) as? Int,
let reportInterval = IOHIDDeviceGetProperty(device, kIOHIDReportIntervalKey as CFString) as? Int else {
return nil
}
// 创建并返回HIDDeviceInfo实例
return HIDDeviceInfo(
Transport: transport,
VendorID: vendorID,
ProductID: productID,
Product: product,
PrimaryUsagePage: primaryUsagePage,
PrimaryUsage: primaryUsage,
ReportInterval: reportInterval
)
}
// 打开指定的HID设备
func openHID(vid: Int, pid: Int) {
// 创建HID管理器
manager = IOHIDManagerCreate(kCFAllocatorDefault, IOOptionBits(kIOHIDOptionsTypeNone))
// 设置设备匹配条件
let deviceMatching: [String: Any] = [
kIOHIDVendorIDKey: vid,
kIOHIDProductIDKey: pid
]
IOHIDManagerSetDeviceMatching(manager, deviceMatching as CFDictionary)
// 打开 HID Manager
let result = IOHIDManagerOpen(manager, IOOptionBits(kIOHIDOptionsTypeNone))
if result != kIOReturnSuccess {
print("Failed to open HID Manager")
return
}
// 获取匹配的设备
if let deviceSet = IOHIDManagerCopyDevices(manager) as? Set<IOHIDDevice>, let matchedDevice = deviceSet.first {
// 尝试打开设备
let openResult = IOHIDDeviceOpen(matchedDevice, IOOptionBits(kIOHIDOptionsTypeNone))
if openResult == kIOReturnSuccess {
self.device = matchedDevice
self.isOpen = true
} else {
self.isOpen = false
}
} else {
self.isOpen = nil
}
}
// 关闭HID设备
func closeHID() {
// 关闭 HID Device
if let device = self.device {
IOHIDDeviceClose(device, IOOptionBits(kIOHIDOptionsTypeNone))
}
// 关闭 HID Manager
IOHIDManagerClose(manager, IOOptionBits(kIOHIDOptionsTypeNone))
print("HID Manager closed")
}
// 读取 HID 报告
func readHIDReport() {
guard let device = self.device else { return }
var report = [UInt8](repeating: 0, count: 9) // 创建一个9字节的缓冲区
var reportLength = report.count
// 从设备读取输入报告
let result = IOHIDDeviceGetReport(device, kIOHIDReportTypeInput, CFIndex(0), &report, &reportLength)
if result == kIOReturnSuccess {
print("HID Report read: \(report)")
} else {
print("Failed to read HID Report")
}
}
// 向设备发送 HID 报告
func sendHIDReport(report: [UInt8]) {
guard let device = self.device else { return }
var report = report
// 向设备发送输出报告
let result = IOHIDDeviceSetReport(device, kIOHIDReportTypeOutput, CFIndex(0), &report, report.count)
if result == kIOReturnSuccess {
print("HID Report sent: \(report)")
} else {
print("Failed to send HID Report")
}
}
}
我的MacBook插上了一些带有HID的设备,并且发送了数据给设备,设备也都返回数据回来进行交互。
如下图,输出所示: