1 ///
2 module serialport.base;
3 
4 package import std.algorithm;
5 package import std.array;
6 package import std.conv : to, text;
7 package import std.exception;
8 package import std.experimental.logger;
9 package import std.path;
10 package import std.string;
11 package import std.datetime.stopwatch : StopWatch, AutoStart;
12 package import core.time;
13 
14 package import serialport.config;
15 package import serialport.exception;
16 package import serialport.types;
17 package import serialport.util;
18 
19 /++
20  +/
21 abstract class SerialPortBase
22 {
23 protected:
24     /// 
25     string port;
26 
27     SPHandle _handle = initHandle;
28 
29 public:
30 
31     ///
32     alias Config = SPConfig;
33 
34     /++ Construct SerialPort using extend mode string.
35 
36         First part of extend mode string must have port name
37         (e.g. "com1" or "/dev/ttyUSB0"), second part is equal
38         to config mode string. Parts separates by `modeSplitChar` (`:`).
39 
40         Example extend mode string: "/dev/ttyUSB0:9600:8N1"
41 
42         Params:
43             exmode = extend mode string
44 
45         See_Also: Config.parse, Config.set(string mode)
46 
47         Throws:
48             ParseModeException
49      +/
50     this(string exmode)
51     {
52         auto s = exmode.split(modeSplitChar);
53         if (s.length == 0) throw new ParseModeException("empty config mode");
54         this(s[0], s.length > 1 ? Config.parse(s[1..$].join(modeSplitChar)) : Config.init);
55     }
56 
57     /++ Construct SerialPort using port name and mode string.
58 
59         Params:
60             port = port name
61             mode = config mode string
62 
63         See_Also: Config.parse, Config.set(string mode)
64      +/
65     this(string port, string mode) { this(port, Config.parse(mode)); }
66 
67     /++ Params:
68             port = port name
69             baudRate = baudrate
70      +/
71     this(string port, uint baudRate) { this(port, Config(baudRate)); }
72 
73     /++ Params:
74             port = port name
75             baudRate = baudrate
76             mode = config mode string
77 
78         See_Also: Config.parse, Config.set(string mode)
79      +/
80     this(string port, uint baudRate, string mode)
81     { this(port, Config(baudRate).set(mode)); }
82 
83     /++ Params:
84             port = port name
85             conf = config of serialport
86      +/
87     this(string port, Config conf) { reopen(port, conf); }
88 
89     ~this() { close(); }
90 
91     /// Close handle
92     void close() @nogc
93     {
94         if (closed) return;
95         closeHandle(_handle);
96         _handle = initHandle;
97     }
98 
99     /// Port name
100     string name() const @nogc @property { return port; }
101 
102     ///
103     inout(SPHandle) handle() inout @nogc @property { return _handle; }
104 
105     ///
106     void reopen(string np, Config cfg)
107     {
108         if (!closed) close();
109         port = np;
110         setup(cfg);
111     }
112 
113     ///
114     void reopen(string np) { reopen(np, config); }
115     ///
116     void reopen(Config cfg) { reopen(port, cfg); }
117     ///
118     void reopen() { reopen(port, config); }
119 
120     /++ Returns extend mode string (example: "/dev/ttyUSB0:38400:8N1")
121      +/
122     override string toString() const { return port ~ modeSplitChar ~ config.mode; }
123 
124     /++ Set config value
125         Params:
126             T = typeof of parameter, avalable:
127                     int -> baudrate,
128                     DataBit -> dataBits,
129                     StopBits -> stopBits,
130                     Parity -> parity
131             val = value
132      +/
133     typeof(this) set(T)(T val) @nogc if (is(typeof(Config.init.set(val))))
134     {
135         Config tmp = config;
136         tmp.set(val);
137         config = tmp;
138         return this;
139     }
140 
141     /// Set config mode string
142     typeof(this) set(string val)
143     {
144         Config tmp = config;
145         tmp.set(val);
146         config = tmp;
147         return this;
148     }
149 
150     ///
151     bool closed() const @property @nogc nothrow
152     {
153         version (Posix) return _handle == initHandle;
154         version (Windows) return _handle is initHandle;
155     }
156 
157     /++ Get config
158 
159         const for disallow `com.config.set(value)`
160         use `com.set(value)` instead
161      +/
162     const(Config) config() @property const @nogc
163     {
164         if (closed) throwPortClosedException(port);
165 
166         Config ret;
167 
168         version (Posix)
169         {
170             termios opt;
171             m_tcgetattr(&opt);
172 
173             ret.baudRate = getUintBaudRate();
174 
175             if (opt.c_cflag.hasFlag(PARODD)) ret.parity = Parity.odd;
176             else if (!(opt.c_cflag & PARENB)) ret.parity = Parity.none;
177             else ret.parity = Parity.even;
178 
179                  if (opt.c_cflag.hasFlag(CS8)) ret.dataBits = DataBits.data8;
180             else if (opt.c_cflag.hasFlag(CS7)) ret.dataBits = DataBits.data7;
181             else if (opt.c_cflag.hasFlag(CS6)) ret.dataBits = DataBits.data6;
182             else if (opt.c_cflag.hasFlag(CS5)) ret.dataBits = DataBits.data5;
183             else throwSerialPortException(port, "unknown flags for databits");
184 
185             ret.stopBits = opt.c_cflag.hasFlag(CSTOPB) ? StopBits.two : StopBits.one;
186         }
187         version (Windows)
188         {
189             DCB cfg;
190             GetCommState(cast(SPHandle)_handle, &cfg);
191 
192             ret.baudRate = cast(uint)cfg.BaudRate;
193 
194             switch (cfg.Parity)
195             {
196                 case NOPARITY:   ret.parity = Parity.none; break;
197                 case ODDPARITY:  ret.parity = Parity.odd; break;
198                 case EVENPARITY: ret.parity = Parity.even; break;
199                 default: throwSerialPortException(port, "unknown parity"); break;
200             }
201 
202             if (cfg.ByteSize < 5 || cfg.ByteSize > 8)
203                 throwSerialPortException(port, "unknown databist count");
204             ret.dataBits = cast(DataBits)cfg.ByteSize;
205 
206             ret.stopBits = cfg.StopBits == ONESTOPBIT ? StopBits.one : StopBits.two;
207         }
208 
209         return ret;
210     }
211 
212     /// Set config
213     void config(Config c) @property @nogc
214     {
215         if (closed) throwPortClosedException(port);
216 
217         version (Posix)
218         {
219             setUintBaudRate(c.baudRate);
220 
221             termios opt;
222             m_tcgetattr(&opt);
223 
224             final switch (c.parity)
225             {
226                 case Parity.none:
227                     opt.c_cflag &= ~PARENB;
228                     break;
229                 case Parity.odd:
230                     opt.c_cflag |= (PARENB | PARODD);
231                     break;
232                 case Parity.even:
233                     opt.c_cflag &= ~PARODD;
234                     opt.c_cflag |= PARENB;
235                     break;
236             }
237 
238             final switch (c.stopBits)
239             {
240                 case StopBits.one:
241                     opt.c_cflag &= ~CSTOPB;
242                     break;
243                 case StopBits.onePointFive:
244                 case StopBits.two:
245                     opt.c_cflag |= CSTOPB;
246                     break;
247             }
248 
249             opt.c_cflag &= ~CSIZE;
250             final switch (c.dataBits) with (DataBits)
251             {
252                 case data5: opt.c_cflag |= CS5; break;
253                 case data6: opt.c_cflag |= CS6; break;
254                 case data7: opt.c_cflag |= CS7; break;
255                 case data8: opt.c_cflag |= CS8; break;
256             }
257 
258             m_tcsetattr(TCSANOW, &opt);
259 
260             const test = config;
261             if (test.baudRate != c.baudRate) throwUnsupportedException(port, c.baudRate);
262             if (test.parity   != c.parity)   throwUnsupportedException(port, c.parity);
263             if (test.stopBits != c.stopBits) throwUnsupportedException(port, c.stopBits);
264             if (test.dataBits != c.dataBits) throwUnsupportedException(port, c.dataBits);
265         }
266         version (Windows)
267         {
268             DCB cfg;
269             GetCommState(_handle, &cfg);
270 
271             if (cfg.BaudRate != cast(DWORD)c.baudRate)
272             {
273                 cfg.BaudRate = cast(DWORD)c.baudRate;
274                 if (!SetCommState(_handle, &cfg))
275                     throwUnsupportedException(port, c.baudRate);
276             }
277 
278             auto tmpParity = NOPARITY;
279             if (c.parity == Parity.odd) tmpParity = ODDPARITY;
280             if (c.parity == Parity.even) tmpParity = EVENPARITY;
281 
282             if (cfg.Parity != tmpParity)
283             {
284                 cfg.Parity = cast(ubyte)tmpParity;
285                 if (!SetCommState(_handle, &cfg))
286                     throwUnsupportedException(port, c.parity);
287             }
288 
289             auto tmpStopBits = ONESTOPBIT;
290             if (c.stopBits == StopBits.two) tmpStopBits = TWOSTOPBITS;
291 
292             if (cfg.StopBits != tmpStopBits)
293             {
294                 cfg.StopBits = cast(ubyte)tmpStopBits;
295                 if (!SetCommState(_handle, &cfg))
296                     throwUnsupportedException(port, c.stopBits);
297             }
298 
299             if (cfg.ByteSize != cast(typeof(cfg.ByteSize))c.dataBits)
300             {
301                 cfg.ByteSize = cast(typeof(cfg.ByteSize))c.dataBits;
302                 if (!SetCommState(_handle, &cfg))
303                     throwUnsupportedException(port, c.dataBits);
304             }
305         }
306     }
307 
308     @property @nogc
309     {
310         ///
311         Parity parity() { return config.parity; }
312         ///
313         uint baudRate() { return config.baudRate; }
314         ///
315         DataBits dataBits() { return config.dataBits; }
316         ///
317         StopBits stopBits() { return config.stopBits; }
318 
319         ///
320         Parity parity(Parity v) { set(v); return v; }
321         ///
322         uint baudRate(uint v) { set(v); return v; }
323         ///
324         DataBits dataBits(DataBits v) { set(v); return v; }
325         ///
326         StopBits stopBits(StopBits v) { set(v); return v; }
327     }
328 
329     /++ List of available serial ports in system
330         +/
331     static string[] listAvailable() @property
332     {
333         version (linux)
334         {
335             import std.file : exists;
336             return dirEntries("/sys/class/tty", SpanMode.shallow)
337                     .map!(a=>"/dev/"~a.name.baseName)
338                     .filter!(a=>a.exists)
339                     .array.sort.array
340                     ~
341                     dirEntries("/dev/pts", SpanMode.shallow)
342                     .map!(a=>a.name).array.sort.array;
343         }
344         version (OSX)
345         {
346             return dirEntries("/dev/", "{tty,cu}*", SpanMode.shallow)
347                     .map!(a=>a.name).array;
348         }
349         version (Windows)
350         {
351             import std.windows.registry : Registry;
352             string[] arr;
353             try foreach (v; Registry
354                             .localMachine()
355                             .getKey("HARDWARE")
356                             .getKey("DEVICEMAP")
357                             .getKey("SERIALCOMM")
358                             .values)
359                 arr ~= v.value_SZ;
360             catch (Throwable e) .error(e.msg);
361             return arr;
362         }
363     }
364 
365 protected:
366     void[] m_read(void[] buf) @nogc
367     {
368         // non-blocking algorithm
369         if (closed) throwPortClosedException(port);
370 
371         auto ptr = buf.ptr;
372         auto len = buf.length;
373 
374         size_t res;
375 
376         version (Posix)
377         {
378             auto sres = posixRead(_handle, ptr, len);
379 
380             // no bytes for read, it's ok
381             if (sres < 0)
382             {
383                 if (errno == EAGAIN) sres = 0;
384                 else throwReadException(port, "posix read", errno);
385             }
386             res = sres;
387         }
388         version (Windows)
389         {
390             uint sres;
391             auto rfr = ReadFile(_handle, ptr, cast(uint)len, &sres, null);
392             if (!rfr)
393             {
394                 auto err = GetLastError();
395                 if (err == ERROR_IO_PENDING) { /+ buffer empty +/ }
396                 else throwReadException(port, "win read", err);
397             }
398             res = sres;
399         }
400 
401         return buf[0..res];
402     }
403 
404     size_t m_write(const(void[]) arr) @nogc
405     {
406         // non-blocking algorithm
407         if (closed) throwPortClosedException(port);
408 
409         auto ptr = arr.ptr;
410         auto len = arr.length;
411 
412         version (Posix)
413         {
414             ptrdiff_t res = posixWrite(_handle, ptr, len);
415             if (res < 0)
416             {
417                 if (errno == EAGAIN) res = 0; // buffer filled
418                 else throwWriteException(port, "posix write", errno);
419             }
420         }
421         version (Windows)
422         {
423             uint res;
424             auto wfr = WriteFile(_handle, ptr, cast(uint)len, &res, null);
425             if (!wfr)
426             {
427                 auto err = GetLastError();
428                 if (err == ERROR_IO_PENDING) res = 0;
429                 else throwWriteException(port, "win write", err);
430             }
431         }
432 
433         return res;
434     }
435 
436     /// open handler, set new config
437     void setup(Config conf)
438     {
439         if (port.length == 0)
440             throwSerialPortException("", "zero length name");
441 
442         version (Posix) posixSetup(conf);
443         else winSetup();
444 
445         config = conf;
446     }
447 
448     version (Posix)
449     {
450         void m_tcgetattr(termios* t) const @nogc
451         {
452             if (tcgetattr(_handle, t) == -1)
453                 throwSysCallException(port, "tcgetattr", errno);
454         }
455 
456         void m_tcsetattr(int v, const(termios*) t) inout @nogc
457         {
458             if (tcsetattr(_handle, v, t) == -1)
459                 throwSysCallException(port, "tcsetattr", errno);
460         }
461 
462         version (usetermios2)
463         {
464             void m_ioctl(int v, termios2* t) inout
465             {
466                 if (ioctl(_handle, v, t) == -1)
467                     throwSysCallException(port, "ioctl", errno);
468             }
469         }
470 
471         void posixSetup(Config conf)
472         {
473             openPort();
474             initialConfig(conf);
475         }
476 
477         void openPort()
478         {
479             _handle = open(port.toStringz(), O_RDWR | O_NOCTTY | O_NONBLOCK);
480             if (_handle == -1)
481                 throwSysCallException(port, "open", errno);
482         }
483 
484         /// Set termios.c_cc[VMIN] and .c_cc[VMAX]
485         void setCC(ubyte[2] val) @nogc
486         {
487             termios opt;
488             m_tcgetattr(&opt);
489             opt.c_cc[VMIN] = val[0];
490             opt.c_cc[VTIME] = val[1];
491             m_tcsetattr(TCSADRAIN, &opt);
492         }
493 
494         /// Get termios.c_cc[VMIN] and .c_cc[VMAX]
495         ubyte[2] getCC() @nogc
496         {
497             ubyte[2] ret;
498             termios opt;
499             m_tcgetattr(&opt);
500             ret[0] = opt.c_cc[VMIN];
501             ret[1] = opt.c_cc[VTIME];
502             return ret;
503         }
504 
505         void initialConfig(Config conf)
506         {
507             termios opt;
508             m_tcgetattr(&opt);
509 
510             // make raw
511             opt.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP |
512                              INLCR | IGNCR | ICRNL | IXON);
513             opt.c_oflag &= ~OPOST;
514             opt.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
515             opt.c_cflag &= ~(CSIZE | PARENB);
516             opt.c_cc[VMIN] = 0;
517             opt.c_cc[VTIME] = 0;
518 
519             // hardware flow control
520             version (OSX)
521             {
522                 /+
523                 The CCTS_OFLOW (CRTSCTS) flag is currently unused.
524                 http://www.manpages.info/macosx/termios.4.html
525                  +/
526             }
527             else
528             {
529                 if (conf.hardwareDisableFlowControl)
530                     opt.c_cflag &= ~CRTSCTS;
531             }
532 
533             opt.c_cflag |= CS8;
534 
535             m_tcsetattr(TCSANOW, &opt);
536         }
537 
538         void setUintBaudRate(uint br) @nogc
539         {
540             version (usetermios2)
541             {
542                 import std.conv : octal;
543                 enum CBAUD  = octal!10017;
544                 enum BOTHER = octal!10000;
545 
546                 termios2 opt2;
547                 m_ioctl(TCGETS2, &opt2);
548                 opt2.c_cflag &= ~CBAUD; //Remove current BAUD rate
549                 opt2.c_cflag |= BOTHER; //Allow custom BAUD rate using int input
550                 opt2.c_ispeed = br;     //Set the input BAUD rate
551                 opt2.c_ospeed = br;     //Set the output BAUD rate
552                 m_ioctl(TCSETS2, &opt2);
553             }
554             else
555             {
556                 if (unixBaudList.countA(br) == 0)
557                     throwUnsupportedException(port, br);
558 
559                 auto baud = unixBaudList.firstA2B(br, B0);
560 
561                 termios opt;
562                 m_tcgetattr(&opt);
563                 cfsetispeed(&opt, B0);
564                 cfsetospeed(&opt, baud);
565                 m_tcsetattr(TCSANOW, &opt);
566             }
567         }
568 
569         uint getUintBaudRate() const @nogc
570         {
571             version (usetermios2)
572             {
573                 termios2 opt2;
574                 m_ioctl(TCGETS2, &opt2);
575                 return opt2.c_ospeed;
576             }
577             else
578             {
579                 termios opt;
580                 m_tcgetattr(&opt);
581                 version (OSX) alias true_speed_t = uint;
582                 else alias true_speed_t = typeof(cfgetospeed(&opt));
583                 auto b = cast(true_speed_t)cfgetospeed(&opt);
584                 return unixBaudList.firstB2A(b, 0);
585             }
586         }
587     }
588 
589     version (Windows)
590     {
591         void winSetup()
592         {
593             auto fname = `\\.\` ~ port;
594             _handle = CreateFileA(fname.toStringz,
595                         GENERIC_READ | GENERIC_WRITE, 0, null,
596                         OPEN_EXISTING, 0, null);
597 
598             if (_handle is INVALID_HANDLE_VALUE)
599                 throwSysCallException(port, "CreateFileA", GetLastError());
600 
601             SetupComm(_handle, 4096, 4096);
602             PurgeComm(_handle, PURGE_TXABORT | PURGE_TXCLEAR |
603                                PURGE_RXABORT | PURGE_RXCLEAR);
604             
605             updTimeouts();
606         }
607 
608         void updTimeouts() @nogc
609         {
610             setTimeouts(DWORD.max, 0, 0, 0, 0);
611         }
612 
613         void setTimeouts(DWORD rit, DWORD rttm, DWORD rttc, DWORD wttm, DWORD wttc) @nogc
614         {
615             COMMTIMEOUTS tm;
616             tm.ReadIntervalTimeout         = rit;
617             tm.ReadTotalTimeoutMultiplier  = rttm;
618             tm.ReadTotalTimeoutConstant    = rttc;
619             tm.WriteTotalTimeoutMultiplier = wttm;
620             tm.WriteTotalTimeoutConstant   = wttc;
621 
622             if (SetCommTimeouts(_handle, &tm) == 0)
623                 throwSysCallException(port, "SetCommTimeouts", GetLastError());
624         }
625     }
626 }
627 
628 /++ Timed work with serial port
629 
630  +/
631 abstract class SerialPort : SerialPortBase
632 {
633 protected:
634 
635     Duration _writeTimeout = 1.seconds,
636              _writeTimeoutMult = Duration.zero,
637              _readTimeout = 1.seconds,
638              _readTimeoutMult = Duration.zero;
639 
640 
641     void updateTimeouts() @nogc {}
642     
643 public:
644 
645     /++ Construct SerialPort
646 
647         See_Also: SerialPortBase.this
648      +/
649     this(string exmode) { super(exmode); }
650 
651     /// ditto
652     this(string port, string mode) { super(port, mode); }
653 
654     /// ditto
655     this(string port, uint baudRate) { super(port, baudRate); }
656 
657     /// ditto
658     this(string port, uint baudRate, string mode)
659     { super(port, baudRate, mode); }
660 
661     /// ditto
662     this(string port, Config conf) { super(port, conf); }
663 
664     /++ Read data from serial port while exists
665      +/
666     void flush()
667     {
668         void[128] buf = void;
669         const rt = _readTimeout;
670         const rtm = _readTimeoutMult;
671 
672         _readTimeout = 10.msecs;
673         _readTimeoutMult = Duration.zero;
674         updateTimeouts();
675 
676         version (Posix)
677         {
678             const last = getCC();
679             setCC([0,0]);
680         }
681 
682         void[] tmp;
683         do tmp = read(buf, CanRead.zero);
684         while (tmp.length);
685 
686         version (Posix)
687         {
688             setCC(last);
689         }
690 
691         _readTimeout = rt;
692         _readTimeoutMult = rtm;
693         updateTimeouts();
694     }
695 
696     @property @nogc
697     {
698         const
699         {
700             ///
701             Duration readTimeout() { return _readTimeout; }
702             ///
703             Duration readTimeoutMult() { return _readTimeoutMult; }
704             ///
705             Duration writeTimeout() { return _writeTimeout; }
706             ///
707             Duration writeTimeoutMult() { return _writeTimeoutMult; }
708         }
709 
710         ///
711         void readTimeout(Duration tm)
712         {
713             _readTimeout = tm;
714             updateTimeouts();
715         }
716 
717         ///
718         void readTimeoutMult(Duration tm)
719         {
720             _readTimeoutMult = tm;
721             updateTimeouts();
722         }
723 
724         ///
725         void writeTimeout(Duration tm)
726         {
727             _writeTimeout = tm;
728             updateTimeouts();
729         }
730 
731         ///
732         void writeTimeoutMult(Duration tm)
733         {
734             _writeTimeout = tm;
735             updateTimeouts();
736         }
737     }
738 
739     ///
740     enum CanRead
741     {
742         allOrNothing, ///
743         anyNonZero, ///
744         zero ///
745     }
746 
747     /++ Read data from port
748 
749         Receive data time schema:
750 
751         ------
752         ---|-------|--------------|-------|--> t
753          call      |              |       |
754          read      |              |       |
755            |       |              |       |
756            |       |<data receive process>|
757            |       |=====   ====  | ======|
758            |       |              |
759            |       |<-readedData->|
760            |                      | 
761            |<---readTimeoutSum--->|
762            |                    return
763            |<---read work time--->|
764         ------
765 
766         where `readTimeoutSum = readTimeout + readTimeoutMult * dataBuffer.length;`
767 
768         if canReturn is:
769         
770         CanRead.allOrNothing
771 
772         ---
773         if (readedData.length < dataBuffer.length)
774             throw TimeoutException(port);
775         else return readedData;
776         ---
777 
778         CanReturn.anyNonZero
779 
780         ---
781         if (readedData.length == 0)
782             throw TimeoutException(port);
783         else return readedData;
784         ---
785 
786         CanReturn.zero
787 
788         ---
789         return readedData;
790         ---
791 
792         Params:
793             buf = preallocated buffer for reading
794             cr = flag what define behavior if readedData.length < buf.length
795                  then readTimeoutSum is expires
796 
797         Returns: slice of buf with readed data
798         Throws:
799             PortClosedException if port closed
800             ReadException if read error occurs
801             TimeoutException if timeout expires
802      +/
803     abstract void[] read(void[] buf, CanRead cr=CanRead.allOrNothing) @nogc;
804 
805     ///
806     protected void checkAbility(CanRead cr, size_t readed, size_t buffer) @nogc
807     {
808         bool err;
809 
810         final switch (cr) with(CanRead)
811         {
812             case allOrNothing: err = readed != buffer; break;
813             case anyNonZero:   err = readed == 0; break;
814             case zero: /+ no errors +/ break;
815         }
816 
817         if (err) throwTimeoutException(port, "read timeout");
818     }
819 
820     /++ Write data to port
821 
822         Params:
823             arr = data for writing
824 
825         Throws:
826             PortClosedException if port closed
827             WriteException if read error occurs
828             TimeoutException if timeout expires
829      +/
830     abstract void write(const(void[]) buf) @nogc;
831 }