Can I open a serial port with Swift? Part I

This article is part of a series.

The answer is yes, on MacOS and Linux. But what I did is maybe sort of cheating? It uses a library that puts old fashioned C code right in the Swift. No I/OKit, DriverKit, Objective-C libraries. Just C. On one hand it does require turning off Sandbox mode which is a No-No for software destined for the MacOS App Store. On the other hand it works on Linux as well.

This will not work on iOS, iPad, tvOS, visionOS… nothing. For that… use Bluetooth.

This post represents a brain dump / code dump of the minimal viable working code. I am already working on repos with increasingly restructured code which will be in the next post:

If instead you’d like to have this code in the state I left it (no promises):

Library | App | Ardunio:ForRead | Ardunio:ForWrite

Misc Resources

From SwiftSerial

Who’s connected

There are two main ways to find out what devices are connected and more information about them.

Frist, since on a POSIX style system everything is a file, using the ls command to find the devices listed tops the list. Arduino boards will generally show up on a Mac as a /dev/cu.usbmodemXXX device file where the XXX will be replaced by some number. They’ll be listed as a tty device file as well.

ls /dev/cu.* # "calling unit"
# results on my machine:  /dev/cu.Bluetooth-Incoming-Port	/dev/cu.usbmodem1101
ls /dev/tty.* # TeleTYpes
#/dev/tty.usbmodem1101

Note that Arduino boards get listed as modems because they fall in the category of virtual serial communication devices known as CDC/ACM devices (Communication Device Class/Abstract Control Model). They do not implement the Hayes AT command set which what one might think of as what qualifies as “being a modem” if one had used them in the past.

Second, on MacOS specifically you can use the I/O Kit Device Registry lookup utility to find out lots of information about the USB devices hooked up to the computer. more info

ioreg -p IOUSB
ioreg -r -c IOUSBHostDevice -l
usage: ioreg [-abfilrtxy] [-c class] [-d depth] [-k key] [-n name] [-p plane] [-w width]
where options are:
	-a archive output
	-b show object name in bold
	-c list properties of objects with the given class
	-d limit tree to the given depth
	-f enable smart formatting
	-i show object inheritance
	-k list properties of objects with the given key
	-l list properties of all objects
	-n list properties of objects with the given name
	-p traverse registry over the given plane (IOService is default)
	-r show subtrees rooted by the given criteria
	-t show location of each subtree
	-w clip output to the given line width (0 is unlimited)
	-x show data and numbers as hexadecimal
	-y do not consider DriverKit classes with -c

Put and Arduino in Serial Loop Back Mode

“Hello World” on the library I picked requires using Serial Loop Back mode.

I recommend using an old fashioned Uno R3 board, as they definitely can do it (external driver chips).

https://support.arduino.cc/hc/en-us/articles/360020366520-How-to-do-a-loopback-test

Picking a Library

After quite a bit of poking around I decided to get up and running with the SwiftSerial library. It’s small and uses C commands I’m already familiar with.

SwiftSerial “Hello World”

To side step the issues of entitlements I decided to set up a package. (swift package init --type executable)

// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "HelloSerialTest",
    products: [
        .executable(
            name: "HelloSerialTest",
            targets: ["HelloSerialTest"]),
    ],
    dependencies: [
        .package(url: "https://github.com/yeokm1/swiftserial.git", from: "0.1.2")
    ],
    targets: [
        .executableTarget(
            name: "HelloSerialTest", 
            dependencies: [
                .product(name: "SwiftSerial", package: "swiftserial")
            ]
        ),
        .testTarget(
            name: "HelloSerialTestTests",
            dependencies: ["HelloSerialTest"]),
    ]
)
// 
//
//  HelloSerialTest.swift
//  HelloArduino
//
//  Created by Carlyn Maw on 8/24/23.
//

@main
struct HelloSerialTest {
    static func main() {
        let test = EchoTest(portName: "/dev/cu.usbmodem1101")
        test.run()

    }
}

I copied the example code from SwiftSerial into a struct so I wouldn’t have to build it separately as it says in the instructions. I did do that. It did work. However, this is about adding the code to my own projects.

//
//  EchoTest.swift
//  HelloArduino
//
//  Created by Carlyn Maw on 8/24/23.
//

import Foundation
import SwiftSerial


struct EchoTest {
    let testString: String = "The quick brown fox jumps over the lazy dog 01234567890."
    let numberOfMultiNewLineTest : Int = 5
    
