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