macOS串口编程指南:使用ORSSerial库实现高效通信

本文凌顺实验室(lingshunlab.com)详细介绍了如何在macOS应用中利用ORSSerial库进行串口通信编程。主要内容包括:

  1. macOS串口通信基础
  2. ORSSerial库的安装和配置
  3. 串口设备枚举和列表展示
  4. 串口打开和关闭操作
  5. 十六进制数据的发送技巧
  6. 异步数据接收和解析方法
  7. 串口连接状态监控

本教程采用Swift语言,展示了串口通信的核心功能实现,包括单例模式的串口管理、事件驱动的数据处理,以及用户友好的错误处理。

无论您是开发串口调试工具、硬件控制软件,还是需要与串口设备交互的应用,本文都为您提供了全面的macOS串口编程解决方案。掌握这些技能,将帮助您在macOS平台上轻松实现各种串口通信需求。

#macOS开发 #串口通信 #ORSSerial #Swift编程 #硬件交互 #异步通信 #串口调试

新建项目

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

image-20240812143316589

添加APP访问串口的权限

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

打开的方法需要在项目中以项目名称命名的文件,例如例子中打开如下图的easySerial文件,添加一个Key 为「com.apple.security.device.serial」的字段,并且Key的Type为「Bool」,Value为「Yes」

image-20240812143625337

添加ORSSerial库

在顶部菜单栏中yi「File」->「Add Package Dependencies...」打开Package的管理窗口。

image-20240812143717443

在Package的管理窗口中添加ORSSerial库

image-20240812143901080

在打开的「Add Package Collection」对话框中,输入ORSSerial库的github地址:

https://github.com/armadsen/ORSSerialPort

编写代码

列出所有串口的代码

例如在本例子,在项目的根目录会新建一个「AppDelegate」的swift文件,代码如下:

// welcom to www.lingshunlab.com

import SwiftUI
import ORSSerial

final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate, NSMenuDelegate {
    let serial = SerialPortManager.shared

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        print(serial.listSerialPorts()) // print serial prots in debug window
    }
}

class SerialPortManager {
    static let shared = SerialPortManager()
    let serialPortManger = ORSSerialPortManager.shared()

    @Published var serialPorts: [ORSSerialPort] = []

    @objc dynamic var serialPort: ORSSerialPort? {
        didSet {
            oldValue?.close()
            oldValue?.delegate = nil
            serialPort?.delegate = self
        }
    }

    func listSerialPorts() -> [ORSSerialPort]{
        self.serialPorts = serialPortManger.availablePorts // list serial prots
        return self.serialPorts
    }
}

并且在「easySerialApp」文件中添加调用AppDelegate的代码:

@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate

点击运行App,可以在调试窗口中看到当前系统中有多少个串口,如下图显示:

image-20240812144709192

打开指定串口

通过listSerialPorts()方法我们得知所有可用串口的名称,现在我们只需要查找到符合名称的对应串口即可指定打开。

在SerialPortManager类中添加这个函数,实现打开指定串口的功能:

func openSerialPort(portName: String, baudRate: Int) {
    self.serialPort = self.serialPorts.filter{ $0.path.contains(portName)}.first
    self.serialPort?.baudRate = NSNumber(value: baudRate)

    if let port = self.serialPort {
        port.open()
        if port.isOpen {
            print("The serial port has been opened")
        } else {
            print("the serial port fail to open")
        }
    }
}

使用方法如下:

serial.openSerialPort(portName: "usbserial-14120", baudRate: 9600)

读取串口数据

打开串口后,就需要进行读写操作了。ORSSerial库提供了非常丰富的API和功能,可以非常简单地实现异步读取串口数据,只需要在SerialPortManager类中添加以下代码:

func serialPort(_ serialPort: ORSSerialPort, didReceive data: Data) {
    print("  ⬅️ 接收 到的数据原始内容: \(data as NSData)")
}

即可实现当串口有数据返回的时候,会立即响应。

