1 /** 2 * A serial port library that support non blocking IO 3 * Main class that encapsulates access to the serial port 4 * 5 * Examples: 6 * ---------------- 7 * DSerial serialPort = new DSerial("/dev/ttyS0"); 8 * serialPort.setBlockingMode(DSerial.BlockingMode.TimedImmediately); 9 * serialPort.setTimeout(200); // 200 millis 10 * serialPort.open(); 11 * ubyte c; 12 * // reading 13 * while (serialPort.read(c) == 1) { 14 * // Do work 15 * } 16 * // writing 17 * ubyte[] msgBuf = messageToBytes(msg); 18 * return serialPort.write(msgBuf); 19 * --------------- 20 * 21 * Author: Jaap Geurts 22 * Date: 08-2022 23 * v0.0.1: 08-2022 24 * v0.0.2: 09-2023 Fixed baudrate bug 25 * 26 */ 27 28 module dserial; 29 30 import std.string; 31 import std.conv; 32 33 import core.sys.posix.termios; 34 import core.sys.linux.termios; 35 import core.sys.posix.unistd; 36 import fcntl = core.sys.posix.fcntl; 37 import unistd = core.sys.posix.unistd; 38 import core.stdc.string; 39 import core.stdc.errno; 40 41 import serialexception; 42 43 /** Main class for performing serial port operations */ 44 class DSerial { 45 46 // dfmt off 47 /** Parity of the connection */ 48 enum Parity { None, Even, Odd, Mark, Space } 49 50 /** Number of bits per character */ 51 enum DataBits { DB5 = CS5, DB6 = CS6, DB7 = CS7, DB8 = CS8 } 52 53 /** Number of stop bits per char transmission */ 54 enum StopBits { SB1, SB2 } 55 56 enum BaudRate { 57 B0 = 0, /* hang up */ 58 B50 = 1, 59 B75 = 2, 60 B110 = 3, 61 B134 = 4, 62 B150 = 5, 63 B200 = 6, 64 B300 = 7, 65 B600 = 10, 66 B1200 = 11, 67 B1800 = 12, 68 B2400 = 13, 69 B4800 = 14, 70 B9600 = 14, 71 B19200 = 15, 72 B38400 = 17, 73 B57600 = 0x1001, 74 B115200 = 0x1002, 75 B230400 = 0x1003, 76 B460800 = 0x1004, 77 B500000 = 0x1005, 78 B576000 = 0x1006, 79 B921600 = 0x1007, 80 B1000000 = 0x1008, 81 B1152000 = 0x1009, 82 B1500000 = 0x100A, 83 B2000000 = 0x100B, 84 B2500000 = 0x100C, 85 B3000000 = 0x100D, 86 B3500000 = 0x100E, 87 B4000000 = 0x100F 88 } 89 90 /** Set the port access mode to 91 NonBlocking(never blocks), 92 TimedImmediately(timer starts immediately), 93 TimedAfterReceive(timer starts after receiving first char), 94 Blocking(blocks forever) */ 95 enum BlockingMode 96 { 97 NonBlocking, 98 TimedImmediately, 99 TimedAfterReceive, 100 Blocking, 101 } 102 // dfmt on 103 104 private string deviceName; 105 private DataBits dataBits; 106 private Parity parity; 107 private StopBits stopBits; 108 private BaudRate baudRate; 109 private BlockingMode blockingMode = BlockingMode.Blocking; 110 private ubyte readTimeout = 5; // == 0.5 secs 111 112 private bool isOpen = false; 113 114 version (linux) { 115 private int fd; 116 private termios options; 117 } 118 119 /** Constructor. Creates object with default settings of 9600,8N1. 120 Default is blocking read/write operations */ 121 this(string deviceName, BaudRate baudRate = BaudRate.B9600, DataBits dataBits = DataBits.DB8, 122 Parity parity = Parity.None, StopBits stopBits = StopBits.SB1) { 123 this.deviceName = deviceName; 124 setOptions(baudRate, dataBits, parity, stopBits); 125 } 126 127 ~this() { 128 close(); 129 } 130 131 /** Set the options of the port. Is applied immediately if the port is already open */ 132 void setOptions(BaudRate baudRate, DataBits dataBits, Parity parity, StopBits stopBits) { 133 this.baudRate = baudRate; 134 this.dataBits = dataBits; 135 this.parity = parity; 136 this.stopBits = stopBits; 137 // apply immediately if the port is already open 138 if (isOpen) 139 applyOptions(); 140 } 141 142 /** Sets read timeout in millis. On linux only increments of 100ms are available. 143 Timeout maximum is 255000 = 25.5secs */ 144 void setTimeout(uint millis) { 145 readTimeout = cast(ubyte)(millis / 100); 146 if (isOpen && (blockingMode == BlockingMode.TimedImmediately 147 || blockingMode == BlockingMode.TimedAfterReceive)) // reapply so that the timeout 148 applyBlockingMode(); 149 } 150 151 /** Apply the options to the connection. Note: only call this when the port is already open */ 152 private void applyOptions() { 153 if (!isOpen) 154 throw new SerialException("Can't apply options on closed connection"); 155 // TODO: check error codes 156 // set attributes 157 version (linux) { 158 tcgetattr(fd, &options); 159 160 // baud rate 161 cfsetispeed(&options, baudRate); 162 cfsetospeed(&options, baudRate); 163 164 // disable IGNBRK for mismatched speed tests; otherwise receive break 165 // as \000 chars 166 options.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL); // disable break processing 167 options.c_iflag &= ~(IXON | IXOFF | IXANY); // shut off xon/xoff ctrl 168 // TODO: these flags should be unset. not set to 0 169 options.c_lflag &= ~(ICANON|ECHO|ECHOE|ECHONL|ISIG); // no signaling chars, no echo, 170 // no canonical processing(reading lines) 171 172 options.c_cflag |= (CLOCAL | CREAD); // ignore modem controls, enable reading 173 174 // set databits 175 options.c_cflag &= ~CSIZE; // clear field first 176 options.c_cflag |= dataBits; 177 178 // parity 179 final switch (parity) { 180 case Parity.None: 181 options.c_cflag &= ~(PARENB | PARODD); 182 break; 183 case Parity.Even: 184 options.c_cflag |= PARENB; 185 break; 186 case Parity.Odd: 187 options.c_cflag |= (PARENB | PARODD); 188 break; 189 case Parity.Mark: 190 options.c_cflag |= (PARENB | PARODD | PARMRK); 191 break; 192 case Parity.Space: 193 options.c_cflag |= (PARENB | PARMRK); 194 break; 195 } 196 // stop bits 197 final switch (stopBits) { 198 case StopBits.SB1: 199 options.c_cflag &= ~CSTOPB; 200 break; 201 case StopBits.SB2: 202 options.c_cflag |= CSTOPB; 203 break; 204 } 205 206 options.c_cflag &= ~CRTSCTS; // disable crtscts 207 208 options.c_oflag &= ~OPOST; // Prevent special interpretation of output bytes (e.g. newline chars) 209 options.c_oflag &= ~ONLCR; // Prevent conversion of newline to carriage return/line feed 210 211 // TODO: check error codes 212 tcsetattr(fd, TCSANOW, &options); 213 } 214 215 } 216 217 /** Apply blocking mode settings to the connection. Don't apply on closed connections */ 218 private void applyBlockingMode() { 219 if (!isOpen) 220 throw new SerialException("Can't apply blocking mode on closed connection"); 221 222 version (linux) { 223 final switch (blockingMode) { 224 case BlockingMode.NonBlocking: 225 options.c_cc[VMIN] = 0; // read doesn't block 226 options.c_cc[VTIME] = 0; // don't wait for timeout 227 break; 228 case BlockingMode.TimedImmediately: 229 options.c_cc[VMIN] = 0; // read doesn't block, only timeout 230 options.c_cc[VTIME] = readTimeout; 231 break; 232 case BlockingMode.TimedAfterReceive: 233 options.c_cc[VMIN] = 1; // blocks until at least 1 byte 234 options.c_cc[VTIME] = readTimeout; 235 break; 236 case BlockingMode.Blocking: 237 options.c_cc[VMIN] = 1; // blocks until at least 1 byte 238 options.c_cc[VTIME] = 0; 239 break; 240 } 241 tcsetattr(fd, TCSANOW, &options); 242 } 243 } 244 245 /** set blocking mode for operations */ 246 void setBlockingMode(BlockingMode mode) { 247 blockingMode = mode; 248 if (isOpen) 249 applyBlockingMode(); 250 } 251 252 /** Opens the serial port with current settings. Throws an exception if the port can't be opened */ 253 void open() { 254 255 version (linux) { 256 // open the port 257 fd = fcntl.open(deviceName.toStringz(), fcntl.O_RDWR | fcntl.O_NOCTTY); 258 if (fd == -1) { 259 throw new SerialException("Can't open device '" ~ deviceName ~ "'"); 260 } 261 } 262 263 isOpen = true; 264 265 applyOptions(); 266 applyBlockingMode(); 267 } 268 269 /** Closes the serial port */ 270 void close() { 271 if (!isOpen) 272 return; 273 274 version (linux) { 275 unistd.close(fd); 276 } 277 isOpen = false; 278 } 279 280 /** convenience function. reads a single byte */ 281 ulong read(ref ubyte data) { 282 return read(&data, 1); 283 } 284 285 /** convenience function. Reads up to data.length bytes*/ 286 ulong read(ubyte[] data) { 287 return read(data.ptr, data.length); 288 } 289 290 /** Reads data into the bytes array. 291 Blocks until at least one char has been read. 292 Returns: 293 positive number of bytes read 294 throws exception upon error */ 295 ulong read(ubyte* data, ulong length) { 296 297 if (!isOpen) 298 throw new SerialException("Attempted read from closed port."); 299 300 long n; 301 version (linux) { 302 n = unistd.read(fd, data, length); 303 // TODO: check read return values and return appropriate result 304 if (n < 0) 305 throw new SerialException(to!string(strerror(errno).fromStringz)); 306 else if (blockingMode == BlockingMode.Blocking && n == 0) // TODO: check if blockingmode == timedimmediately 307 throw new SerialException("Error: probably device removal"); 308 } 309 return n; 310 } 311 312 /** Write bytes to the serial port 313 returns bytes written, 314 Only supports blocking and nonblocking writes. 315 Timed writes are unsupported. */ 316 long write(const ubyte[] bytes) { 317 long n; 318 version (linux) { 319 n = unistd.write(fd, bytes.ptr, bytes.length); 320 // 321 if (blockingMode == BlockingMode.Blocking) { 322 tcdrain(fd); 323 } 324 } 325 326 return n; 327 } 328 }