    let portName:String
    let serialPort: SerialPort
    
    init(portName: String) {
        self.portName = portName
        self.serialPort = SerialPort(path: portName)
    }
    
    func run() {
        print("You should do a loopback i.e short the TX and RX pins of the target serial port before testing.")

        do {

            print("Attempting to open port: \(portName)")
            try serialPort.openPort()
            print("Serial port \(portName) opened successfully.")
            defer {
                serialPort.closePort()
                print("Port Closed")
            }

            serialPort.setSettings(receiveRate: .baud9600,
                                   transmitRate: .baud9600,
                                   minimumBytesToRead: 1)

            print("Writing test string <\(testString)> of \(testString.count) characters to serial port")

            let bytesWritten = try serialPort.writeString(testString)

            print("Successfully wrote \(bytesWritten) bytes")
            print("Waiting to receive what was written...")

            let stringReceived = try serialPort.readString(ofLength: bytesWritten)

            if testString == stringReceived {
                print("Received string is the same as transmitted string. Test successful!")
            } else {
                print("Uh oh! Received string is not the same as what was transmitted. This was what we received,")
                print("<\(stringReceived)>")
            }


            print("Now testing reading/writing of \(numberOfMultiNewLineTest) lines")

            var multiLineString: String = ""


            for _ in 1...numberOfMultiNewLineTest {
                multiLineString += testString + "\n"
            }

            print("Now writing multiLineString")

            var _ = try serialPort.writeString(multiLineString)


            for i in 1...numberOfMultiNewLineTest {
                let stringReceived = try serialPort.readLine()
                  
                if testString == stringReceived {
                    print("Received string \(i) is the same as transmitted section. Moving on...")
                } else {
                    print("Uh oh! Received string \(i) is not the same as what was transmitted. This was what we received,")
                    print("<\(stringReceived)>")
                    break
                }
            }
            
            print("End of example");


        } catch PortError.failedToOpen {
            print("Serial port \(portName) failed to open. You might need root permissions.")
        } catch {
            print("Error: \(error)")
        }

    }
}

And it all worked!

Adding it to an App

Add the package using the link: https://github.com/yeokm1/swiftserial.git

I did set the USB Entitlements: Screenshot of how to set the USB Entitlements

And turn off the Sandbox: Screenshot of the entitlements file open with the Sandbox setting set to off

I did not set com.apple.security.device.serial in the entitlements file, but I will try that in the future to see if it lets me turn the SandBox back on. reference.

I added the same EchoTest.swift file from the package, and updated the default ContentView()

//
//  ContentView.swift
//  HelloArduino
//
//  Created by Carlyn Maw on 8/24/23.
//

import SwiftUI

struct ContentView: View {
    @State var echoTest:EchoTest = EchoTest(portName: "/dev/cu.usbmodem1101")
    
    @State var brightness:Double = 0.5
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text("Hello, world!")
        }
        .padding()
        .onAppear() {
            echoTest.run()
        }
    }
}

Works again!

Interacting with the Arduino more realistically

Arduino a while back decided that having to hold down the reset pin in order to upload code onto them was too fiddly. So now, every time one opens the serial port, the whole board resets, waiting to receive new instructions for a split second before proceeding. They do this by linking the DTR line to the reset pin. It can be turned off (cut the trace, add a cap) but that’s not the norm.

This means that it’s important to not be opening and closing that serial port too often. I created a class that could be made an observable object, not to publish a value at this time, but to persist the connection.

The tricky business in this class is that I want to option to persist the connection, or not. This means a wrapper for every call, checking to see if the class thinks the port is open. This setup is not ideal. At some point I will have to go into SwiftSerial and create a function to ask the port itself if it’s open, etc.

//
//  SerialManager.swift
//  HelloArduino
//
//  Created by Carlyn Maw on 8/24/23.
//

import Foundation
import SwiftSerial


class SerialManager:ObservableObject {
    let portName:String
    let serialPort: SerialPort
    
    let maintainConnection:Bool
    var isOpen:Bool = false
    
    init(portName: String, maintainConnection:Bool = true) {
        self.portName = portName
        self.serialPort = SerialPort(path: portName)
        self.maintainConnection = maintainConnection
        
        if maintainConnection {
            self.safeOpen()
        }
    }
    
    deinit {
        close()
    }
    