发送串口数据

在这里演示一个比较复杂的数据转换,输入String字符串组转换为十六进制数据组发送给串口的方法:

func sendHexData(_ hexString: String) {
    guard let serialPort = self.serialPort else {
        print("Serial port is not open")
        return
    }

    // 将16进制字符串转换为Data
    let data = hexStringToData(hexString)
    // 发送数据
    let dataString = data.map { String(format: "%02X", $0) }.joined(separator: " ")
    print("➡️   发送 的十六进制数据: \(dataString)")
    serialPort.send(data)
}

private func hexStringToData(_ hexString: String) -> Data {
    var data = Data()
    var temp = ""
    for char in hexString {
        temp.append(char)
        if temp.count == 2 {
            if let byte = UInt8(temp, radix: 16) {
                data.append(byte)
            }
            temp = ""
        }
    }
    return data
}

使用方法如下,例如输入“57AB00010003”,实际上会转换成“57 AB 00 01 00 03”的十六进制数据组:

serial.sendHexData("57AB00010003")

关闭串口数据

使用一下简单的例子,可以实现关闭当前打开的串口:

serial.serialPort?.close()

添加串口连接,断开事件

有时候我们还需要知道串口的突然断开和突然接上做一些处理,例如即插即用的应用,则我们需要用到串口的通知事件。

在SerialPortManager类添加以下代码,即可实现:

override init() {
    super.init()
    setupNotifications()
    updateAvailablePorts()
}

private func setupNotifications() {
    NotificationCenter.default.addObserver(self,
                                           selector: #selector(serialPortsWereConnected(_:)),
                                           name: .ORSSerialPortsWereConnected,
                                           object: nil)

    NotificationCenter.default.addObserver(self,
                                           selector: #selector(serialPortsWereDisconnected(_:)),
                                           name: .ORSSerialPortsWereDisconnected,
                                           object: nil)
}

@objc private func serialPortsWereConnected(_ notification: Notification) {
    if let connectedPorts = notification.userInfo?[ORSConnectedSerialPortsKey] as? [ORSSerialPort] {
        print("新的串口被连接:")
        for port in connectedPorts {
            print("- \(port.name)")
        }
        updateAvailablePorts()
    }
}

@objc private func serialPortsWereDisconnected(_ notification: Notification) {
    if let disconnectedPorts = notification.userInfo?[ORSDisconnectedSerialPortsKey] as? [ORSSerialPort] {
        print("串口被断开连接:")
        for port in disconnectedPorts {
            print("- \(port.name)")
        }
        updateAvailablePorts()
    }
}

private func updateAvailablePorts() {
    serialPorts = serialPortManger.availablePorts
    print("可用串口更新:")
    for port in serialPorts {
        print("- \(port.name)")
    }
}

至此,我们已经实现了在MacOS系统开发的APP中使用串口的大多数应用场景。

完整的代码

// welcom to www.lingshunlab.com

import SwiftUI
import ORSSerial

final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate, NSMenuDelegate {
    let serial = SerialPortManager.shared

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        print(serial.listSerialPorts())
        serial.openSerialPort(portName: "usbserial-14120", baudRate: 9600)

        serial.sendHexData("57AB00010003")

        blockMainThreadFor2Seconds()

        serial.serialPort?.close()

    }

    func blockMainThreadFor2Seconds() {
        let expirationDate = Date(timeIntervalSinceNow: 2)
        while Date() < expirationDate {
            RunLoop.current.run(mode: .default, before: expirationDate)
        }
    }

    func calculateChecksum(data: [UInt8]) -> UInt8 {
        return UInt8(data.reduce(0, { (sum, element) in sum + Int(element) }) & 0xFF)
    }
}

class SerialPortManager: NSObject, ORSSerialPortDelegate {

    static let shared = SerialPortManager()
    let serialPortManger = ORSSerialPortManager.shared()

    @Published var serialPorts: [ORSSerialPort] = []

