├── .gitignore ├── first-slide.png ├── Examples ├── SwiftSerialBinary │ ├── gitignore │ ├── Package.swift │ └── Sources │ │ └── main.swift ├── SwiftSerialExample │ ├── gitignore │ ├── Package.swift │ └── Sources │ │ └── main.swift └── SwiftSerialIM │ ├── Package.swift │ └── Sources │ └── main.swift ├── swift-serial-talk-slides.pptx ├── Package.swift ├── LICENSE ├── README.md └── Sources └── SwiftSerial.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /first-slide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeokm1/SwiftSerial/HEAD/first-slide.png -------------------------------------------------------------------------------- /Examples/SwiftSerialBinary/gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /Examples/SwiftSerialExample/gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /swift-serial-talk-slides.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yeokm1/SwiftSerial/HEAD/swift-serial-talk-slides.pptx -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftSerial", 8 | products: [ 9 | .library(name: "SwiftSerial", targets: ["SwiftSerial"]), 10 | ], 11 | dependencies: [], 12 | targets: [ 13 | .target( 14 | name: "SwiftSerial", 15 | dependencies: [], 16 | path: "Sources" 17 | ), 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /Examples/SwiftSerialIM/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftSerialIM", 8 | dependencies: [ 9 | .package(url: "https://github.com/yeokm1/SwiftSerial.git", from: "0.1.2") 10 | ], 11 | targets: [ 12 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 13 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 14 | .target( 15 | name: "SwiftSerialIM", 16 | dependencies: ["SwiftSerial"], 17 | path: "Sources" 18 | ), 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /Examples/SwiftSerialBinary/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftSerialBinary", 8 | dependencies: [ 9 | .package(url: "https://github.com/yeokm1/SwiftSerial.git", from: "0.1.2") 10 | ], 11 | targets: [ 12 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 13 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 14 | .target( 15 | name: "SwiftSerialBinary", 16 | dependencies: ["SwiftSerial"], 17 | path: "Sources" 18 | ), 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /Examples/SwiftSerialExample/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SwiftSerialExample", 8 | dependencies: [ 9 | .package(url: "https://github.com/yeokm1/SwiftSerial.git", from: "0.1.2") 10 | ], 11 | targets: [ 12 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 13 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 14 | .target( 15 | name: "SwiftSerialExample", 16 | dependencies: ["SwiftSerial"], 17 | path: "Sources" 18 | ), 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Yeo Kheng Meng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Examples/SwiftSerialBinary/Sources/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftSerial 3 | 4 | print("You should do a loopback i.e short the TX and RX pins of the target serial port before testing.") 5 | 6 | let testBinaryArray : [UInt8] = [0x11, 0x22, 0x33, 0x0D, 0x44] 7 | 8 | let arguments = CommandLine.arguments 9 | guard arguments.count >= 2 else { 10 | print("Need serial port name, e.g. /dev/ttyUSB0 or /dev/cu.usbserial as the first argument.") 11 | exit(1) 12 | } 13 | 14 | let portName = arguments[1] 15 | let serialPort: SerialPort = SerialPort(path: portName) 16 | 17 | do { 18 | 19 | print("Attempting to open port: \(portName)") 20 | try serialPort.openPort() 21 | print("Serial port \(portName) opened successfully.") 22 | defer { 23 | serialPort.closePort() 24 | print("Port Closed") 25 | } 26 | 27 | serialPort.setSettings(receiveRate: .baud9600, 28 | transmitRate: .baud9600, 29 | minimumBytesToRead: 1) 30 | 31 | print("Sending: ", terminator:"") 32 | print(testBinaryArray.map { String($0, radix: 16, uppercase: false) }) 33 | 34 | let dataToSend: Data = Data(_: testBinaryArray) 35 | 36 | let bytesWritten = try serialPort.writeData(dataToSend) 37 | 38 | print("Successfully wrote \(bytesWritten) bytes") 39 | print("Waiting to receive what was written...") 40 | 41 | let dataReceived = try serialPort.readData(ofLength: bytesWritten) 42 | 43 | print("Received: ", terminator:"") 44 | print(dataReceived.map { String($0, radix: 16, uppercase: false) }) 45 | 46 | if(dataToSend.elementsEqual(dataReceived)){ 47 | print("Received data is the same as transmitted data. Test successful!") 48 | } else { 49 | print("Uh oh! Received data is not the same as what was transmitted. This was what we received,") 50 | print(dataReceived.map { String($0, radix: 16, uppercase: false) }) 51 | } 52 | 53 | print("End of example"); 54 | 55 | 56 | } catch PortError.failedToOpen { 57 | print("Serial port \(portName) failed to open. You might need root permissions.") 58 | } catch { 59 | print("Error: \(error)") 60 | } 61 | -------------------------------------------------------------------------------- /Examples/SwiftSerialExample/Sources/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftSerial 3 | 4 | print("You should do a loopback i.e short the TX and RX pins of the target serial port before testing.") 5 | 6 | let testString: String = "The quick brown fox jumps over the lazy dog 01234567890." 7 | 8 | let numberOfMultiNewLineTest : Int = 5 9 | 10 | let test3Strings: String = testString + "\n" + testString + "\n" + testString + "\n" 11 | 12 | let arguments = CommandLine.arguments 13 | guard arguments.count >= 2 else { 14 | print("Need serial port name, e.g. /dev/ttyUSB0 or /dev/cu.usbserial as the first argument.") 15 | exit(1) 16 | } 17 | 18 | let portName = arguments[1] 19 | let serialPort: SerialPort = SerialPort(path: portName) 20 | 21 | do { 22 | 23 | print("Attempting to open port: \(portName)") 24 | try serialPort.openPort() 25 | print("Serial port \(portName) opened successfully.") 26 | defer { 27 | serialPort.closePort() 28 | print("Port Closed") 29 | } 30 | 31 | serialPort.setSettings(receiveRate: .baud9600, 32 | transmitRate: .baud9600, 33 | minimumBytesToRead: 1) 34 | 35 | print("Writing test string <\(testString)> of \(testString.count) characters to serial port") 36 | 37 | let bytesWritten = try serialPort.writeString(testString) 38 | 39 | print("Successfully wrote \(bytesWritten) bytes") 40 | print("Waiting to receive what was written...") 41 | 42 | let stringReceived = try serialPort.readString(ofLength: bytesWritten) 43 | 44 | if testString == stringReceived { 45 | print("Received string is the same as transmitted string. Test successful!") 46 | } else { 47 | print("Uh oh! Received string is not the same as what was transmitted. This was what we received,") 48 | print("<\(stringReceived)>") 49 | } 50 | 51 | 52 | print("Now testing reading/writing of \(numberOfMultiNewLineTest) lines") 53 | 54 | var multiLineString: String = "" 55 | 56 | 57 | for _ in 1...numberOfMultiNewLineTest { 58 | multiLineString += testString + "\n" 59 | } 60 | 61 | print("Now writing multiLineString") 62 | 63 | var _ = try serialPort.writeString(multiLineString) 64 | 65 | 66 | for i in 1...numberOfMultiNewLineTest { 67 | let stringReceived = try serialPort.readLine() 68 | 69 | if testString == stringReceived { 70 | print("Received string \(i) is the same as transmitted section. Moving on...") 71 | } else { 72 | print("Uh oh! Received string \(i) is not the same as what was transmitted. This was what we received,") 73 | print("<\(stringReceived)>") 74 | break 75 | } 76 | } 77 | 78 | print("End of example"); 79 | 80 | 81 | } catch PortError.failedToOpen { 82 | print("Serial port \(portName) failed to open. You might need root permissions.") 83 | } catch { 84 | print("Error: \(error)") 85 | } 86 | -------------------------------------------------------------------------------- /Examples/SwiftSerialIM/Sources/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftSerial 3 | 4 | 5 | let arguments = CommandLine.arguments 6 | guard arguments.count >= 2 else { 7 | print("Need serial port name, e.g. /dev/ttyUSB0 or /dev/cu.usbserial as the first argument.") 8 | exit(1) 9 | } 10 | 11 | print("Connect a null modem serial cable between two machines before you continue to use this program") 12 | 13 | let portName = arguments[1] 14 | let serialPort: SerialPort = SerialPort(path: portName) 15 | 16 | var myturn = true 17 | 18 | // Prepares the stdin so we can getchar() without echoing 19 | func prepareStdin() { 20 | 21 | // Set up the control structure 22 | var settings = termios() 23 | 24 | // Get options structure for stdin 25 | tcgetattr(STDIN_FILENO, &settings) 26 | 27 | //Turn off ICANON and ECHO 28 | settings.c_lflag &= ~tcflag_t(ICANON | ECHO) 29 | 30 | tcsetattr(STDIN_FILENO, TCSANOW, &settings) 31 | } 32 | 33 | func getKeyPress () -> UnicodeScalar { 34 | let valueRead: Int = Int(getchar()) 35 | 36 | guard let charRead = UnicodeScalar(valueRead) else{ 37 | return UnicodeScalar("")! 38 | } 39 | 40 | return charRead 41 | } 42 | 43 | func printToScreenFrom(myself: Bool, characterToPrint: UnicodeScalar){ 44 | 45 | if(myturn && !myself){ 46 | myturn = false 47 | print("\n\nOther: ", terminator:"") 48 | } else if (!myturn && myself){ 49 | myturn = true 50 | print("\n\nMe: ", terminator:"") 51 | } 52 | 53 | print(characterToPrint, terminator:"") 54 | } 55 | 56 | func backgroundRead() { 57 | while true{ 58 | do{ 59 | let readCharacter = try serialPort.readChar() 60 | printToScreenFrom(myself: false, characterToPrint: readCharacter) 61 | } catch { 62 | print("Error: \(error)") 63 | } 64 | } 65 | } 66 | 67 | 68 | 69 | do { 70 | 71 | print("Attempting to open port: \(portName)") 72 | try serialPort.openPort() 73 | print("Serial port \(portName) opened successfully.") 74 | defer { 75 | serialPort.closePort() 76 | print("Port Closed") 77 | } 78 | 79 | serialPort.setSettings(receiveRate: .baud9600, 80 | transmitRate: .baud9600, 81 | minimumBytesToRead: 1) 82 | 83 | prepareStdin() 84 | 85 | 86 | //Turn off output buffering if not multiple threads will have problems printing 87 | setbuf(stdout, nil); 88 | 89 | 90 | //Run the serial port reading function in another thread 91 | #if os(Linux) 92 | var readingThread = pthread_t() 93 | 94 | let pthreadFunc: @convention(c) (UnsafeMutableRawPointer?) -> UnsafeMutableRawPointer? = { 95 | observer in 96 | 97 | backgroundRead() 98 | return nil 99 | } 100 | 101 | pthread_create(&readingThread, nil, pthreadFunc, nil) 102 | 103 | #elseif os(OSX) 104 | DispatchQueue.global(qos: .userInitiated).async { 105 | backgroundRead() 106 | } 107 | 108 | #endif 109 | 110 | 111 | print("\nReady to send and receive messages in realtime!") 112 | print("\nMe: ", terminator:"") 113 | 114 | 115 | while true { 116 | let enteredKey = getKeyPress() 117 | printToScreenFrom(myself: true, characterToPrint: enteredKey) 118 | var _ = try serialPort.writeChar(enteredKey) 119 | } 120 | 121 | 122 | } catch PortError.failedToOpen { 123 | print("Serial port \(portName) failed to open. You might need root permissions.") 124 | } catch { 125 | print("Error: \(error)") 126 | } 127 | 128 | 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftSerial 2 | 3 | (This repository is now archived as I'm no longer maintaining this. You may consider using a fork of this library by `mredig` https://github.com/mredig/SwiftSerial) 4 | 5 | A Swift Linux and Mac library for reading and writing to serial ports. This library has been tested to work on macOS Mojave, Linux Mint 18 (based on Ubuntu 16.04) and on the [Raspberry Pi 3 on Ubuntu 16.04](https://wiki.ubuntu.com/ARM/RaspberryPi) and Raspberry Pi 4 on Raspian Buster. Other platforms using Ubuntu like the Beaglebone might work as well. 6 | 7 | This library is an improvement over my previous now deprecated library [SwiftLinuxSerial](https://github.com/yeokm1/SwiftLinuxSerial) which was less Swifty and supported only Linux. This library is thanks largely to [Jay Jun](https://github.com/jayjun). His original pull request can be found [here](https://github.com/yeokm1/SwiftLinuxSerial/pull/1). 8 | 9 | ## Talk on this library 10 | 11 | I gave a talk on this library and one of its examples SwiftSerialIM. Click on the links below to see the slides and video. 12 | 13 | [![My slides on slideshare](first-slide.png)](http://www.slideshare.net/yeokm1/a-science-project-swift-serial-chat) 14 | 15 | [![](http://img.youtube.com/vi/6PWP1eZo53s/0.jpg)](https://www.youtube.com/watch?v=6PWP1eZo53s) 16 | 17 | ## Mac OS Preparation 18 | 19 | You should have Xcode 8 or above installed with the command line tools. 20 | 21 | You may need to enable USB and/or serial entitlement. 22 | 23 | To develop app with XCode, enable the App Sandbox capability in Xcode, and under Hardware, select USB. (Mac Apps are sandboxed and you need the USB entitlement.) 24 | Swift 3.0 25 | 26 | For Serial entitlement, there is no checkbox for it. So `com.apple.security.device.serial` needs to be manually added to the "YourApp.entitlements" file in the Xcode project. Thanks to [this issue](https://github.com/yeokm1/SwiftSerial/issues/18) raised by @doHernandezM. 27 | 28 | ## Linux System Preparation 29 | 30 | Varies depending on system... 31 | 32 | ## Jumping straight into sample code 33 | To get started quickly, you can take a look at my example projects [here](Examples/). 34 | 35 | ### Example 1: Loopback Test 36 | 37 | In order to run this example properly, you need to connect one of your (USB/UART) serial ports in a loopback manner. Basically, you short the TX and RX pins of the serial port. This library currently only support the `/dev/cu.*` variant on Mac. Read the beginning of the API usage section for more details. 38 | 39 | ```bash 40 | git clone https://github.com/yeokm1/SwiftSerial.git 41 | cd SwiftSerial/Examples/SwiftSerialExample/ 42 | swift build 43 | 44 | #For Linux: You need root to access the serial port. Replace /dev/ttyUSB0 with the name of your serial port under test 45 | sudo ./.build/debug/SwiftSerialExample /dev/ttyUSB0 46 | 47 | #For Mac: Root is not required 48 | ./.build/debug/SwiftSerialExample /dev/cu.usbserial 49 | 50 | #If all goes well you should see a series of messages informing you that data transmitted has been received properly. 51 | ``` 52 | 53 | ### Example 2: Binary Loopback Test 54 | 55 | Variant of example 1 but testing the transfer of binary data specifically ensuring the`0x0D` bit is not converted to another character. 56 | 57 | ```bash 58 | git clone https://github.com/yeokm1/SwiftSerial.git 59 | cd SwiftSerial/Examples/SwiftSerialBinary/ 60 | swift build 61 | 62 | #For Linux: You need root to access the serial port. Replace /dev/ttyUSB0 with the name of your serial port under test 63 | sudo ./.build/debug/SwiftSerialBinary /dev/ttyUSB0 64 | 65 | #For Mac: Root is not required 66 | ./.build/debug/SwiftSerialBinary /dev/cu.usbserial 67 | 68 | #If all goes well you should see a series of messages informing you that data transmitted has been received properly. 69 | ``` 70 | 71 | ### Example 3: A chat app between 2 machines 72 | 73 | In order to run this example properly, you need 2 machines connected by a [null-modem cable](https://en.wikipedia.org/wiki/Null_modem) or 2 USB-Serial adapters with the TX-RX pins connected to each other. Run a copy of my program on both machines. 74 | 75 | ```bash 76 | git clone https://github.com/yeokm1/SwiftSerial.git 77 | cd SwiftSerial/Examples/SwiftSerialIM/ 78 | swift build 79 | 80 | #For Linux: You need root to access the serial port. Replace /dev/ttyUSB0 with the name of your serial port under test 81 | sudo ./.build/debug/SwiftSerialIM /dev/ttyUSB0 82 | 83 | #For Mac: Root is not required 84 | ./.build/debug/SwiftSerialIM /dev/cu.usbserial 85 | ``` 86 | People at both machines can now "chat" with each other. 87 | 88 | ## Integrating with your project 89 | 90 | Add SwiftSerial as a dependency to your project by editing the `Package.swift` file. 91 | 92 | ```swift 93 | let package = Package( 94 | name: "NameOfMyProject", 95 | dependencies: [ 96 | .package(url: "https://github.com/yeokm1/SwiftSerial.git", from: "0.1.2"), 97 | ... 98 | ] 99 | ... 100 | ) 101 | ``` 102 | 103 | Make sure to `import SwiftSerial` in the source files that use my API. 104 | 105 | Then run `swift build` to download the dependencies and compile your project. Your executable will be found in the `./.build/debug/` directory. 106 | 107 | ## API usage 108 | 109 | ### Initialise the class 110 | 111 | ```swift 112 | let serialPort: SerialPort = SerialPort(path: portName) 113 | ``` 114 | Supply the portname that you wish to open like `/dev/ttyUSB0` or `/dev/cu.usbserial`. 115 | 116 | For Macs, this library currently only works with the `/dev/cu.*` ports instead of the `/dev/tty.*`. I have enabled blocking on the serial port to prevent high CPU usage which will prevent the `/dev/tty.*` from working. Read more about the differences between the two [here](http://stackoverflow.com/questions/8632586/macos-whats-the-difference-between-dev-tty-and-dev-cu). If there is a problem, open an issue describing your situation and let me look into it. 117 | 118 | ### Opening the Serial Port 119 | 120 | ```swift 121 | func openPort() 122 | func openPort(toReceive receive: Bool, andTransmit transmit: Bool) 123 | ``` 124 | Opening the port without any parameters will set the port to receive and transmit by default. You can still choose to receive-only, transmit-only or both. Will throw `PortError.mustReceiveOrTransmit` if you set both parameters to false. Can also throw `PortError.failedToOpen` and `PortError.invalidPath`. 125 | 126 | ### Set port settings 127 | 128 | ```swift 129 | serialPort.setSettings(receiveRate: .baud9600, transmitRate: .baud9600, minimumBytesToRead: 1) 130 | ``` 131 | The port settings call can be as simple as the above. For the baud rate, just supply both transmit and receive even if you are only intending to use one transfer direction. For example, transmitRate will be ignored if you specified `andTransmit : false` when opening the port. 132 | 133 | `minimumBytesToRead` determines how many characters the system must wait to receive before it will return from a [read()](https://linux.die.net/man/2/read) function. If in doubt, just put 1. 134 | 135 | This function has been defined with default settings as shown in the function definition. 136 | 137 | ```swift 138 | func setSettings(receiveRate: BaudRate, 139 | transmitRate: BaudRate, 140 | minimumBytesToRead: Int, 141 | timeout: Int = 0, /* 0 means wait indefinitely */ 142 | parityType: ParityType = .none, 143 | sendTwoStopBits: Bool = false, /* 1 stop bit is the default */ 144 | dataBitsSize: DataBitsSize = .bits8, 145 | useHardwareFlowControl: Bool = false, 146 | useSoftwareFlowControl: Bool = false, 147 | processOutput: Bool = false) 148 | ``` 149 | If the default settings do not suit you, just pass in extra parameters to override them. 150 | 151 | ### Reading data from port 152 | 153 | There are several functions you can use to read data. All functions here are blocking till the expected number of bytes has been received or a condition has been met. All functions can throw `PortError.mustBeOpen`. 154 | 155 | ```swift 156 | func readString(ofLength length: Int) throws -> String 157 | ``` 158 | This is the easiest to use if you are sending text data. Just provide how many bytes you expect to read. The result will then be returned as a typical Swift String. This function internally calls `readData()`. 159 | 160 | ```swift 161 | func readData(ofLength length: Int) throws -> Data 162 | ``` 163 | This function is if you intend to receive binary data. This function internally calls `readBytes()` 164 | 165 | ```swift 166 | func readBytes(into buffer: UnsafeMutablePointer, size: Int) throws -> Int 167 | ``` 168 | If you intend to play with unsafe pointers directly, this is the function for you! Will return the number of bytes read. Note that you are responsible for allocating the pointer before passing into this function then deallocate the pointer once you are done. 169 | 170 | ```swift 171 | func readLine() throws -> String 172 | ``` 173 | Read byte by byte till the newline character `\n` is encountered. A String containing the result so far will be returned without the newline character. This function internally calls `readUntilChar()`. Can throw `PortError.stringsMustBeUTF8`. 174 | 175 | ```swift 176 | func readUntilChar(_ terminator: CChar) throws -> String 177 | ``` 178 | Keep reading until the specified CChar is encountered. Return the string read so far without that value. 179 | 180 | ```swift 181 | func readByte() throws -> UInt8 182 | ``` 183 | Read only one byte. This works best if `minimumBytesToRead` has been set to `1` when opening the port. This function internally calls `readBytes()`. 184 | 185 | ```swift 186 | func readChar() throws -> UnicodeScalar 187 | ``` 188 | Read only one character. This works best if `minimumBytesToRead` has been set to `1` when opening the port. This function internally calls `readByte()`. 189 | 190 | ### Writing data to the port 191 | 192 | There are several functions you can use to write data. All functions here are blocking till all the data has been written. All functions can throw `PortError.mustBeOpen`. 193 | 194 | ```swift 195 | func writeString(_ string: String) throws -> Int 196 | ``` 197 | Most straightforward function, String in then transmit! Will return how many bytes actually written. Internally calls `writeData()` 198 | 199 | ```swift 200 | func writeData(_ data: Data) throws -> Int 201 | ``` 202 | Binary data in, then transmit! Will return how many bytes actually written. Internally calls `writeBytes()`. 203 | 204 | ```swift 205 | func writeBytes(from buffer: UnsafeMutablePointer, size: Int) throws -> Int 206 | ``` 207 | Function for those that want to mess with unsafe pointers. You have to specify how many bytes have to be written. Will return how many bytes actually written. 208 | 209 | ```swift 210 | func writeChar(_ character: UnicodeScalar) throws -> Int{ 211 | ``` 212 | Writes only one character. Will return `1` if successful. This function internally calls `writeString()`. Pull requests for a better way of doing this is appreciated. 213 | 214 | ### Closing the port 215 | 216 | Just do `serialPort.closePort()` to close the port once you are done using it. 217 | 218 | ## External References 219 | 220 | This library cannot be written without the amazing reference code I depended on. 221 | 222 | 1. [Xanthium's Serial Port Programming on Linux](http://xanthium.in/Serial-Port-Programming-on-Linux) 223 | 2. [Chrishey Drick's Reading data from Serial Port](https://chrisheydrick.com/2012/06/17/how-to-read-serial-data-from-an-arduino-in-linux-with-c-part-3/) 224 | -------------------------------------------------------------------------------- /Sources/SwiftSerial.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if os(Linux) 4 | public enum BaudRate { 5 | case baud0 6 | case baud50 7 | case baud75 8 | case baud110 9 | case baud134 10 | case baud150 11 | case baud200 12 | case baud300 13 | case baud600 14 | case baud1200 15 | case baud1800 16 | case baud2400 17 | case baud4800 18 | case baud9600 19 | case baud19200 20 | case baud38400 21 | case baud57600 22 | case baud115200 23 | case baud230400 24 | case baud460800 25 | case baud500000 26 | case baud576000 27 | case baud921600 28 | case baud1000000 29 | case baud1152000 30 | case baud1500000 31 | case baud2000000 32 | case baud2500000 33 | case baud3500000 34 | case baud4000000 35 | 36 | var speedValue: speed_t { 37 | switch self { 38 | case .baud0: 39 | return speed_t(B0) 40 | case .baud50: 41 | return speed_t(B50) 42 | case .baud75: 43 | return speed_t(B75) 44 | case .baud110: 45 | return speed_t(B110) 46 | case .baud134: 47 | return speed_t(B134) 48 | case .baud150: 49 | return speed_t(B150) 50 | case .baud200: 51 | return speed_t(B200) 52 | case .baud300: 53 | return speed_t(B300) 54 | case .baud600: 55 | return speed_t(B600) 56 | case .baud1200: 57 | return speed_t(B1200) 58 | case .baud1800: 59 | return speed_t(B1800) 60 | case .baud2400: 61 | return speed_t(B2400) 62 | case .baud4800: 63 | return speed_t(B4800) 64 | case .baud9600: 65 | return speed_t(B9600) 66 | case .baud19200: 67 | return speed_t(B19200) 68 | case .baud38400: 69 | return speed_t(B38400) 70 | case .baud57600: 71 | return speed_t(B57600) 72 | case .baud115200: 73 | return speed_t(B115200) 74 | case .baud230400: 75 | return speed_t(B230400) 76 | case .baud460800: 77 | return speed_t(B460800) 78 | case .baud500000: 79 | return speed_t(B500000) 80 | case .baud576000: 81 | return speed_t(B576000) 82 | case .baud921600: 83 | return speed_t(B921600) 84 | case .baud1000000: 85 | return speed_t(B1000000) 86 | case .baud1152000: 87 | return speed_t(B1152000) 88 | case .baud1500000: 89 | return speed_t(B1500000) 90 | case .baud2000000: 91 | return speed_t(B2000000) 92 | case .baud2500000: 93 | return speed_t(B2500000) 94 | case .baud3500000: 95 | return speed_t(B3500000) 96 | case .baud4000000: 97 | return speed_t(B4000000) 98 | } 99 | } 100 | } 101 | #elseif os(OSX) 102 | public enum BaudRate { 103 | case baud0 104 | case baud50 105 | case baud75 106 | case baud110 107 | case baud134 108 | case baud150 109 | case baud200 110 | case baud300 111 | case baud600 112 | case baud1200 113 | case baud1800 114 | case baud2400 115 | case baud4800 116 | case baud9600 117 | case baud19200 118 | case baud38400 119 | case baud57600 120 | case baud115200 121 | case baud230400 122 | 123 | var speedValue: speed_t { 124 | switch self { 125 | case .baud0: 126 | return speed_t(B0) 127 | case .baud50: 128 | return speed_t(B50) 129 | case .baud75: 130 | return speed_t(B75) 131 | case .baud110: 132 | return speed_t(B110) 133 | case .baud134: 134 | return speed_t(B134) 135 | case .baud150: 136 | return speed_t(B150) 137 | case .baud200: 138 | return speed_t(B200) 139 | case .baud300: 140 | return speed_t(B300) 141 | case .baud600: 142 | return speed_t(B600) 143 | case .baud1200: 144 | return speed_t(B1200) 145 | case .baud1800: 146 | return speed_t(B1800) 147 | case .baud2400: 148 | return speed_t(B2400) 149 | case .baud4800: 150 | return speed_t(B4800) 151 | case .baud9600: 152 | return speed_t(B9600) 153 | case .baud19200: 154 | return speed_t(B19200) 155 | case .baud38400: 156 | return speed_t(B38400) 157 | case .baud57600: 158 | return speed_t(B57600) 159 | case .baud115200: 160 | return speed_t(B115200) 161 | case .baud230400: 162 | return speed_t(B230400) 163 | } 164 | } 165 | } 166 | #endif 167 | 168 | public enum DataBitsSize { 169 | case bits5 170 | case bits6 171 | case bits7 172 | case bits8 173 | 174 | var flagValue: tcflag_t { 175 | switch self { 176 | case .bits5: 177 | return tcflag_t(CS5) 178 | case .bits6: 179 | return tcflag_t(CS6) 180 | case .bits7: 181 | return tcflag_t(CS7) 182 | case .bits8: 183 | return tcflag_t(CS8) 184 | } 185 | } 186 | 187 | } 188 | 189 | public enum ParityType { 190 | case none 191 | case even 192 | case odd 193 | 194 | var parityValue: tcflag_t { 195 | switch self { 196 | case .none: 197 | return 0 198 | case .even: 199 | return tcflag_t(PARENB) 200 | case .odd: 201 | return tcflag_t(PARENB | PARODD) 202 | } 203 | } 204 | } 205 | 206 | public enum PortError: Int32, Error { 207 | case failedToOpen = -1 // refer to open() 208 | case invalidPath 209 | case mustReceiveOrTransmit 210 | case mustBeOpen 211 | case stringsMustBeUTF8 212 | case unableToConvertByteToCharacter 213 | case deviceNotConnected 214 | } 215 | 216 | public class SerialPort { 217 | 218 | var path: String 219 | var fileDescriptor: Int32? 220 | 221 | public init(path: String) { 222 | self.path = path 223 | } 224 | 225 | public func openPort() throws { 226 | try openPort(toReceive: true, andTransmit: true) 227 | } 228 | 229 | public func openPort(toReceive receive: Bool, andTransmit transmit: Bool) throws { 230 | guard !path.isEmpty else { 231 | throw PortError.invalidPath 232 | } 233 | 234 | guard receive || transmit else { 235 | throw PortError.mustReceiveOrTransmit 236 | } 237 | 238 | var readWriteParam : Int32 239 | 240 | if receive && transmit { 241 | readWriteParam = O_RDWR 242 | } else if receive { 243 | readWriteParam = O_RDONLY 244 | } else if transmit { 245 | readWriteParam = O_WRONLY 246 | } else { 247 | fatalError() 248 | } 249 | 250 | #if os(Linux) 251 | fileDescriptor = open(path, readWriteParam | O_NOCTTY) 252 | #elseif os(OSX) 253 | fileDescriptor = open(path, readWriteParam | O_NOCTTY | O_EXLOCK) 254 | #endif 255 | 256 | // Throw error if open() failed 257 | if fileDescriptor == PortError.failedToOpen.rawValue { 258 | throw PortError.failedToOpen 259 | } 260 | } 261 | 262 | public func setSettings(receiveRate: BaudRate, 263 | transmitRate: BaudRate, 264 | minimumBytesToRead: Int, 265 | timeout: Int = 0, /* 0 means wait indefinitely */ 266 | parityType: ParityType = .none, 267 | sendTwoStopBits: Bool = false, /* 1 stop bit is the default */ 268 | dataBitsSize: DataBitsSize = .bits8, 269 | useHardwareFlowControl: Bool = false, 270 | useSoftwareFlowControl: Bool = false, 271 | processOutput: Bool = false) { 272 | 273 | guard let fileDescriptor = fileDescriptor else { 274 | return 275 | } 276 | 277 | 278 | // Set up the control structure 279 | var settings = termios() 280 | 281 | // Get options structure for the port 282 | tcgetattr(fileDescriptor, &settings) 283 | 284 | // Set baud rates 285 | cfsetispeed(&settings, receiveRate.speedValue) 286 | cfsetospeed(&settings, transmitRate.speedValue) 287 | 288 | // Enable parity (even/odd) if needed 289 | settings.c_cflag |= parityType.parityValue 290 | 291 | // Set stop bit flag 292 | if sendTwoStopBits { 293 | settings.c_cflag |= tcflag_t(CSTOPB) 294 | } else { 295 | settings.c_cflag &= ~tcflag_t(CSTOPB) 296 | } 297 | 298 | // Set data bits size flag 299 | settings.c_cflag &= ~tcflag_t(CSIZE) 300 | settings.c_cflag |= dataBitsSize.flagValue 301 | 302 | //Disable input mapping of CR to NL, mapping of NL into CR, and ignoring CR 303 | settings.c_iflag &= ~tcflag_t(ICRNL | INLCR | IGNCR) 304 | 305 | // Set hardware flow control flag 306 | #if os(Linux) 307 | if useHardwareFlowControl { 308 | settings.c_cflag |= tcflag_t(CRTSCTS) 309 | } else { 310 | settings.c_cflag &= ~tcflag_t(CRTSCTS) 311 | } 312 | #elseif os(OSX) 313 | if useHardwareFlowControl { 314 | settings.c_cflag |= tcflag_t(CRTS_IFLOW) 315 | settings.c_cflag |= tcflag_t(CCTS_OFLOW) 316 | } else { 317 | settings.c_cflag &= ~tcflag_t(CRTS_IFLOW) 318 | settings.c_cflag &= ~tcflag_t(CCTS_OFLOW) 319 | } 320 | #endif 321 | 322 | // Set software flow control flags 323 | let softwareFlowControlFlags = tcflag_t(IXON | IXOFF | IXANY) 324 | if useSoftwareFlowControl { 325 | settings.c_iflag |= softwareFlowControlFlags 326 | } else { 327 | settings.c_iflag &= ~softwareFlowControlFlags 328 | } 329 | 330 | // Turn on the receiver of the serial port, and ignore modem control lines 331 | settings.c_cflag |= tcflag_t(CREAD | CLOCAL) 332 | 333 | // Turn off canonical mode 334 | settings.c_lflag &= ~tcflag_t(ICANON | ECHO | ECHOE | ISIG) 335 | 336 | // Set output processing flag 337 | if processOutput { 338 | settings.c_oflag |= tcflag_t(OPOST) 339 | } else { 340 | settings.c_oflag &= ~tcflag_t(OPOST) 341 | } 342 | 343 | //Special characters 344 | //We do this as c_cc is a C-fixed array which is imported as a tuple in Swift. 345 | //To avoid hardcoding the VMIN or VTIME value to access the tuple value, we use the typealias instead 346 | #if os(Linux) 347 | typealias specialCharactersTuple = (VINTR: cc_t, VQUIT: cc_t, VERASE: cc_t, VKILL: cc_t, VEOF: cc_t, VTIME: cc_t, VMIN: cc_t, VSWTC: cc_t, VSTART: cc_t, VSTOP: cc_t, VSUSP: cc_t, VEOL: cc_t, VREPRINT: cc_t, VDISCARD: cc_t, VWERASE: cc_t, VLNEXT: cc_t, VEOL2: cc_t, spare1: cc_t, spare2: cc_t, spare3: cc_t, spare4: cc_t, spare5: cc_t, spare6: cc_t, spare7: cc_t, spare8: cc_t, spare9: cc_t, spare10: cc_t, spare11: cc_t, spare12: cc_t, spare13: cc_t, spare14: cc_t, spare15: cc_t) 348 | var specialCharacters: specialCharactersTuple = (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) // NCCS = 32 349 | #elseif os(OSX) 350 | typealias specialCharactersTuple = (VEOF: cc_t, VEOL: cc_t, VEOL2: cc_t, VERASE: cc_t, VWERASE: cc_t, VKILL: cc_t, VREPRINT: cc_t, spare1: cc_t, VINTR: cc_t, VQUIT: cc_t, VSUSP: cc_t, VDSUSP: cc_t, VSTART: cc_t, VSTOP: cc_t, VLNEXT: cc_t, VDISCARD: cc_t, VMIN: cc_t, VTIME: cc_t, VSTATUS: cc_t, spare: cc_t) 351 | var specialCharacters: specialCharactersTuple = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) // NCCS = 20 352 | #endif 353 | 354 | specialCharacters.VMIN = cc_t(minimumBytesToRead) 355 | specialCharacters.VTIME = cc_t(timeout) 356 | settings.c_cc = specialCharacters 357 | 358 | // Commit settings 359 | tcsetattr(fileDescriptor, TCSANOW, &settings) 360 | } 361 | 362 | public func closePort() { 363 | if let fileDescriptor = fileDescriptor { 364 | close(fileDescriptor) 365 | } 366 | fileDescriptor = nil 367 | } 368 | } 369 | 370 | // MARK: Receiving 371 | 372 | extension SerialPort { 373 | 374 | public func readBytes(into buffer: UnsafeMutablePointer, size: Int) throws -> Int { 375 | guard let fileDescriptor = fileDescriptor else { 376 | throw PortError.mustBeOpen 377 | } 378 | 379 | var s: stat = stat() 380 | fstat(fileDescriptor, &s) 381 | if s.st_nlink != 1 { 382 | throw PortError.deviceNotConnected 383 | } 384 | 385 | let bytesRead = read(fileDescriptor, buffer, size) 386 | return bytesRead 387 | } 388 | 389 | public func readData(ofLength length: Int) throws -> Data { 390 | let buffer = UnsafeMutablePointer.allocate(capacity: length) 391 | defer { 392 | buffer.deallocate() 393 | } 394 | 395 | let bytesRead = try readBytes(into: buffer, size: length) 396 | 397 | var data : Data 398 | 399 | if bytesRead > 0 { 400 | data = Data(bytes: buffer, count: bytesRead) 401 | } else { 402 | //This is to avoid the case where bytesRead can be negative causing problems allocating the Data buffer 403 | data = Data(bytes: buffer, count: 0) 404 | } 405 | 406 | return data 407 | } 408 | 409 | public func readString(ofLength length: Int) throws -> String { 410 | var remainingBytesToRead = length 411 | var result = "" 412 | 413 | while remainingBytesToRead > 0 { 414 | let data = try readData(ofLength: remainingBytesToRead) 415 | 416 | if let string = String(data: data, encoding: String.Encoding.utf8) { 417 | result += string 418 | remainingBytesToRead -= data.count 419 | } else { 420 | return result 421 | } 422 | } 423 | 424 | return result 425 | } 426 | 427 | public func readUntilChar(_ terminator: CChar) throws -> String { 428 | var data = Data() 429 | let buffer = UnsafeMutablePointer.allocate(capacity: 1) 430 | defer { 431 | buffer.deallocate() 432 | } 433 | 434 | while true { 435 | let bytesRead = try readBytes(into: buffer, size: 1) 436 | 437 | if bytesRead > 0 { 438 | if ( buffer[0] > 127) { 439 | throw PortError.unableToConvertByteToCharacter 440 | } 441 | let character = CChar(buffer[0]) 442 | 443 | if character == terminator { 444 | break 445 | } else { 446 | data.append(buffer, count: 1) 447 | } 448 | } 449 | } 450 | 451 | if let string = String(data: data, encoding: String.Encoding.utf8) { 452 | return string 453 | } else { 454 | throw PortError.stringsMustBeUTF8 455 | } 456 | } 457 | 458 | public func readLine() throws -> String { 459 | let newlineChar = CChar(10) // Newline/Line feed character `\n` is 10 460 | return try readUntilChar(newlineChar) 461 | } 462 | 463 | public func readByte() throws -> UInt8 { 464 | let buffer = UnsafeMutablePointer.allocate(capacity: 1) 465 | 466 | defer { 467 | buffer.deallocate() 468 | } 469 | 470 | while true { 471 | let bytesRead = try readBytes(into: buffer, size: 1) 472 | 473 | if bytesRead > 0 { 474 | return buffer[0] 475 | } 476 | } 477 | } 478 | 479 | public func readChar() throws -> UnicodeScalar { 480 | let byteRead = try readByte() 481 | let character = UnicodeScalar(byteRead) 482 | return character 483 | } 484 | 485 | } 486 | 487 | // MARK: Transmitting 488 | 489 | extension SerialPort { 490 | 491 | public func writeBytes(from buffer: UnsafeMutablePointer, size: Int) throws -> Int { 492 | guard let fileDescriptor = fileDescriptor else { 493 | throw PortError.mustBeOpen 494 | } 495 | 496 | let bytesWritten = write(fileDescriptor, buffer, size) 497 | return bytesWritten 498 | } 499 | 500 | public func writeData(_ data: Data) throws -> Int { 501 | let size = data.count 502 | let buffer = UnsafeMutablePointer.allocate(capacity: size) 503 | defer { 504 | buffer.deallocate() 505 | } 506 | 507 | data.copyBytes(to: buffer, count: size) 508 | 509 | let bytesWritten = try writeBytes(from: buffer, size: size) 510 | return bytesWritten 511 | } 512 | 513 | public func writeString(_ string: String) throws -> Int { 514 | guard let data = string.data(using: String.Encoding.utf8) else { 515 | throw PortError.stringsMustBeUTF8 516 | } 517 | 518 | return try writeData(data) 519 | } 520 | 521 | public func writeChar(_ character: UnicodeScalar) throws -> Int{ 522 | let stringEquiv = String(character) 523 | let bytesWritten = try writeString(stringEquiv) 524 | return bytesWritten 525 | } 526 | } 527 | --------------------------------------------------------------------------------