macOS 上使用 Swift 和 IOKit 开发 HID 设备通信应用

在现代计算机系统中,人机接口设备(HID)扮演着至关重要的角色,为用户提供与计算机交互的途径。本文将指导您如何使用 Swift 编程语言和 macOS 的 IOKit 框架来开发一个可以与 HID 设备进行通信的应用程序。无论您是想开发自定义键盘、游戏控制器还是其他 USB 设备的驱动程序,这篇教程都将为您提供坚实的基础。

凌顺实验室(lingshunlab.com)将分享逐步介绍如何设置项目、获取必要的权限、列出系统中的 HID 设备、打开特定设备、读取和发送 HID 报告,以及正确关闭设备。通过实际的代码示例和详细的解释,您将学会如何:

  1. 创建 macOS 应用程序项目
  2. 配置 USB 设备访问权限
  3. 使用 IOKit 枚举 HID 设备
  4. 打开和关闭 HID 设备
  5. 与 HID 设备进行数据交换

这个教程适合于有 Swift 基础的 macOS 开发者,特别是那些对底层硬件通信感兴趣的开发者。通过学习本教程,您将能够开发出能够与各种 HID 设备无缝交互的强大应用程序。

关键词:macOS 开发, Swift, IOKit, HID 设备, USB 通信, 硬件接口, 设备驱动, 人机接口

创建项目

新建一个项目,这里名称为「easyHID」

image-20240813111952722

添加APP访问USB的权限

要让App可以访问到USB,我们需要添加USB的权限,不然的话是无法打开任何的串口或HID设备。

通过以下方法,即可为App添加USB的访问权限:

image-20240813134253462

编写程序

添加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

修改成如下图所示即可:

image-20240813150244309

列出所有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设备信息):

image-20240813160714323

打开指定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的设备,并且发送了数据给设备,设备也都返回数据回来进行交互。

如下图,输出所示:

image-20240813171733262