    @objc dynamic var serialPort: ORSSerialPort? {
        didSet {
            oldValue?.close()
            oldValue?.delegate = nil
            serialPort?.delegate = self
        }
    }

    override init() {
        super.init()
        setupNotifications()
        updateAvailablePorts()
    }

    private func setupNotifications() {
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(serialPortsWereConnected(_:)),
                                               name: .ORSSerialPortsWereConnected,
                                               object: nil)

        NotificationCenter.default.addObserver(self,
                                               selector: #selector(serialPortsWereDisconnected(_:)),
                                               name: .ORSSerialPortsWereDisconnected,
                                               object: nil)
    }

    @objc private func serialPortsWereConnected(_ notification: Notification) {
        if let connectedPorts = notification.userInfo?[ORSConnectedSerialPortsKey] as? [ORSSerialPort] {
            print("新的串口被连接:")
            for port in connectedPorts {
                print("- \(port.name)")
            }
            updateAvailablePorts()
        }
    }

    @objc private func serialPortsWereDisconnected(_ notification: Notification) {
        if let disconnectedPorts = notification.userInfo?[ORSDisconnectedSerialPortsKey] as? [ORSSerialPort] {
            print("串口被断开连接:")
            for port in disconnectedPorts {
                print("- \(port.name)")
            }
            updateAvailablePorts()
        }
    }

    private func updateAvailablePorts() {
        serialPorts = serialPortManger.availablePorts
        print("可用串口更新:")
        for port in serialPorts {
            print("- \(port.name)")
        }
    }

    func listSerialPorts() -> [ORSSerialPort]{
        self.serialPorts = serialPortManger.availablePorts
        return self.serialPorts
    }

    func openSerialPort(portName: String, baudRate: Int) {
        self.serialPort = self.serialPorts.filter{ $0.path.contains(portName)}.first
        self.serialPort?.baudRate = NSNumber(value: baudRate)

        if let port = self.serialPort {
            port.open()
            if port.isOpen {
                print("The serial port has been opened")
            } else {
                print("the serial port fail to open")
            }
        }
    }

    func sendHexData(_ hexString: String) {
        guard let serialPort = self.serialPort else {
            print("Serial port is not open")
            return
        }

        // 将16进制字符串转换为Data
        let data = hexStringToData(hexString)
        // 发送数据
        let dataString = data.map { String(format: "%02X", $0) }.joined(separator: " ")
        print("➡️   发送 的十六进制数据: \(dataString)")
        serialPort.send(data)
    }

    private func hexStringToData(_ hexString: String) -> Data {
        var data = Data()
        var temp = ""
        for char in hexString {
            temp.append(char)
            if temp.count == 2 {
                if let byte = UInt8(temp, radix: 16) {
                    data.append(byte)
                }
                temp = ""
            }
        }
        return data
    }

    func serialPort(_ serialPort: ORSSerialPort, didEncounterError error: Error) {
        print(error)
    }

    func serialPortWasOpened(_ serialPort: ORSSerialPort) {
        print("串口已打开")
    }

    func serialPortWasClosed(_ serialPort: ORSSerialPort) {
        print("串口已关闭")
    }

    func serialPortWasRemovedFromSystem(_ serialPort: ORSSerialPort) {
        self.serialPort = nil
    }

    func serialPort(_ serialPort: ORSSerialPort, didReceive data: Data) {
        print("  ⬅️ 接收 到的数据原始内容: \(data as NSData)")
        // 将接收到的数据转换为十六进制字符串
        let dataString = data.map { String(format: "%02X", $0) }.joined(separator: " ")
        print("  ⬅️ 接收 到的十六进制数据: \(dataString)")
        // 将接收到的数据转换为字符串
        if let string = String(data: data, encoding: .utf8) {
            print("转换后的字符串: \(string)")
        } else {
            print("无法将数据转换为字符串")
        }
    }
}

运行代码,在特定的串口通信机制下,实现如下图的所有功能:

image-20240812182505230