    func open() throws {
        print("Attempting to open port: \(portName)")
        try serialPort.openPort()
        print("Serial port \(portName) opened successfully.")
        serialPort.setSettings(receiveRate: .baud9600,
                               transmitRate: .baud9600, minimumBytesToRead: 1)
        isOpen = true
    }
    
    func safeOpen() {
        do {
            try open()
        } catch PortError.failedToOpen {
            isOpen = false
            print("Serial port \(portName) failed to open.")
        } catch {
            fatalError("Error: \(error)")
        }
    }
    
    func close() {
        serialPort.closePort()
        print("Port Closed")
        isOpen = false
    }
    
    private func wrappedThrowing<I, R>(function:(I) throws -> R, parameter:I) -> Result<R,Error> {
        defer {
            if !maintainConnection { close() }
        }
        do {
            if !isOpen { try open() }
            let r = try function(parameter)
            return .success(r)
            
        } catch PortError.failedToOpen {
            print("Serial port \(portName) failed to open.")
            return .failure(PortError.failedToOpen)
        } catch {
            fatalError("Error: \(error)")
        }
    }
    
    
    @discardableResult
    func pingII() -> Int {
        defer {
            if !maintainConnection { close() }
        }
        do {
            if !isOpen { try open() }
            let bytesWritten = try serialPort.writeString("A")
            return bytesWritten
        } catch PortError.failedToOpen {
            print("Serial port \(portName) failed to open.")
            return -1
        } catch {
            //TODO: catch other errors
            fatalError("Error: \(error)")
        }
    }
    
    @discardableResult
    func sendByte(_ byte:UInt8) -> Int {
        func pingII() -> Int {
            defer {
                if !maintainConnection { close() }
            }
            do {
                if !isOpen { try open() }
                var m_message = byte
                let bytesWritten =  try serialPort.writeBytes(from: &m_message, size: 1)
                return bytesWritten
            } catch PortError.failedToOpen {
                print("Serial port \(portName) failed to open.")
                return -1
            } catch {
                //TODO: catch other errors
                fatalError("Error: \(error)")
            }
        }
        return -1
    }
}

With this code loaded on the Arduino:

/*
  Variation on Dimmer example. 
  https://www.arduino.cc/en/Tutorial/BuiltInExamples/Dimmer

The circuit:
  - LED attached from digital pin 9 to ground through 220 ohm resistor.
  - LED attached to digital pin 6 to power via a 220 ohm resistor (sinking).
  - Serial connection to Processing, Max/MSP, or another serial application

  Edited Carlyn Maw 2023 Aug 24
*/
const int ledPin = 9;
const int noSignalPin = 6; 

unsigned long previousMillis = 0;
const long interval = 30;
byte brightness = 0;

void setup() {
  // initialize the serial communication:
  Serial.begin(9600);
  // initialize the ledPin as an output:
  pinMode(ledPin, OUTPUT);
  analogWrite(ledPin, 0); //LOW is off because anode is wired to Arduino

  //ADDITION TO DIMMER CODE
  pinMode(noSignalPin, OUTPUT);
  digitalWrite(noSignalPin, HIGH);  //HIGH is off because cathode is wired to Arduino
}

void loop() {
  unsigned long currentMillis = millis();

  if (Serial.available()) {
    digitalWrite(noSignalPin, HIGH);
    brightness = Serial.read();

  } else {
     if (currentMillis - previousMillis >= interval) {
      previousMillis = currentMillis;
      if (brightness > 0) {
        brightness = brightness - 1;
      }
     }
     digitalWrite(noSignalPin, LOW);
  }

  analogWrite(ledPin, brightness);
}

And content view updated to:

//
//  ContentView.swift
//  HelloArduino
//
//  Created by Carlyn Maw on 8/24/23.
//

import SwiftUI

struct ContentView: View {
    @StateObject var serialWriter:SerialManager = SerialManager(portName: "/dev/cu.usbmodem11201")
    
    @State var brightness:Double = 0.5
    var body: some View {
        VStack {
            Slider(value: $brightness) { editing in
                if !editing {
                    let val = UInt8(brightness * 255)
                    print(val)
                    serialWriter.sendByte(val)
                }
            }
        }
        .padding()
        .onAppear() {
            serialWriter.pingII()
        }
    }
}

My LEDs behaved and dimmed as expected. Success!

Reading Serial

