├── .editorconfig ├── .spi.yml ├── archived ├── 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 └── README.original.md ├── .gitignore ├── Sources ├── SwiftSerial │ ├── ParityType.swift │ ├── PortError.swift │ ├── DataBitsSize.swift │ ├── PortMode.swift │ ├── BaudRate.swift │ ├── SerialPortDeprecated.swift │ └── SerialPort.swift └── SerialTerminal │ └── Terminal.swift ├── Package.resolved ├── Package.swift ├── LICENSE ├── README.md └── .swiftpm └── xcode └── xcshareddata └── xcschemes └── SerialTerminal.xcscheme /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = tab 3 | indent_size = 4 4 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [SwiftSerial] 5 | -------------------------------------------------------------------------------- /archived/first-slide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mredig/SwiftSerial/HEAD/archived/first-slide.png -------------------------------------------------------------------------------- /archived/Examples/SwiftSerialBinary/gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /archived/Examples/SwiftSerialExample/gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /archived/swift-serial-talk-slides.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mredig/SwiftSerial/HEAD/archived/swift-serial-talk-slides.pptx -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | .AppleDouble 5 | .LSOverride 6 | xcuserdata/ 7 | 8 | build/ 9 | DerivedData/ 10 | .build/ 11 | *.xcworkspacedata 12 | *.xcuserstate 13 | *.xcuserdatad 14 | .swiftpm/configuration/registries.json 15 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 16 | .netrc 17 | 18 | -------------------------------------------------------------------------------- /Sources/SwiftSerial/ParityType.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum ParityType { 4 | case none 5 | case even 6 | case odd 7 | 8 | var parityValue: tcflag_t { 9 | switch self { 10 | case .none: 11 | return 0 12 | case .even: 13 | return tcflag_t(PARENB) 14 | case .odd: 15 | return tcflag_t(PARENB | PARODD) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/SwiftSerial/PortError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum PortError: Int32, Error { 4 | case failedToOpen = -1 // refer to open() 5 | case invalidPath 6 | case mustReceiveOrTransmit 7 | case mustBeOpen 8 | case stringsMustBeUTF8 9 | case unableToConvertByteToCharacter 10 | case deviceNotConnected 11 | case instanceAlreadyOpen 12 | case invalidPort 13 | } 14 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "swift-argument-parser", 6 | "repositoryURL": "https://github.com/apple/swift-argument-parser.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "c8ed701b513cf5177118a175d85fbbbcd707ab41", 10 | "version": "1.3.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Sources/SwiftSerial/DataBitsSize.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum DataBitsSize { 4 | case bits5 5 | case bits6 6 | case bits7 7 | case bits8 8 | 9 | var flagValue: tcflag_t { 10 | switch self { 11 | case .bits5: 12 | return tcflag_t(CS5) 13 | case .bits6: 14 | return tcflag_t(CS6) 15 | case .bits7: 16 | return tcflag_t(CS7) 17 | case .bits8: 18 | return tcflag_t(CS8) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/SwiftSerial/PortMode.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum PortMode { 4 | case receive 5 | case transmit 6 | case receiveAndTransmit 7 | 8 | var receive: Bool { 9 | switch self { 10 | case .receive: 11 | true 12 | case .transmit: 13 | false 14 | case .receiveAndTransmit: 15 | true 16 | } 17 | } 18 | 19 | var transmit: Bool { 20 | switch self { 21 | case .receive: 22 | false 23 | case .transmit: 24 | true 25 | case .receiveAndTransmit: 26 | true 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /archived/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 | -------------------------------------------------------------------------------- /archived/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 | -------------------------------------------------------------------------------- /archived/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 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 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 | platforms: [ 9 | .macOS(.v10_15), 10 | ], 11 | products: [ 12 | .library(name: "SwiftSerial", targets: ["SwiftSerial"]), 13 | .executable(name: "SerialTerminal", targets: ["SerialTerminal"]) 14 | ], 15 | dependencies: [ 16 | .package(url: "https://github.com/apple/swift-argument-parser.git", .upToNextMinor(from: "1.3.0")), 17 | ], 18 | targets: [ 19 | .target( 20 | name: "SwiftSerial", 21 | dependencies: [] 22 | ), 23 | .executableTarget( 24 | name: "SerialTerminal", 25 | dependencies: [ 26 | "SwiftSerial", 27 | .product(name: "ArgumentParser", package: "swift-argument-parser"), 28 | ] 29 | ) 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /Sources/SerialTerminal/Terminal.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftSerial 3 | import ArgumentParser 4 | 5 | private var readTask: Task? 6 | 7 | @main 8 | struct Terminal: AsyncParsableCommand { 9 | @Argument(help: "Path to port (aka /dev/cu.serialsomethingorother)") 10 | var path: String 11 | 12 | @Argument(help: "Baud Rate", transform: { 13 | let value = UInt($0) ?? 1 14 | return try BaudRate(value) 15 | }) 16 | var baudRate: BaudRate 17 | 18 | private var writeBuffer: String = "" 19 | 20 | func run() async throws { 21 | let serialPort = SerialPort(path: path) 22 | try serialPort.openPort() 23 | try serialPort.setSettings(baudRateSetting: .symmetrical(baudRate), minimumBytesToRead: 1) 24 | 25 | readTask = Task { 26 | let lines = try serialPort.asyncLines() 27 | 28 | for await line in lines { 29 | print(line, terminator: "") 30 | } 31 | } 32 | 33 | while let line = readLine(strippingNewline: false) { 34 | _ = try serialPort.writeString(line) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Michael Redig 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 | -------------------------------------------------------------------------------- /archived/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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift Serial 2 | 3 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fmredig%2FSwiftSerial%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/mredig/SwiftSerial) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fmredig%2FSwiftSerial%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/mredig/SwiftSerial) 4 | 5 | This project began its life as yeokm1's [SwiftSerial](https://github.com/yeokm1/SwiftSerial). He has since archived the project and was kind enough to link this fork going forward. 6 | 7 | ### Getting started 8 | 9 | ```swift 10 | 11 | import SwiftSerial 12 | 13 | ... 14 | 15 | // setup 16 | let serialPort = SerialPort(path: "/dev/cu.usbmodem1234") // you'll need to find the correct device on your own, but this is what it will resemble on a mac 17 | try serialPort.openPort() 18 | 19 | try serialPort.setSettings( 20 | baudRateSetting: .symmetrical(.baud115200), 21 | minimumBytesToRead: 1) 22 | 23 | 24 | // read output 25 | Task { 26 | let readStream = try serialPort.asyncLines() 27 | 28 | for await line in readStream { 29 | print(line, terminator: "") 30 | } 31 | } 32 | 33 | 34 | // send data 35 | try serialPort.writeString("foo") 36 | // or 37 | try serialPort.writeData(Data([1,2,3,4])) 38 | ``` 39 | 40 | See the demo CLI app `SwiftTerminal` for a working example. 41 | 42 | ### SPM Import 43 | ```swift 44 | .package(url: "https://github.com/mredig/SwiftSerial", .upToNextMinor("1.0.0") 45 | ``` 46 | 47 | ### What's New? 48 | * Modernized and Swiftier syntax 49 | * TABS! 50 | * Modular indentation style, allowing for anyone to read the code however it reads best to them 51 | * Broke separate symbols into their own files 52 | * Monitoring output and delivering via AsyncStream for reading instead of the old polling, or dare I say, omniscience, 53 | method, where you need to know exactly how many bytes or lines to read. 54 | * Thread safety 55 | * BaudRate has UInt initializer 56 | * Added `SwiftTerminal` demo to connect and interface with a serial connection 57 | * I kept the original methods that I changed around, but marked as deprecated. I intend to eventually remove them, but I don't want to disrupt anyone relying on this in the meantime. 58 | 59 | ##### What needs fixing? 60 | * I made these changes before I had a better grasp of how async/await streams worked. Turns out I used them wrong! All the AsyncStream types need to create a new copy of the stream instead of returning the same "instance". 61 | * Swift6 concurrency support 62 | -------------------------------------------------------------------------------- /archived/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 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/SerialTerminal.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 44 | 50 | 51 | 52 | 53 | 56 | 57 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /archived/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 | -------------------------------------------------------------------------------- /Sources/SwiftSerial/BaudRate.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum BaudRate { 4 | case baud0 5 | case baud50 6 | case baud75 7 | case baud110 8 | case baud134 9 | case baud150 10 | case baud200 11 | case baud300 12 | case baud600 13 | case baud1200 14 | case baud1800 15 | case baud2400 16 | case baud4800 17 | case baud9600 18 | case baud19200 19 | case baud38400 20 | case baud57600 21 | case baud115200 22 | case baud230400 23 | #if os(Linux) 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 | #endif 36 | 37 | public init(_ value: UInt) throws { 38 | switch value { 39 | case 0: 40 | self = .baud0 41 | case 50: 42 | self = .baud50 43 | case 75: 44 | self = .baud75 45 | case 110: 46 | self = .baud110 47 | case 134: 48 | self = .baud134 49 | case 150: 50 | self = .baud150 51 | case 200: 52 | self = .baud200 53 | case 300: 54 | self = .baud300 55 | case 600: 56 | self = .baud600 57 | case 1200: 58 | self = .baud1200 59 | case 1800: 60 | self = .baud1800 61 | case 2400: 62 | self = .baud2400 63 | case 4800: 64 | self = .baud4800 65 | case 9600: 66 | self = .baud9600 67 | case 19200: 68 | self = .baud19200 69 | case 38400: 70 | self = .baud38400 71 | case 57600: 72 | self = .baud57600 73 | case 115200: 74 | self = .baud115200 75 | case 230400: 76 | self = .baud230400 77 | #if os(Linux) 78 | case 460800: 79 | self = .baud460800 80 | case 500000: 81 | self = .baud500000 82 | case 576000: 83 | self = .baud576000 84 | case 921600: 85 | self = .baud921600 86 | case 1000000: 87 | self = .baud1000000 88 | case 1152000: 89 | self = .baud1152000 90 | case 1500000: 91 | self = .baud1500000 92 | case 2000000: 93 | self = .baud2000000 94 | case 2500000: 95 | self = .baud2500000 96 | case 3500000: 97 | self = .baud3500000 98 | case 4000000: 99 | self = .baud4000000 100 | #endif 101 | default: 102 | throw PortError.invalidPort 103 | } 104 | } 105 | 106 | var speedValue: speed_t { 107 | switch self { 108 | case .baud0: 109 | return speed_t(B0) 110 | case .baud50: 111 | return speed_t(B50) 112 | case .baud75: 113 | return speed_t(B75) 114 | case .baud110: 115 | return speed_t(B110) 116 | case .baud134: 117 | return speed_t(B134) 118 | case .baud150: 119 | return speed_t(B150) 120 | case .baud200: 121 | return speed_t(B200) 122 | case .baud300: 123 | return speed_t(B300) 124 | case .baud600: 125 | return speed_t(B600) 126 | case .baud1200: 127 | return speed_t(B1200) 128 | case .baud1800: 129 | return speed_t(B1800) 130 | case .baud2400: 131 | return speed_t(B2400) 132 | case .baud4800: 133 | return speed_t(B4800) 134 | case .baud9600: 135 | return speed_t(B9600) 136 | case .baud19200: 137 | return speed_t(B19200) 138 | case .baud38400: 139 | return speed_t(B38400) 140 | case .baud57600: 141 | return speed_t(B57600) 142 | case .baud115200: 143 | return speed_t(B115200) 144 | case .baud230400: 145 | return speed_t(B230400) 146 | #if os(Linux) 147 | case .baud460800: 148 | return speed_t(B460800) 149 | case .baud500000: 150 | return speed_t(B500000) 151 | case .baud576000: 152 | return speed_t(B576000) 153 | case .baud921600: 154 | return speed_t(B921600) 155 | case .baud1000000: 156 | return speed_t(B1000000) 157 | case .baud1152000: 158 | return speed_t(B1152000) 159 | case .baud1500000: 160 | return speed_t(B1500000) 161 | case .baud2000000: 162 | return speed_t(B2000000) 163 | case .baud2500000: 164 | return speed_t(B2500000) 165 | case .baud3500000: 166 | return speed_t(B3500000) 167 | case .baud4000000: 168 | return speed_t(B4000000) 169 | #endif 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /Sources/SwiftSerial/SerialPortDeprecated.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension SerialPort { 4 | @available(*, deprecated, message: "Use `setSettings(baudRateSetting:.....)` instead") 5 | public func setSettings( 6 | receiveRate: BaudRate, 7 | transmitRate: BaudRate, 8 | minimumBytesToRead: Int, 9 | timeout: Int = 0, /* 0 means wait indefinitely */ 10 | parityType: ParityType = .none, 11 | sendTwoStopBits: Bool = false, /* 1 stop bit is the default */ 12 | dataBitsSize: DataBitsSize = .bits8, 13 | useHardwareFlowControl: Bool = false, 14 | useSoftwareFlowControl: Bool = false, 15 | processOutput: Bool = false 16 | ) throws { 17 | try setSettings( 18 | baudRateSetting: .asymmetrical(receiveRate: receiveRate, transmitRate: transmitRate), 19 | minimumBytesToRead: minimumBytesToRead, 20 | timeout: timeout, 21 | parityType: parityType, 22 | sendTwoStopBits: sendTwoStopBits, 23 | dataBitsSize: dataBitsSize, 24 | useHardwareFlowControl: useHardwareFlowControl, 25 | useSoftwareFlowControl: useSoftwareFlowControl, 26 | processOutput: processOutput) 27 | } 28 | 29 | @available(*, deprecated, message: "Use `open(portMode:)` instead") 30 | public func openPort(toReceive receive: Bool, andTransmit transmit: Bool) throws { 31 | switch (receive, transmit) { 32 | case (true, false): 33 | try openPort(portMode: .receive) 34 | case (false, true): 35 | try openPort(portMode: .transmit) 36 | case (true, true): 37 | try openPort(portMode: .receiveAndTransmit) 38 | case (false, false): 39 | throw PortError.mustReceiveOrTransmit 40 | } 41 | } 42 | 43 | @available(*, deprecated, message: "Use async reading methods") 44 | public func readBytes(into buffer: UnsafeMutablePointer, size: Int) throws -> Int { 45 | guard let fileDescriptor = fileDescriptor else { 46 | throw PortError.mustBeOpen 47 | } 48 | 49 | var s: stat = stat() 50 | fstat(fileDescriptor, &s) 51 | if s.st_nlink != 1 { 52 | throw PortError.deviceNotConnected 53 | } 54 | 55 | let bytesRead = read(fileDescriptor, buffer, size) 56 | return bytesRead 57 | } 58 | 59 | @available(*, deprecated, message: "Use async reading methods") 60 | public func readData(ofLength length: Int) throws -> Data { 61 | let buffer = UnsafeMutablePointer.allocate(capacity: length) 62 | defer { 63 | buffer.deallocate() 64 | } 65 | 66 | let bytesRead = try readBytes(into: buffer, size: length) 67 | 68 | var data : Data 69 | 70 | if bytesRead > 0 { 71 | data = Data(bytes: buffer, count: bytesRead) 72 | } else { 73 | //This is to avoid the case where bytesRead can be negative causing problems allocating the Data buffer 74 | data = Data(bytes: buffer, count: 0) 75 | } 76 | 77 | return data 78 | } 79 | 80 | @available(*, deprecated, message: "Use async reading methods") 81 | public func readString(ofLength length: Int) throws -> String { 82 | var remainingBytesToRead = length 83 | var result = "" 84 | 85 | while remainingBytesToRead > 0 { 86 | let data = try readData(ofLength: remainingBytesToRead) 87 | 88 | if let string = String(data: data, encoding: String.Encoding.utf8) { 89 | result += string 90 | remainingBytesToRead -= data.count 91 | } else { 92 | return result 93 | } 94 | } 95 | 96 | return result 97 | } 98 | 99 | @available(*, deprecated, message: "Use async reading methods") 100 | public func readUntilChar(_ terminator: CChar) throws -> String { 101 | var data = Data() 102 | let buffer = UnsafeMutablePointer.allocate(capacity: 1) 103 | defer { 104 | buffer.deallocate() 105 | } 106 | 107 | while true { 108 | let bytesRead = try readBytes(into: buffer, size: 1) 109 | 110 | if bytesRead > 0 { 111 | if ( buffer[0] > 127) { 112 | throw PortError.unableToConvertByteToCharacter 113 | } 114 | let character = CChar(buffer[0]) 115 | 116 | if character == terminator { 117 | break 118 | } else { 119 | data.append(buffer, count: 1) 120 | } 121 | } 122 | } 123 | 124 | if let string = String(data: data, encoding: String.Encoding.utf8) { 125 | return string 126 | } else { 127 | throw PortError.stringsMustBeUTF8 128 | } 129 | } 130 | 131 | @available(*, deprecated, message: "Use async reading methods") 132 | public func readLine() throws -> String { 133 | let newlineChar = CChar(10) // Newline/Line feed character `\n` is 10 134 | return try readUntilChar(newlineChar) 135 | } 136 | 137 | @available(*, deprecated, message: "Use async reading methods") 138 | public func readByte() throws -> UInt8 { 139 | let buffer = UnsafeMutablePointer.allocate(capacity: 1) 140 | 141 | defer { 142 | buffer.deallocate() 143 | } 144 | 145 | while true { 146 | let bytesRead = try readBytes(into: buffer, size: 1) 147 | 148 | if bytesRead > 0 { 149 | return buffer[0] 150 | } 151 | } 152 | } 153 | 154 | @available(*, deprecated, message: "Use async reading methods") 155 | public func readChar() throws -> UnicodeScalar { 156 | let byteRead = try readByte() 157 | let character = UnicodeScalar(byteRead) 158 | return character 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /archived/README.original.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/SerialPort.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// SerialPort acts as the handle to manipulate and read the serial input and output. 4 | public class SerialPort { 5 | 6 | /// Path to the system device the serial port resides at. For example, `/dev/cu.serialsoemthingorother` 7 | var path: String 8 | /// Storage for the file descriptor of the path. Probably don't edit this. (I should probably make private, but I don't know if there's anything relying on it) 9 | var fileDescriptor: Int32? 10 | 11 | private var isOpen: Bool { fileDescriptor != nil } 12 | 13 | private var pollSource: DispatchSourceRead? 14 | private var readDataStream: AsyncStream? 15 | private var readBytesStream: AsyncStream? 16 | private var readLinesStream: AsyncStream? 17 | 18 | private let lock = NSLock() 19 | 20 | /// Create a new `SerialPort` object 21 | /// - Parameter path: Path to the system device the serial port resides at. For example, `/dev/cu.serialsoemthingorother` 22 | public init(path: String) { 23 | self.path = path 24 | } 25 | 26 | /// Opens and establishes the connection with the serial port. 27 | public func openPort(portMode: PortMode = .receiveAndTransmit) throws(PortError) { 28 | lock.lock() 29 | defer { lock.unlock() } 30 | guard !path.isEmpty else { throw PortError.invalidPath } 31 | guard isOpen == false else { throw PortError.instanceAlreadyOpen } 32 | 33 | let readWriteParam: Int32 34 | 35 | switch portMode { 36 | case .receive: 37 | readWriteParam = O_RDONLY 38 | case .transmit: 39 | readWriteParam = O_WRONLY 40 | case .receiveAndTransmit: 41 | readWriteParam = O_RDWR 42 | } 43 | 44 | #if os(Linux) 45 | fileDescriptor = open(path, readWriteParam | O_NOCTTY) 46 | #elseif os(OSX) 47 | fileDescriptor = open(path, readWriteParam | O_NOCTTY | O_EXLOCK) 48 | #endif 49 | 50 | // Throw error if open() failed 51 | if fileDescriptor == PortError.failedToOpen.rawValue { 52 | throw PortError.failedToOpen 53 | } 54 | 55 | guard 56 | portMode.receive, 57 | let fileDescriptor 58 | else { return } 59 | let pollSource = DispatchSource.makeReadSource(fileDescriptor: fileDescriptor, queue: .global(qos: .default)) 60 | let stream = AsyncStream { continuation in 61 | pollSource.setEventHandler { [lock] in 62 | lock.lock() 63 | defer { lock.unlock() } 64 | 65 | let bufferSize = 1024 66 | let buffer = UnsafeMutableRawPointer 67 | .allocate(byteCount: bufferSize, alignment: 8) 68 | let bytesRead = read(fileDescriptor, buffer, bufferSize) 69 | guard bytesRead > 0 else { return } 70 | let bytes = Data(bytes: buffer, count: bytesRead) 71 | continuation.yield(bytes) 72 | } 73 | 74 | pollSource.setCancelHandler { 75 | continuation.finish() 76 | } 77 | } 78 | pollSource.resume() 79 | self.pollSource = pollSource 80 | self.readDataStream = stream 81 | } 82 | 83 | /// Value for the BaudRate of a connection 84 | public struct BaudRateSetting { 85 | public let receiveRate: BaudRate 86 | public let transmitRate: BaudRate 87 | 88 | public init(receiveRate: BaudRate, transmitRate: BaudRate) { 89 | self.receiveRate = receiveRate 90 | self.transmitRate = transmitRate 91 | } 92 | 93 | public static func symmetrical(_ baudRate: BaudRate) -> BaudRateSetting { 94 | Self(receiveRate: baudRate, transmitRate: baudRate) 95 | } 96 | 97 | public static func asymmetrical(receiveRate: BaudRate, transmitRate: BaudRate) -> BaudRateSetting { 98 | Self(receiveRate: receiveRate, transmitRate: transmitRate) 99 | } 100 | } 101 | 102 | /// Sets the settings of a serial port connection. 103 | /// 104 | /// Many of these values should be referenced from the underlying C calls and structs: 105 | /// * `tcsetattr` 106 | /// * `termios` 107 | /// * `cfsetispeed` 108 | /// * `cfsetospeed` 109 | /// * `cc_t` 110 | /// * `tcflag_t` 111 | /// 112 | /// - Parameters: 113 | /// - baudRateSetting: Speed/baud rate of the connection 114 | /// - minimumBytesToRead: The minimum bytes to read 115 | /// - timeout: `0` means indefinite. 116 | /// - parityType: parity type 117 | /// - sendTwoStopBits: defaults to false. `1` stop bit is used on false 118 | /// - dataBitsSize: defaults to `.bits8` 119 | /// - useHardwareFlowControl: defaults to `false` 120 | /// - useSoftwareFlowControl: defaults to `false` 121 | /// - processOutput: defaults to `false` 122 | public func setSettings( 123 | baudRateSetting: BaudRateSetting, 124 | minimumBytesToRead: Int, 125 | timeout: Int = 0, /* 0 means wait indefinitely */ 126 | parityType: ParityType = .none, 127 | sendTwoStopBits: Bool = false, /* 1 stop bit is the default */ 128 | dataBitsSize: DataBitsSize = .bits8, 129 | useHardwareFlowControl: Bool = false, 130 | useSoftwareFlowControl: Bool = false, 131 | processOutput: Bool = false 132 | ) throws { 133 | lock.lock() 134 | defer { lock.unlock() } 135 | guard let fileDescriptor = fileDescriptor else { 136 | throw PortError.mustBeOpen 137 | } 138 | 139 | // Set up the control structure 140 | var settings = termios() 141 | 142 | // Get options structure for the port 143 | tcgetattr(fileDescriptor, &settings) 144 | 145 | // Set baud rates 146 | cfsetispeed(&settings, baudRateSetting.receiveRate.speedValue) 147 | cfsetospeed(&settings, baudRateSetting.transmitRate.speedValue) 148 | 149 | // Enable parity (even/odd) if needed 150 | settings.c_cflag |= parityType.parityValue 151 | 152 | // Set stop bit flag 153 | if sendTwoStopBits { 154 | settings.c_cflag |= tcflag_t(CSTOPB) 155 | } else { 156 | settings.c_cflag &= ~tcflag_t(CSTOPB) 157 | } 158 | 159 | // Set data bits size flag 160 | settings.c_cflag &= ~tcflag_t(CSIZE) 161 | settings.c_cflag |= dataBitsSize.flagValue 162 | 163 | //Disable input mapping of CR to NL, mapping of NL into CR, and ignoring CR 164 | settings.c_iflag &= ~tcflag_t(ICRNL | INLCR | IGNCR) 165 | 166 | // Set hardware flow control flag 167 | #if os(Linux) 168 | if useHardwareFlowControl { 169 | settings.c_cflag |= tcflag_t(CRTSCTS) 170 | } else { 171 | settings.c_cflag &= ~tcflag_t(CRTSCTS) 172 | } 173 | #elseif os(OSX) 174 | if useHardwareFlowControl { 175 | settings.c_cflag |= tcflag_t(CRTS_IFLOW) 176 | settings.c_cflag |= tcflag_t(CCTS_OFLOW) 177 | } else { 178 | settings.c_cflag &= ~tcflag_t(CRTS_IFLOW) 179 | settings.c_cflag &= ~tcflag_t(CCTS_OFLOW) 180 | } 181 | #endif 182 | 183 | // Set software flow control flags 184 | let softwareFlowControlFlags = tcflag_t(IXON | IXOFF | IXANY) 185 | if useSoftwareFlowControl { 186 | settings.c_iflag |= softwareFlowControlFlags 187 | } else { 188 | settings.c_iflag &= ~softwareFlowControlFlags 189 | } 190 | 191 | // Turn on the receiver of the serial port, and ignore modem control lines 192 | settings.c_cflag |= tcflag_t(CREAD | CLOCAL) 193 | 194 | // Turn off canonical mode 195 | settings.c_lflag &= ~tcflag_t(ICANON | ECHO | ECHOE | ISIG) 196 | 197 | // Set output processing flag 198 | if processOutput { 199 | settings.c_oflag |= tcflag_t(OPOST) 200 | } else { 201 | settings.c_oflag &= ~tcflag_t(OPOST) 202 | } 203 | 204 | //Special characters 205 | //We do this as c_cc is a C-fixed array which is imported as a tuple in Swift. 206 | //To avoid hardcoding the VMIN or VTIME value to access the tuple value, we use the typealias instead 207 | #if os(Linux) 208 | 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) 209 | 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 210 | #elseif os(OSX) 211 | 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) 212 | 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 213 | #endif 214 | 215 | specialCharacters.VMIN = cc_t(minimumBytesToRead) 216 | specialCharacters.VTIME = cc_t(timeout) 217 | settings.c_cc = specialCharacters 218 | 219 | // Commit settings 220 | tcsetattr(fileDescriptor, TCSANOW, &settings) 221 | } 222 | 223 | /// Closes the port 224 | public func closePort() { 225 | lock.lock() 226 | defer { lock.unlock() } 227 | pollSource?.cancel() 228 | pollSource = nil 229 | 230 | readDataStream = nil 231 | readBytesStream = nil 232 | readLinesStream = nil 233 | 234 | if let fileDescriptor = fileDescriptor { 235 | close(fileDescriptor) 236 | } 237 | fileDescriptor = nil 238 | } 239 | } 240 | 241 | // MARK: Receiving 242 | extension SerialPort { 243 | /// Retrieves the `AsyncStream`. Don't run this more than once as streams can only produce output for a single subscriber. 244 | public func asyncData() throws(PortError) -> AsyncStream { 245 | guard 246 | isOpen, 247 | let readDataStream 248 | else { 249 | throw PortError.mustBeOpen 250 | } 251 | 252 | return readDataStream 253 | } 254 | 255 | /// Retrieves the `AsyncStream`. Don't run this more than once as streams can only produce output for a single subscriber. 256 | public func asyncBytes() throws -> AsyncStream { 257 | guard 258 | isOpen, 259 | let readDataStream 260 | else { 261 | throw PortError.mustBeOpen 262 | } 263 | 264 | if let existing = readBytesStream { 265 | return existing 266 | } else { 267 | let new = AsyncStream { continuation in 268 | Task { 269 | for try await data in readDataStream { 270 | for byte in data { 271 | continuation.yield(byte) 272 | } 273 | } 274 | continuation.finish() 275 | } 276 | } 277 | readBytesStream = new 278 | return new 279 | } 280 | } 281 | 282 | /// Retrieves the `AsyncStream`. Don't run this more than once as streams can only produce output for a single subscriber. 283 | public func asyncLines() throws -> AsyncStream { 284 | guard isOpen else { throw PortError.mustBeOpen } 285 | 286 | if let existing = readLinesStream { 287 | return existing 288 | } else { 289 | let byteStream = try asyncBytes() 290 | let new = AsyncStream { continuation in 291 | Task { 292 | var accumulator = Data() 293 | for try await byte in byteStream { 294 | accumulator.append(byte) 295 | 296 | guard 297 | UnicodeScalar(byte) == "\n".unicodeScalars.first 298 | else { continue } 299 | 300 | defer { accumulator = Data() } 301 | guard 302 | let string = String(data: accumulator, encoding: .utf8) 303 | else { 304 | continuation.yield("Error: Non string data. Perhaps you wanted data or bytes output?") 305 | continue 306 | } 307 | continuation.yield(string) 308 | } 309 | continuation.finish() 310 | } 311 | } 312 | readLinesStream = new 313 | return new 314 | } 315 | } 316 | } 317 | 318 | // MARK: Transmitting 319 | extension SerialPort { 320 | /// Writes to the `SerialPort`. You can also think of this as sending data. 321 | /// - Parameters: 322 | /// - buffer: pointer to the raw memory being sent 323 | /// - size: how many bytes to read from `buffer` 324 | /// - Returns: Count of bytes written 325 | public func writeBytes(from buffer: UnsafeMutablePointer, size: Int) throws -> Int { 326 | lock.lock() 327 | defer { lock.unlock() } 328 | guard let fileDescriptor = fileDescriptor else { 329 | throw PortError.mustBeOpen 330 | } 331 | 332 | let bytesWritten = write(fileDescriptor, buffer, size) 333 | return bytesWritten 334 | } 335 | 336 | /// Writes to the `SerialPort`. You can also think of this as sending data. 337 | /// - Parameter data: The chunk of data you want to send. 338 | /// - Returns: Count of bytes written 339 | public func writeData(_ data: Data) throws -> Int { 340 | let size = data.count 341 | let buffer = UnsafeMutablePointer.allocate(capacity: size) 342 | defer { 343 | buffer.deallocate() 344 | } 345 | 346 | data.copyBytes(to: buffer, count: size) 347 | 348 | let bytesWritten = try writeBytes(from: buffer, size: size) 349 | return bytesWritten 350 | } 351 | 352 | /// Writes to the `SerialPort`. You can also think of this as sending data. 353 | /// - Parameter string: String of characters you wish to send. 354 | /// - Returns: Count of bytes written. 355 | public func writeString(_ string: String) throws -> Int { 356 | guard let data = string.data(using: String.Encoding.utf8) else { 357 | throw PortError.stringsMustBeUTF8 358 | } 359 | 360 | return try writeData(data) 361 | } 362 | 363 | /// Writes to the `SerialPort`. You can also think of this as sending data. 364 | /// - Parameter character: The single `UnicodeScalar` to write to the buffer. 365 | /// - Returns: Count of byte(s) written. 366 | public func writeChar(_ character: UnicodeScalar) throws -> Int{ 367 | let stringEquiv = String(character) 368 | let bytesWritten = try writeString(stringEquiv) 369 | return bytesWritten 370 | } 371 | } 372 | --------------------------------------------------------------------------------