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 }