The SerialManager got some upgrades to reduce boiler plate once I added the read functions. I’ve also changed the return types to result builders to prep for async. The below code all works together, but it has some serious problems. First of all the program will hang if the Arduino is connected but not sending serial information. I tried changing one of the settings in the SwiftSerial library that is supposed to fix that. It did not. That may be on me and I will try again. Second, SwiftSerial has no flush command to nuke the buffer and get the latest readings when the button is pressed. It’s now pretty certain that I will be making some changes to my copy of SwiftSerial, but as written it’s still an incredibly helpful bootstrap.

//
//  SerialManager.swift
//  HelloArduino
//
//  Created by Carlyn Maw on 8/24/23.
//

import Foundation
import SwiftSerial


class SerialManager:ObservableObject {
    let portName:String
    let serialPort: SerialPort
    
    let maintainConnection:Bool
    var isOpen:Bool = false
    
    init(portName: String, maintainConnection:Bool = true) {
        self.portName = portName
        self.serialPort = SerialPort(path: portName)
        self.maintainConnection = maintainConnection
        
        if maintainConnection {
            //print("safe open")
            self.safeOpen()
        }
    }
    
    deinit {
        close()
    }
    
    func open() throws {
        print("Attempting to open port: \(portName)")
        try serialPort.openPort()
        print("Serial port \(portName) opened successfully.")
        serialPort.setSettings(receiveRate: .baud9600,
                               transmitRate: .baud9600, minimumBytesToRead: 1)
        isOpen = true
    }
    
    func safeOpen() {
        do {
            try open()
        } catch PortError.failedToOpen {
            isOpen = false
            print("Serial port \(portName) failed to open.")
        } catch {
            //TODO: Handle other errors
            fatalError("Error: \(error)")
        }
    }
    
    func close() {
        serialPort.closePort()
        print("Port Closed")
        isOpen = false
    }
    
    private func wrappedThrowing<I, R>(function:(I) throws -> R, parameter:I) -> Result<R,Error> {
        defer {
            if !maintainConnection { close() }
        }
        do {
            if !isOpen { try open() }
            let r = try function(parameter)
            return .success(r)

        } catch PortError.failedToOpen {
            print("Serial port \(portName) failed to open.")
            return .failure(PortError.failedToOpen)
        } catch {
            //TODO: Handle other errors
            fatalError("Error: \(error)")
        }
    }
    

    @discardableResult
    func pingII() -> Result<Int, Error> {
        return wrappedThrowing(function: serialPort.writeString, parameter: "A")
        
    }
    
    @discardableResult
    func sendByte(_ byte:UInt8) -> Result<Int, Error> {
        return wrappedThrowing(function: writeByteWrapper, parameter: byte)
    }
    
    private func writeByteWrapper(_ byte:UInt8) throws  -> Int {
        var m_message = byte
        return try serialPort.writeBytes(from: &m_message, size: 1)
    }
    
    func readBytes(count:Int) -> Result<Data, Error>{
        return wrappedThrowing(function: serialPort.readData, parameter: count)
    }
    
    func readLine() -> Result<String, Error>{
        return wrappedThrowing(function: serialPort.readUntilChar, parameter: CChar(10))
    }
}

Arduino Code:

/*
  Exactly the example code:
  https://www.arduino.cc/en/Tutorial/BuiltInExamples/Graph
*/

void setup() {
  // initialize the serial communication:
  Serial.begin(9600);
}

void loop() {
  // send the value of analog input 0:
  Serial.println(analogRead(A0));
  // wait a bit for the analog-to-digital converter to stabilize after the last
  // reading:
  delay(2);
}

And a ReadView:

//
//  ReadView.swift
//  HelloArduino
//
//  Created by Carlyn Maw on 8/24/23.
//

import SwiftUI

struct ReadView: View {
    @StateObject var serialReader:SerialManager = SerialManager(portName: "/dev/cu.usbmodem11201")
    
    @State var reading:String = ""
    var body: some View {
        VStack {
            Text("\(reading)")
            Button("Read") {
                updateText()
            }
        }
        .padding()
        .onAppear() {
            serialReader.pingII()
        }
    }
    
    func updateText() {
        //let result = serialReader.readLine()
        let result = serialReader.readBytes(count: 256)
        switch result {
        case .success(let message):
            reading = String(data: message, encoding: .utf8)!
        case .failure(let error):
            print(error)
        }
    }
}

This article is part of a series.