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 Initial version 24 * v0.0.2: 09-2023 Fixed baudrate bug 25 * v0.0.4: 06-2026 Linux version switched to ioctl interface instead of tcsetattr 26 * 27 */ 28 29 module dserial; 30 31 import std.string; 32 import std.conv; 33 34 import core.stdc.errno; 35 import core.stdc.string; 36 37 import core.sys.linux.termios; 38 import core.sys.posix.sys.ioctl; 39 import core.sys.posix.termios; 40 import core.sys.posix.unistd; 41 import fcntl = core.sys.posix.fcntl; 42 import unistd = core.sys.posix.unistd; 43 44 import serialexception; 45 46 /** Parity of the connection */ 47 enum Parity { 48 None, 49 Even, 50 Odd, 51 Mark, 52 Space 53 } 54 55 /** Number of bits per character */ 56 enum DataBits { 57 DB5 = CS5, 58 DB6 = CS6, 59 DB7 = CS7, 60 DB8 = CS8 61 } 62 63 /** Number of stop bits per char transmission */ 64 enum StopBits { 65 SB1, 66 SB2 67 } 68 69 /** Set the port access mode to 70 NonBlocking(never blocks), 71 TimedImmediately(timer starts immediately), 72 TimedAfterReceive(timer starts after receiving first char), 73 Blocking(blocks forever) */ 74 enum BlockingMode { 75 NonBlocking, 76 TimedImmediately, 77 TimedAfterReceive, 78 Blocking, 79 } 80 81 private enum CBAUD = 0x0000100f; 82 private enum BOTHER = 0x00001000; 83 84 85 /** Main class for performing serial port operations */ 86 class DSerial { 87 88 private string deviceName; 89 private DataBits dataBits; 90 private Parity parity; 91 private StopBits stopBits; 92 private uint baudRate; 93 private BlockingMode blockingMode = BlockingMode.Blocking; 94 private ubyte readTimeout = 5; // == 0.5 secs 95 96 private bool isOpen = false; 97 98 version (linux) { 99 private int fd; 100 private termios2 options; 101 } 102 103 /** Constructor. Creates object with default settings of 9600,8N1. 104 Default is blocking read/write operations */ 105 this(string deviceName, uint baudRate = 9600, DataBits dataBits = DataBits.DB8, 106 Parity parity = Parity.None, StopBits stopBits = StopBits.SB1) { 107 this.deviceName = deviceName; 108 setOptions(baudRate, dataBits, parity, stopBits); 109 } 110 111 ~this() { 112 close(); 113 } 114 115 /** Set the options of the port. Is applied immediately if the port is already open */ 116 void setOptions(uint baudRate, DataBits dataBits, Parity parity, StopBits stopBits) { 117 this.baudRate = baudRate; 118 this.dataBits = dataBits; 119 this.parity = parity; 120 this.stopBits = stopBits; 121 // apply immediately if the port is already open 122 if (isOpen) 123 applyOptions(); 124 } 125 126 /** Sets read timeout in millis. On linux only increments of 100ms are available. 127 Timeout maximum is 255000 = 25.5secs */ 128 void setTimeout(uint millis) { 129 readTimeout = cast(ubyte)(millis / 100); 130 if (isOpen && (blockingMode == BlockingMode.TimedImmediately 131 || blockingMode == BlockingMode.TimedAfterReceive)) // reapply so that the timeout 132 applyBlockingMode(); 133 } 134 135 /** Apply the options to the connection. Note: only call this when the port is already open */ 136 private void applyOptions() { 137 if (!isOpen) 138 throw new SerialException("Can't apply options on closed connection"); 139 // TODO: check error codes 140 // set attributes 141 version (linux) { 142 143 // On linux use ioctl instead of tcsetattr 144 // since tcsetattr is not guaranteed to work correctly. 145 146 147 ioctl(fd, TCGETS2, &options); // disable IGNBRK for mismatched speed tests; otherwise receive break 148 // as \000 chars 149 options.c_iflag &= ~ ( 150 IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON | IXOFF | IXANY); 151 options.c_lflag &= ~(ECHO | ECHOE | ECHONL | ICANON | ISIG | IEXTEN); // no signaling chars, no echo, 152 // no canonical processing(reading lines) 153 options.c_cflag &= ~CSIZE; 154 options.c_cflag |= dataBits; 155 options.c_cflag |= CS8; 156 options.c_cflag |= CLOCAL | CREAD; 157 options.c_cflag &= ~CRTSCTS; // disable crtscts 158 options.c_cflag &= ~CSTOPB; 159 options.c_cflag &= ~CBAUD; 160 options.c_cflag |= BOTHER; 161 162 options.c_ispeed = baudRate; 163 options.c_ospeed = baudRate; 164 165 options.c_oflag &= ~OPOST; // Prevent special interpretation of output bytes (e.g. newline chars) 166 options.c_oflag &= ~ONLCR; // Prevent conversion of newline to carriage return/line feed 167 168 // parity 169 final switch (parity) { 170 case Parity.None: 171 options.c_cflag &= ~(PARENB | PARODD); 172 break; 173 case Parity.Even: 174 options.c_cflag |= PARENB; 175 break; 176 case Parity.Odd: 177 options.c_cflag |= (PARENB | PARODD); 178 break; 179 case Parity.Mark: 180 options.c_cflag |= (PARENB | PARODD | PARMRK); 181 break; 182 case Parity.Space: 183 options.c_cflag |= (PARENB | PARMRK); 184 break; 185 } 186 // stop bits 187 final switch (stopBits) { 188 case StopBits.SB1: 189 options.c_cflag &= ~CSTOPB; 190 break; 191 case StopBits.SB2: 192 options.c_cflag |= CSTOPB; 193 break; 194 } 195 196 197 ioctl(fd, TCSETS2, &options); 198 } 199 200 } 201 202 /** Apply blocking mode settings to the connection. Don't apply on closed connections */ 203 private void applyBlockingMode() { 204 if (!isOpen) 205 throw new SerialException("Can't apply blocking mode on closed connection"); 206 207 version (linux) { 208 final switch (blockingMode) { 209 case BlockingMode.NonBlocking: 210 options.c_cc[VMIN] = 0; // read doesn't block 211 options.c_cc[VTIME] = 0; // don't wait for timeout 212 break; 213 case BlockingMode.TimedImmediately: 214 options.c_cc[VMIN] = 0; // read doesn't block, only timeout 215 options.c_cc[VTIME] = readTimeout; 216 break; 217 case BlockingMode.TimedAfterReceive: 218 options.c_cc[VMIN] = 1; // blocks until at least 1 byte 219 options.c_cc[VTIME] = readTimeout; 220 break; 221 case BlockingMode.Blocking: 222 options.c_cc[VMIN] = 1; // blocks until at least 1 byte 223 options.c_cc[VTIME] = 0; 224 break; 225 } 226 ioctl(fd, TCSETS2, &options); 227 } 228 } 229 230 /** set blocking mode for operations */ 231 void setBlockingMode(BlockingMode mode) { 232 blockingMode = mode; 233 if (isOpen) 234 applyBlockingMode(); 235 } 236 237 /** Opens the serial port with current settings. Throws an exception if the port can't be opened */ 238 void open() { 239 240 version (linux) { 241 // open the port 242 fd = fcntl.open(deviceName.toStringz(), fcntl.O_RDWR | fcntl.O_NOCTTY); 243 if (fd == -1) { 244 throw new SerialException("Can't open device '" ~ deviceName ~ "'"); 245 } 246 } 247 248 isOpen = true; 249 250 applyOptions(); 251 applyBlockingMode(); 252 } 253 254 /** Closes the serial port */ 255 void close() { 256 if (!isOpen) 257 return; 258 259 version (linux) { 260 unistd.close(fd); 261 } 262 isOpen = false; 263 } 264 265 /** convenience function. reads a single byte */ 266 ulong read(ref ubyte data) { 267 return read(&data, 1); 268 } 269 270 /** convenience function. Reads up to data.length bytes*/ 271 ulong read(ubyte[] data) { 272 return read(data.ptr, data.length); 273 } 274 275 /** Reads data into the bytes array. 276 Blocks until at least one char has been read. 277 Returns: 278 positive number of bytes read 279 throws exception upon error */ 280 ulong read(ubyte* data, ulong length) { 281 282 if (!isOpen) 283 throw new SerialException("Attempted read from closed port."); 284 285 long n; 286 version (linux) { 287 n = unistd.read(fd, data, length); 288 // TODO: check read return values and return appropriate result 289 if (n < 0) 290 throw new SerialException(to!string(strerror(errno).fromStringz)); 291 else if (blockingMode == BlockingMode.Blocking && n == 0) // TODO: check if blockingmode == timedimmediately 292 throw new SerialException("Error: probably device removal"); 293 } 294 return n; 295 } 296 297 /** Write bytes to the serial port 298 returns bytes written, 299 Only supports blocking and nonblocking writes. 300 Timed writes are unsupported. */ 301 long write(const ubyte[] bytes) { 302 long n; 303 version (linux) { 304 n = unistd.write(fd, bytes.ptr, bytes.length); 305 // 306 if (blockingMode == BlockingMode.Blocking) { 307 tcdrain(fd); 308 } 309 } 310 311 return n; 312 } 313 }