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 }