1 ///
2 module serialport.port;
3 
4 import std.algorithm;
5 import std.array;
6 import std.conv : to, text, octal;
7 import std.exception;
8 import std.experimental.logger;
9 import std.path;
10 import std.string;
11 import core.time;
12 import std.datetime : StopWatch;
13 
14 import serialport.types;
15 import serialport.exception;
16 
17 version (Posix) {} else version (Windows) {}
18 else static assert(0, "unsupported platform");
19 
20 ///
21 class SerialPort
22 {
23 protected:
24     ///
25     string port;
26 
27     ///
28     version (Posix) int handle = -1;
29     ///
30     version (Windows) HANDLE handle = null;
31 
32     /// preform pause
33     void yield()
34     {
35         if (yieldFunc !is null) yieldFunc();
36         else
37         {
38             import core.thread;
39             if (Fiber.getThis is null) Thread.yield();
40             else Fiber.yield();
41         }
42     }
43 
44 public:
45 
46     ///
47     static struct Config
48     {
49         ///
50         uint baudRate=9600;
51         ///
52         Parity parity=Parity.none;
53         ///
54         DataBits dataBits=DataBits.data8;
55         ///
56         StopBits stopBits=StopBits.one;
57 
58         auto set(Parity v) { parity = v; return this; }
59         auto set(uint v) { baudRate = v; return this; }
60         auto set(DataBits v) { dataBits = v; return this; }
61         auto set(StopBits v) { stopBits = v; return this; }
62     }
63 
64     /// extended delegate for yielding
65     void delegate() yieldFunc;
66 
67     ///
68     this(string port, Config conf=Config.init, void delegate() yh=null)
69     {
70         this.port = port;
71         this.yieldFunc = yh;
72         setup(conf);
73     }
74 
75     ///
76     this(string port, uint baudRate, void delegate() yh=null)
77     { this(port, Config(baudRate), yh); }
78 
79     ///
80     this(string port, uint baudRate, Parity parity, void delegate() yh=null)
81     { this(port, Config(baudRate, parity), yh); }
82 
83     ///
84     this(string port, uint baudRate, Parity parity,
85             DataBits dataBits,
86             StopBits stopBits, void delegate() yh=null)
87     { this(port, Config(baudRate, parity, dataBits, stopBits), yh); }
88 
89     ~this() { close(); }
90 
91     /// close handle
92     void close()
93     {
94         if (closed) return;
95         version(Windows)
96         {
97             CloseHandle(handle);
98             handle = null;
99         }
100         version(Posix)
101         {
102             posixClose(handle);
103             handle = -1;
104         }
105     }
106 
107     ///
108     override string toString() { return port; }
109 
110     ///
111     SerialPort set(Parity p) { config = config.set(p); return this; }
112     ///
113     SerialPort set(uint br) { config = config.set(br); return this; }
114     ///
115     SerialPort set(DataBits db) { config = config.set(db); return this; }
116     ///
117     SerialPort set(StopBits sb) { config = config.set(sb); return this; }
118 
119     @property
120     {
121         ///
122         bool closed() const
123         {
124             version(Posix) return handle == -1;
125             version(Windows) return handle is null;
126         }
127 
128         ///
129         Config config()
130         {
131             enforce(!closed, new PortClosedException(port));
132 
133             Config ret;
134 
135             version (Posix)
136             {
137                 termios opt;
138                 enforce(tcgetattr(handle, &opt) != -1,
139                         new SerialPortException(format("Failed while call tcgetattr: %d", errno)));
140 
141                 ret.baudRate = getUintBaudRate();
142 
143                 if (opt.c_cflag.hasFlag(PARODD)) ret.parity = Parity.odd;
144                 else if (!(opt.c_cflag & PARENB)) ret.parity = Parity.none;
145                 else ret.parity = Parity.even;
146 
147                      if (opt.c_cflag.hasFlag(CS8)) ret.dataBits = DataBits.data8;
148                 else if (opt.c_cflag.hasFlag(CS7)) ret.dataBits = DataBits.data7;
149                 else if (opt.c_cflag.hasFlag(CS6)) ret.dataBits = DataBits.data6;
150                 else ret.dataBits = DataBits.data5;
151 
152                 ret.stopBits = opt.c_cflag.hasFlag(CSTOPB) ? StopBits.two : StopBits.one;
153             }
154             version (Windows)
155             {
156                 DCB cfg;
157                 GetCommState(handle, &cfg);
158 
159                 ret.baudRate = cast(uint)cfg.BaudRate;
160 
161                 ret.parity = [NOPARITY: Parity.none, ODDPARITY: Parity.odd,
162                               EVENPARITY: Parity.even][cfg.Parity];
163 
164                 ret.dataBits = [5: DataBits.data5, 6: DataBits.data6,
165                                 7: DataBits.data7, 8: DataBits.data8]
166                                     .get(cfg.ByteSize, DataBits.data8);
167 
168                 ret.stopBits = cfg.StopBits == ONESTOPBIT ? StopBits.one : StopBits.two;
169             }
170 
171             return ret;
172         }
173 
174         ///
175         void config(Config c)
176         {
177             if (closed) throw new PortClosedException(port);
178 
179             version (Posix)
180             {
181                 setUintBaudRate(c.baudRate);
182 
183                 termios opt;
184                 enforce(tcgetattr(handle, &opt) != -1,
185                         new SerialPortException(format("Failed while call tcgetattr: %d", errno)));
186 
187                 final switch (c.parity)
188                 {
189                     case Parity.none:
190                         opt.c_cflag &= ~PARENB;
191                         break;
192                     case Parity.odd:
193                         opt.c_cflag |= (PARENB | PARODD);
194                         break;
195                     case Parity.even:
196                         opt.c_cflag &= ~PARODD;
197                         opt.c_cflag |= PARENB;
198                         break;
199                 }
200 
201                 final switch (c.stopBits)
202                 {
203                     case StopBits.one:
204                         opt.c_cflag &= ~CSTOPB;
205                         break;
206                     case StopBits.onePointFive:
207                     case StopBits.two:
208                         opt.c_cflag |= CSTOPB;
209                         break;
210                 }
211 
212                 opt.c_cflag &= ~CSIZE;
213                 switch (c.dataBits) {
214                     case DataBits.data5: opt.c_cflag |= CS5; break;
215                     case DataBits.data6: opt.c_cflag |= CS6; break;
216                     case DataBits.data7: opt.c_cflag |= CS7; break;
217                     case DataBits.data8: opt.c_cflag |= CS8; break;
218                     default:
219                         errorf("config dataBits is setted as %d, set default CS8",
220                                 c.dataBits);
221                         opt.c_cflag |= CS8;
222                         break;
223                 }
224 
225                 enforce(tcsetattr(handle, TCSANOW, &opt) != -1,
226                         new SerialPortException(format("Failed while call tcsetattr: %d", errno)));
227 
228                 auto test = config;
229 
230                 enforce(test.baudRate == c.baudRate,
231                             new BaudRateUnsupportedException(c.baudRate));
232                 enforce(test.parity == c.parity,
233                             new ParityUnsupportedException(c.parity));
234                 enforce(test.stopBits == c.stopBits,
235                             new StopBitsUnsupportedException(c.stopBits));
236                 enforce(test.dataBits == c.dataBits,
237                             new DataBitsUnsupportedException(c.dataBits));
238             }
239             version (Windows)
240             {
241                 DCB cfg;
242                 GetCommState(handle, &cfg);
243 
244                 if (cfg.BaudRate != cast(DWORD)c.baudRate)
245                 {
246                     cfg.BaudRate = cast(DWORD)c.baudRate;
247                     enforce(SetCommState(handle, &cfg),
248                             new BaudRateUnsupportedException(c.baudRate));
249                 }
250 
251                 auto tmpParity = [Parity.none: NOPARITY, Parity.odd: ODDPARITY,
252                                   Parity.even: EVENPARITY][c.parity];
253                 if (cfg.Parity != tmpParity)
254                 {
255                     cfg.Parity = cast(ubyte)tmpParity;
256                     enforce(SetCommState(handle, &cfg),
257                             new ParityUnsupportedException(c.parity));
258                 }
259 
260                 auto tmpStopBits = [StopBits.one: ONESTOPBIT,
261                                     StopBits.onePointFive: ONESTOPBIT,
262                                     StopBits.two: TWOSTOPBITS][c.stopBits];
263 
264                 if (cfg.StopBits != tmpStopBits)
265                 {
266                     cfg.StopBits = cast(ubyte)tmpStopBits;
267                     enforce(SetCommState(handle, &cfg),
268                             new StopBitsUnsupportedException(c.stopBits));
269                 }
270 
271                 if (cfg.ByteSize != cast(typeof(cfg.ByteSize))c.dataBits)
272                 {
273                     cfg.ByteSize = cast(typeof(cfg.ByteSize))c.dataBits;
274                     enforce(SetCommState(handle, &cfg),
275                             new DataBitsUnsupportedException(c.dataBits));
276                 }
277             }
278         }
279 
280         ///
281         Parity parity() { return config.parity; }
282         ///
283         uint baudRate() { return config.baudRate; }
284         ///
285         DataBits dataBits() { return config.dataBits; }
286         ///
287         StopBits stopBits() { return config.stopBits; }
288 
289         ///
290         Parity parity(Parity v) { config = config.set(v); return v; }
291         ///
292         uint baudRate(uint v) { config = config.set(v); return v; }
293         ///
294         DataBits dataBits(DataBits v) { config = config.set(v); return v; }
295         ///
296         StopBits stopBits(StopBits v) { config = config.set(v); return v; }
297 
298         ///
299         static string[] ports()
300         {
301             version (Posix)
302             {
303                 bool onlyComPorts(string n)
304                 {
305                     static bool isInRange(T, U)(T v, U a, U b)
306                     { return a <= v && v <= b; }
307 
308                     version(linux)   return n.startsWith("ttyUSB") ||
309                                             n.startsWith("ttyS");
310                     version(darwin)  return n.startsWith("cu");
311                     version(FreeBSD) return n.startsWith("cuaa") ||
312                                             n.startsWith("cuad");
313                     version(openbsd) return n.startsWith("tty");
314                     version(solaris) return n.startsWith("tty") &&
315                                             isInRange(n[$-1],'a','z');
316                 }
317 
318                 return dirEntries("/dev/", SpanMode.shallow)
319                         .map!(a=>a.name.baseName)
320                         .filter!onlyComPorts
321                         .map!(a=>"/dev/" ~ a)
322                         .array;
323             }
324             version (Windows)
325             {
326                 string[] ret;
327                 enum pre = `\\.\COM`;
328                 foreach (int n; 0 .. 255)
329                 {
330                     auto i = n+1;
331                     HANDLE p = CreateFileA(text(pre, i).toStringz,
332                             GENERIC_READ | GENERIC_WRITE, 0, null,
333                                            OPEN_EXISTING, 0, null);
334                     if (p != INVALID_HANDLE_VALUE)
335                     {
336                         ret ~= text("COM", i);
337                         CloseHandle(p);
338                     }
339                 }
340                 return ret;
341             }
342         }
343     }
344 
345     ///
346     void write(const(void[]) arr, Duration timeout=500.dur!"usecs")
347     {
348         if (closed) throw new PortClosedException(port);
349 
350         size_t written = 0;
351 
352         StopWatch full;
353 
354         full.start();
355         while (written < arr.length)
356         {
357             ptrdiff_t res;
358             auto ptr = arr.ptr + written;
359             auto len = arr.length - written;
360 
361             version (Posix)
362             {
363                 res = posixWrite(handle, ptr, len);
364                 enforce(res >= 0, new WriteException(port, text("errno ", errno)));
365             }
366             version (Windows)
367             {
368                 uint sres;
369                 auto wfr = WriteFile(handle, ptr, cast(uint)len, &sres, null);
370                 if (!wfr)
371                 {
372                     auto err = GetLastError();
373                     if (err == ERROR_IO_PENDING) { /+ asynchronously +/ }
374                     else throw new WriteException(port, text("error ", err));
375                 }
376                 res = sres;
377             }
378 
379             written += res;
380 
381             if (full.peek().to!Duration > timeout)
382                 throw new TimeoutException(port);
383 
384             yield();
385         }
386     }
387 
388     ///
389     void[] read(void[] arr, Duration timeout=1.dur!"seconds",
390                             Duration frameGap=4.dur!"msecs")
391     {
392         if (closed) throw new PortClosedException(port);
393 
394         size_t readed = 0;
395 
396         StopWatch silence, full;
397 
398         full.start();
399         while (true)
400         {
401             enforce(readed < arr.length,
402                     new SerialPortException("read more what can"));
403             size_t res;
404 
405             auto ptr = arr.ptr + readed;
406             auto len = arr.length - readed;
407 
408             version (Posix)
409             {
410                 auto sres = posixRead(handle, ptr, len);
411 
412                 if (sres < 0 && errno == EAGAIN) // no bytes for read, it's ok
413                     sres = 0;
414                 else
415                 {
416                     enforce(sres >= 0,
417                             new ReadException(port, text("errno ", errno)));
418                     res = sres;
419                 }
420             }
421             version (Windows)
422             {
423                 uint sres;
424                 auto rfr = ReadFile(handle, ptr, cast(uint)len, &sres, null);
425                 if (!rfr)
426                 {
427                     auto err = GetLastError();
428                     if (err == ERROR_IO_PENDING) { /+ asynchronously +/ }
429                     else throw new ReadException(port, text("error ", err));
430                 }
431                 res = sres;
432             }
433 
434             readed += res;
435 
436             if (res == 0)
437             {
438                 if (readed > 0 && silence.peek().to!Duration > frameGap)
439                     return arr[0..readed];
440 
441                 if (!silence.running) silence.start();
442             }
443             else
444             {
445                 silence.stop();
446                 silence.reset();
447             }
448 
449             if (readed == 0 && full.peek().to!Duration > timeout)
450                 throw new TimeoutException(port);
451 
452             yield();
453         }
454     }
455 
456 protected:
457 
458     version (Posix)
459     {
460         void setUintBaudRate(uint br)
461         {
462             version(usetermios2)
463             {
464                 enum CBAUD  = octal!10017;
465                 enum BOTHER = octal!10000;
466 
467                 termios2 opt2;
468                 enforce(ioctl(handle, TCGETS2, &opt2) != -1,
469                         new SetupFailException(port, "can't get termios2 options"));
470                 opt2.c_cflag &= ~CBAUD; //Remove current BAUD rate
471                 opt2.c_cflag |= BOTHER; //Allow custom BAUD rate using int input
472                 opt2.c_ispeed = br;     //Set the input BAUD rate
473                 opt2.c_ospeed = br;     //Set the output BAUD rate
474                 ioctl(handle, TCSETS2, &opt2);
475             }
476             else
477             {
478                 enforce(br in unixBaudList,
479                         new BaudRateUnsupportedException(br));
480 
481                 auto baud = unixBaudList[br];
482 
483                 termios opt;
484                 enforce(tcgetattr(handle, &opt) != -1,
485                     new SerialPortException(port, "can't get termios options"));
486 
487                 //cfsetispeed(&opt, B0);
488                 cfsetospeed(&opt, baud);
489 
490                 enforce(tcsetattr(handle, TCSANOW, &opt) != -1,
491                         new SerialPortException("Failed while call tcsetattr"));
492             }
493         }
494 
495         uint getUintBaudRate()
496         {
497             version (usetermios2)
498             {
499                 termios2 opt2;
500                 enforce(ioctl(handle, TCGETS2, &opt2) != -1,
501                         new SetupFailException(port, "can't get termios2 options"));
502                 return opt2.c_ospeed;
503             }
504             else
505             {
506                 termios opt;
507                 enforce(tcgetattr(handle, &opt) != -1,
508                     new SerialPortException(port, "can't get termios options"));
509                 auto b = cfgetospeed(&opt);
510                 if (b !in unixUintBaudList)
511                 {
512                     warningf("unknown baud speed setted: %s", b);
513                     return 0;
514                 }
515                 return unixUintBaudList[b];
516             }
517         }
518     }
519 
520     /// open handler, set new config
521     final void setup(Config conf)
522     {
523         enforce(port.length, new SetupFailException(port, "zero length name"));
524 
525         version (Posix)
526         {
527             handle = open(port.toStringz(), O_RDWR | O_NOCTTY | O_NONBLOCK);
528             enforce(handle != -1,
529                     new SetupFailException(port,
530                         format("Can't open port (errno %d)", errno)));
531 
532             termios opt;
533             enforce(tcgetattr(handle, &opt) != -1,
534                 new SetupFailException(port, "can't get termios options"));
535 
536             // make raw
537             opt.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP |
538                              INLCR | IGNCR | ICRNL | IXON);
539             opt.c_oflag &= ~OPOST;
540             opt.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
541             opt.c_cflag &= ~(CSIZE | PARENB);
542             opt.c_cflag |= CS8;
543 
544             enforce(tcsetattr(handle, TCSANOW, &opt) != -1,
545                     new SetupFailException(format("Failed while" ~
546                             " call tcsetattr (errno %d)", errno)));
547         }
548         else version (Windows)
549         {
550             auto fname = `\\.\` ~ port;
551             handle = CreateFileA(fname.toStringz,
552                         GENERIC_READ | GENERIC_WRITE, 0, null,
553                         OPEN_EXISTING, 0, null);
554 
555             if(handle is INVALID_HANDLE_VALUE)
556             {
557                 auto err = GetLastError();
558                 throw new SetupFailException(port,
559                         format("can't CreateFileA '%s' with error: %d", fname, err));
560             }
561 
562             SetupComm(handle, 4096, 4096);
563             PurgeComm(handle, PURGE_TXABORT | PURGE_TXCLEAR |
564                               PURGE_RXABORT | PURGE_RXCLEAR);
565 
566             COMMTIMEOUTS tm;
567             tm.ReadIntervalTimeout         = DWORD.max;
568             tm.ReadTotalTimeoutMultiplier  = 0;
569             tm.ReadTotalTimeoutConstant    = 0;
570             tm.WriteTotalTimeoutMultiplier = 0;
571             tm.WriteTotalTimeoutConstant   = 0;
572 
573             if (SetCommTimeouts(handle, &tm) == 0)
574                 throw new SetupFailException(port,
575                         format("can't SetCommTimeouts with error: %d", GetLastError()));
576         }
577 
578         config = conf;
579     }
580 }
581 
582 private bool hasFlag(A,B)(A a, B b) @property { return (a & b) == b; }