├── .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 | [](http://www.slideshare.net/yeokm1/a-science-project-swift-serial-chat)
14 |
15 | [](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 |
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 |
--------------------------------------------------------------------------------