1
0

SerialPort.swift 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. import Foundation
  2. public class SerialPort {
  3. var path: String
  4. var fileDescriptor: Int32?
  5. private var isOpen: Bool { fileDescriptor != nil }
  6. private var pollSource: DispatchSourceRead?
  7. private var readDataStream: AsyncStream<Data>?
  8. private var readBytesStream: AsyncStream<UInt8>?
  9. private var readLinesStream: AsyncStream<String>?
  10. public init(path: String) {
  11. self.path = path
  12. }
  13. public func openPort(portMode: PortMode = .receiveAndTransmit) throws {
  14. guard !path.isEmpty else { throw PortError.invalidPath }
  15. guard isOpen == false else { throw PortError.instanceAlreadyOpen }
  16. let readWriteParam: Int32
  17. switch portMode {
  18. case .receive:
  19. readWriteParam = O_RDONLY
  20. case .transmit:
  21. readWriteParam = O_WRONLY
  22. case .receiveAndTransmit:
  23. readWriteParam = O_RDWR
  24. }
  25. #if os(Linux)
  26. fileDescriptor = open(path, readWriteParam | O_NOCTTY)
  27. #elseif os(OSX)
  28. fileDescriptor = open(path, readWriteParam | O_NOCTTY | O_EXLOCK)
  29. #endif
  30. // Throw error if open() failed
  31. if fileDescriptor == PortError.failedToOpen.rawValue {
  32. throw PortError.failedToOpen
  33. }
  34. guard
  35. portMode.receive,
  36. let fileDescriptor
  37. else { return }
  38. let pollSource = DispatchSource.makeReadSource(fileDescriptor: fileDescriptor, queue: .global(qos: .default))
  39. let stream = AsyncStream<Data> { continuation in
  40. pollSource.setEventHandler {
  41. let bufferSize = 1024
  42. let buffer = UnsafeMutableRawPointer
  43. .allocate(byteCount: bufferSize, alignment: 8)
  44. let bytesRead = read(fileDescriptor, buffer, bufferSize)
  45. guard bytesRead > 0 else { return }
  46. let bytes = Data(bytes: buffer, count: bytesRead)
  47. continuation.yield(bytes)
  48. }
  49. pollSource.setCancelHandler {
  50. continuation.finish()
  51. }
  52. }
  53. pollSource.resume()
  54. self.pollSource = pollSource
  55. self.readDataStream = stream
  56. }
  57. public func setSettings(
  58. receiveRate: BaudRate,
  59. transmitRate: BaudRate,
  60. minimumBytesToRead: Int,
  61. timeout: Int = 0, /* 0 means wait indefinitely */
  62. parityType: ParityType = .none,
  63. sendTwoStopBits: Bool = false, /* 1 stop bit is the default */
  64. dataBitsSize: DataBitsSize = .bits8,
  65. useHardwareFlowControl: Bool = false,
  66. useSoftwareFlowControl: Bool = false,
  67. processOutput: Bool = false
  68. ) throws {
  69. guard let fileDescriptor = fileDescriptor else {
  70. throw PortError.mustBeOpen
  71. }
  72. // Set up the control structure
  73. var settings = termios()
  74. // Get options structure for the port
  75. tcgetattr(fileDescriptor, &settings)
  76. // Set baud rates
  77. cfsetispeed(&settings, receiveRate.speedValue)
  78. cfsetospeed(&settings, transmitRate.speedValue)
  79. // Enable parity (even/odd) if needed
  80. settings.c_cflag |= parityType.parityValue
  81. // Set stop bit flag
  82. if sendTwoStopBits {
  83. settings.c_cflag |= tcflag_t(CSTOPB)
  84. } else {
  85. settings.c_cflag &= ~tcflag_t(CSTOPB)
  86. }
  87. // Set data bits size flag
  88. settings.c_cflag &= ~tcflag_t(CSIZE)
  89. settings.c_cflag |= dataBitsSize.flagValue
  90. //Disable input mapping of CR to NL, mapping of NL into CR, and ignoring CR
  91. settings.c_iflag &= ~tcflag_t(ICRNL | INLCR | IGNCR)
  92. // Set hardware flow control flag
  93. #if os(Linux)
  94. if useHardwareFlowControl {
  95. settings.c_cflag |= tcflag_t(CRTSCTS)
  96. } else {
  97. settings.c_cflag &= ~tcflag_t(CRTSCTS)
  98. }
  99. #elseif os(OSX)
  100. if useHardwareFlowControl {
  101. settings.c_cflag |= tcflag_t(CRTS_IFLOW)
  102. settings.c_cflag |= tcflag_t(CCTS_OFLOW)
  103. } else {
  104. settings.c_cflag &= ~tcflag_t(CRTS_IFLOW)
  105. settings.c_cflag &= ~tcflag_t(CCTS_OFLOW)
  106. }
  107. #endif
  108. // Set software flow control flags
  109. let softwareFlowControlFlags = tcflag_t(IXON | IXOFF | IXANY)
  110. if useSoftwareFlowControl {
  111. settings.c_iflag |= softwareFlowControlFlags
  112. } else {
  113. settings.c_iflag &= ~softwareFlowControlFlags
  114. }
  115. // Turn on the receiver of the serial port, and ignore modem control lines
  116. settings.c_cflag |= tcflag_t(CREAD | CLOCAL)
  117. // Turn off canonical mode
  118. settings.c_lflag &= ~tcflag_t(ICANON | ECHO | ECHOE | ISIG)
  119. // Set output processing flag
  120. if processOutput {
  121. settings.c_oflag |= tcflag_t(OPOST)
  122. } else {
  123. settings.c_oflag &= ~tcflag_t(OPOST)
  124. }
  125. //Special characters
  126. //We do this as c_cc is a C-fixed array which is imported as a tuple in Swift.
  127. //To avoid hardcoding the VMIN or VTIME value to access the tuple value, we use the typealias instead
  128. #if os(Linux)
  129. 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)
  130. 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
  131. #elseif os(OSX)
  132. 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)
  133. 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
  134. #endif
  135. specialCharacters.VMIN = cc_t(minimumBytesToRead)
  136. specialCharacters.VTIME = cc_t(timeout)
  137. settings.c_cc = specialCharacters
  138. // Commit settings
  139. tcsetattr(fileDescriptor, TCSANOW, &settings)
  140. }
  141. public func closePort() {
  142. pollSource?.cancel()
  143. pollSource = nil
  144. readDataStream = nil
  145. readBytesStream = nil
  146. readLinesStream = nil
  147. if let fileDescriptor = fileDescriptor {
  148. close(fileDescriptor)
  149. }
  150. fileDescriptor = nil
  151. }
  152. }
  153. // MARK: Receiving
  154. extension SerialPort {
  155. public func asyncData() throws -> AsyncStream<Data> {
  156. guard
  157. isOpen,
  158. let readDataStream
  159. else {
  160. throw PortError.mustBeOpen
  161. }
  162. return readDataStream
  163. }
  164. public func asyncBytes() throws -> AsyncStream<UInt8> {
  165. guard
  166. isOpen,
  167. let readDataStream
  168. else {
  169. throw PortError.mustBeOpen
  170. }
  171. if let existing = readBytesStream {
  172. return existing
  173. } else {
  174. let new = AsyncStream<UInt8> { continuation in
  175. Task {
  176. for try await data in readDataStream {
  177. for byte in data {
  178. continuation.yield(byte)
  179. }
  180. }
  181. continuation.finish()
  182. }
  183. }
  184. readBytesStream = new
  185. return new
  186. }
  187. }
  188. public func asyncLines() throws -> AsyncStream<String> {
  189. guard isOpen else { throw PortError.mustBeOpen }
  190. if let existing = readLinesStream {
  191. return existing
  192. } else {
  193. let byteStream = try asyncBytes()
  194. let new = AsyncStream<String> { continuation in
  195. Task {
  196. var accumulator = Data()
  197. for try await byte in byteStream {
  198. accumulator.append(byte)
  199. guard
  200. UnicodeScalar(byte) == "\n".unicodeScalars.first
  201. else { continue }
  202. defer { accumulator = Data() }
  203. guard
  204. let string = String(data: accumulator, encoding: .utf8)
  205. else {
  206. continuation.yield("Error: Non string data. Perhaps you wanted data or bytes output?")
  207. continue
  208. }
  209. continuation.yield(string)
  210. }
  211. continuation.finish()
  212. }
  213. }
  214. readLinesStream = new
  215. return new
  216. }
  217. }
  218. }
  219. // MARK: Transmitting
  220. extension SerialPort {
  221. public func writeBytes(from buffer: UnsafeMutablePointer<UInt8>, size: Int) throws -> Int {
  222. guard let fileDescriptor = fileDescriptor else {
  223. throw PortError.mustBeOpen
  224. }
  225. let bytesWritten = write(fileDescriptor, buffer, size)
  226. return bytesWritten
  227. }
  228. public func writeData(_ data: Data) throws -> Int {
  229. let size = data.count
  230. let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: size)
  231. defer {
  232. buffer.deallocate()
  233. }
  234. data.copyBytes(to: buffer, count: size)
  235. let bytesWritten = try writeBytes(from: buffer, size: size)
  236. return bytesWritten
  237. }
  238. public func writeString(_ string: String) throws -> Int {
  239. guard let data = string.data(using: String.Encoding.utf8) else {
  240. throw PortError.stringsMustBeUTF8
  241. }
  242. return try writeData(data)
  243. }
  244. public func writeChar(_ character: UnicodeScalar) throws -> Int{
  245. let stringEquiv = String(character)
  246. let bytesWritten = try writeString(stringEquiv)
  247. return bytesWritten
  248. }
  249. }