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 }