diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..02c0875 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..3c26705 --- /dev/null +++ b/Package.swift @@ -0,0 +1,5 @@ +import PackageDescription + +let package = Package( + name: "SwiftSerial" +) diff --git a/Sources/SwiftSerial.swift b/Sources/SwiftSerial.swift new file mode 100644 index 0000000..a76c550 --- /dev/null +++ b/Sources/SwiftSerial.swift @@ -0,0 +1,443 @@ +import Foundation + +#if os(Linux) +public enum BaudRate { + case baud0 + case baud50 + case baud75 + case baud110 + case baud134 + case baud150 + case baud200 + case baud300 + case baud600 + case baud1200 + case baud1800 + case baud2400 + case baud4800 + case baud9600 + case baud19200 + case baud38400 + case baud57600 + case baud115200 + case baud230400 + case baud460800 + case baud500000 + case baud576000 + case baud921600 + case baud1000000 + case baud1152000 + case baud1500000 + case baud2000000 + case baud2500000 + case baud3500000 + case baud4000000 + + var speedValue: speed_t { + switch self { + case .baud0: + return speed_t(B0) + case .baud50: + return speed_t(B50) + case .baud75: + return speed_t(B75) + case .baud110: + return speed_t(B110) + case .baud134: + return speed_t(B134) + case .baud150: + return speed_t(B150) + case .baud200: + return speed_t(B200) + case .baud300: + return speed_t(B300) + case .baud600: + return speed_t(B600) + case .baud1200: + return speed_t(B1200) + case .baud1800: + return speed_t(B1800) + case .baud2400: + return speed_t(B2400) + case .baud4800: + return speed_t(B4800) + case .baud9600: + return speed_t(B9600) + case .baud19200: + return speed_t(B19200) + case .baud38400: + return speed_t(B38400) + case .baud57600: + return speed_t(B57600) + case .baud115200: + return speed_t(B115200) + case .baud230400: + return speed_t(B230400) + case .baud460800: + return speed_t(B460800) + case .baud500000: + return speed_t(B500000) + case .baud576000: + return speed_t(B576000) + case .baud921600: + return speed_t(B921600) + case .baud1000000: + return speed_t(B1000000) + case .baud1152000: + return speed_t(B1152000) + case .baud1500000: + return speed_t(B1500000) + case .baud2000000: + return speed_t(B2000000) + case .baud2500000: + return speed_t(B2500000) + case .baud3500000: + return speed_t(B3500000) + case .baud4000000: + return speed_t(B4000000) + } + } +} +#elseif os(OSX) +public enum BaudRate { + case baud0 + case baud50 + case baud75 + case baud110 + case baud134 + case baud150 + case baud200 + case baud300 + case baud600 + case baud1200 + case baud1800 + case baud2400 + case baud4800 + case baud9600 + case baud19200 + case baud38400 + case baud57600 + case baud115200 + case baud230400 + + var speedValue: speed_t { + switch self { + case .baud0: + return speed_t(B0) + case .baud50: + return speed_t(B50) + case .baud75: + return speed_t(B75) + case .baud110: + return speed_t(B110) + case .baud134: + return speed_t(B134) + case .baud150: + return speed_t(B150) + case .baud200: + return speed_t(B200) + case .baud300: + return speed_t(B300) + case .baud600: + return speed_t(B600) + case .baud1200: + return speed_t(B1200) + case .baud1800: + return speed_t(B1800) + case .baud2400: + return speed_t(B2400) + case .baud4800: + return speed_t(B4800) + case .baud9600: + return speed_t(B9600) + case .baud19200: + return speed_t(B19200) + case .baud38400: + return speed_t(B38400) + case .baud57600: + return speed_t(B57600) + case .baud115200: + return speed_t(B115200) + case .baud230400: + return speed_t(B230400) + } + } +} +#endif + +public enum DataBitsSize { + case bits5 + case bits6 + case bits7 + case bits8 + + var flagValue: tcflag_t { + switch self { + case .bits5: + return tcflag_t(CS5) + case .bits6: + return tcflag_t(CS6) + case .bits7: + return tcflag_t(CS7) + case .bits8: + return tcflag_t(CS8) + } + } +} + +public enum PortError: Int32, Error { + case failedToOpen = -1 // refer to open() + case invalidPath + case mustReceiveOrTransmit + case mustBeOpen + case stringsMustBeUTF8 +} + +public class SerialPort { + + var path: String + var fileDescriptor: Int32? + + public init(path: String) { + self.path = path + } + + public func openPort() throws { + try openPort(toReceive: true, andTransmit: true) + } + + public func openPort(toReceive receive: Bool, andTransmit transmit: Bool) throws { + guard !path.isEmpty else { + throw PortError.invalidPath + } + + guard receive || transmit else { + throw PortError.mustReceiveOrTransmit + } + + if receive && transmit { + fileDescriptor = open(path, O_RDWR | O_NOCTTY) + } else if receive { + fileDescriptor = open(path, O_RDONLY | O_NOCTTY) + } else if transmit { + fileDescriptor = open(path, O_WRONLY | O_NOCTTY) + } else { + fatalError() + } + + // Throw error if open() failed + if fileDescriptor == PortError.failedToOpen.rawValue { + throw PortError.failedToOpen + } + } + + public func setSettings(receiveRate: BaudRate, + transmitRate: BaudRate, + minimumBytesToRead: Int, + timeout: Int = 0, /* 0 means wait indefinitely */ + enableParity: Bool = false, + sendTwoStopBits: Bool = false, /* 1 stop bit is the default */ + dataBitsSize: DataBitsSize = .bits8, + useHardwareFlowControl: Bool = false, + useSoftwareFlowControl: Bool = false, + processOutput: Bool = false) { + guard let fileDescriptor = fileDescriptor else { + return + } + + // Set up the control structure + var settings = termios() + + // Get options structure for the port + tcgetattr(fileDescriptor, &settings) + + // Set baud rates + cfsetispeed(&settings, receiveRate.speedValue) + cfsetospeed(&settings, transmitRate.speedValue) + + // Set parity enable flag + if enableParity { + settings.c_cflag |= ~tcflag_t(PARENB) + } else { + settings.c_cflag &= ~tcflag_t(PARENB) + } + + // Set stop bit flag + if sendTwoStopBits { + settings.c_cflag |= tcflag_t(CSTOPB) + } else { + settings.c_cflag &= ~tcflag_t(CSTOPB) + } + + // Set data bits size flag + settings.c_cflag &= ~tcflag_t(CSIZE) + settings.c_cflag |= dataBitsSize.flagValue + + // Set hardware flow control flag + #if os(Linux) + if useHardwareFlowControl { + settings.c_cflag |= tcflag_t(CRTSCTS) + } else { + settings.c_cflag &= ~tcflag_t(CRTSCTS) + } + #elseif os(OSX) + if useHardwareFlowControl { + settings.c_cflag |= tcflag_t(CRTS_IFLOW) + settings.c_cflag |= tcflag_t(CCTS_OFLOW) + } else { + settings.c_cflag &= ~tcflag_t(CRTS_IFLOW) + settings.c_cflag &= ~tcflag_t(CCTS_OFLOW) + } + #endif + + // Set software flow control flags + let softwareFlowControlFlags = tcflag_t(IXON | IXOFF | IXANY) + if useSoftwareFlowControl { + settings.c_iflag |= softwareFlowControlFlags + } else { + settings.c_iflag &= ~softwareFlowControlFlags + } + + // Turn on the receiver of the serial port, and ignore modem control lines + settings.c_cflag |= tcflag_t(CREAD | CLOCAL) + + // Turn off canonical mode + settings.c_lflag &= ~tcflag_t(ICANON | ECHO | ECHOE | ISIG) + + // Set output processing flag + if processOutput { + settings.c_oflag |= tcflag_t(OPOST) + } else { + settings.c_oflag &= ~tcflag_t(OPOST) + } + + // Special characters + #if os(Linux) + 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) + 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 + #elseif os(OSX) + 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) + 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 + #endif + + specialCharacters.VMIN = cc_t(minimumBytesToRead) + specialCharacters.VTIME = cc_t(timeout) + settings.c_cc = specialCharacters + + // Commit settings + tcsetattr(fileDescriptor, TCSANOW, &settings) + } + + public func closePort() { + if let fileDescriptor = fileDescriptor { + close(fileDescriptor) + } + fileDescriptor = nil + } +} + +// MARK: Receiving + +extension SerialPort { + + public func readBytes(into buffer: UnsafeMutablePointer, size: Int) throws -> Int { + guard let fileDescriptor = fileDescriptor else { + throw PortError.mustBeOpen + } + + let bytesRead = read(fileDescriptor, buffer, size) + return bytesRead + } + + public func readData(ofLength length: Int) throws -> Data { + let buffer = UnsafeMutablePointer.allocate(capacity: length) + defer { + buffer.deallocate(capacity: length) + } + + let bytesRead = try readBytes(into: buffer, size: length) + let data = Data(bytes: buffer, count: bytesRead) + return data + } + + public func readString(ofLength length: Int) throws -> String { + var remainingBytesToRead = length + var result = "" + + while remainingBytesToRead > 0 { + let data = try readData(ofLength: remainingBytesToRead) + if let string = String(data: data, encoding: String.Encoding.utf8) { + result += string + remainingBytesToRead -= data.count + } else { + return result + } + } + + return result + } + + public func readUntilChar(_ terminator: CChar) throws -> String { + var data = Data() + let buffer = UnsafeMutablePointer.allocate(capacity: 1) + defer { + buffer.deallocate(capacity: 1) + } + + // Read byte by byte + while try readBytes(into: buffer, size: 1) > 0 { + let character = CChar(buffer[0]) + if character != terminator { + data.append(buffer, count: 1) + } else { + break + } + } + + if let string = String(data: data, encoding: String.Encoding.utf8) { + return string + } else { + throw PortError.stringsMustBeUTF8 + } + } + + public func readLine() throws -> String { + let newlineChar = CChar(10) // Newline/Line feed character `\n` is 10 + return try readUntilChar(newlineChar) + } +} + +// MARK: Transmitting + +extension SerialPort { + + public func writeBytes(from buffer: UnsafeMutablePointer, size: Int) throws -> Int { + guard let fileDescriptor = fileDescriptor else { + throw PortError.mustBeOpen + } + + let bytesWritten = write(fileDescriptor, buffer, size) + return bytesWritten + } + + public func writeData(_ data: Data) throws -> Int { + let size = data.count + let buffer = UnsafeMutablePointer.allocate(capacity: size) + defer { + buffer.deallocate(capacity: size) + } + + data.copyBytes(to: buffer, count: size) + + let bytesWritten = try writeBytes(from: buffer, size: size) + return bytesWritten + } + + public func writeString(_ string: String) throws -> Int { + guard let data = string.data(using: String.Encoding.utf8) else { + throw PortError.stringsMustBeUTF8 + } + + return try writeData(data) + } +}