1 /**
2  * Serial port library
3  *
4  * Copyright: © 2014-2021
5  * License: MIT license. License terms written in licence.txt file
6  *
7  * Authors: Oleg Nykytenko, onyx.itdevelopment@gmail.com
8  *
9  * Version: 0.xx Date: 2014
10  */
11 
12 module onyx.serial;
13 
14 
15 import onyx.bundle;
16 
17 import std.string;
18 import std.conv;
19 import core.thread;
20 
21 import std.stdio:StdioException;
22 
23 
24 
25 /**
26  * Base Serial port exception
27  */
28 abstract class SerialPortException: StdioException
29 {
30     private string _port;
31 
32     string port() {return _port;}
33 
34     this(string port, string msg)
35     {
36         super(msg);
37     }
38 }
39 
40 
41 /**
42  * Construct exception with parrent: SerialPortException
43  */
44 template childSerialPortException(string exceptionName)
45 {
46     const char[] childSerialPortException =
47 
48     "class " ~exceptionName~":SerialPortException
49     {
50         this(string port, string msg)
51         {
52         	super(port, msg);
53         }
54     }";
55 }
56 
57 
58 /**
59  * Realize child Exceptions
60  */
61 mixin(childSerialPortException!"SerialPortSetupException");
62 mixin(childSerialPortException!"SerialPortOpenException");
63 mixin(childSerialPortException!"SerialPortCloseException");
64 mixin(childSerialPortException!"SerialPortIOException");
65 mixin(childSerialPortException!"SerialPortTimeOutException");
66 
67 
68 template getterImplMember(string type, string name)
69 {
70     const char[] getterImplMember =
71 
72     type ~ " " ~ name ~ "()
73     {
74         return impl." ~ name ~ ";
75     }";
76 }
77 
78 
79 
80 
81 
82 
83 
84 mixin (genSpeed);
85 
86 
87 /**
88  * Serial port.
89  *
90  * Hardware independent level
91  */
92 struct OxSerialPort
93 {
94     /**
95      * Low level Serial port implementation
96      */
97     private Impl impl;
98 
99 
100     mixin(getterImplMember!("string", "name"));
101 
102 
103     /**
104      * Create new serial port
105      */
106     pure
107     nothrow
108     this(string portName, Speed speed = Speed.S9600, Parity parity = Parity.none, uint timeOut = 0)
109     {
110         impl =  Impl(portName, speed, parity, timeOut);
111     }
112 
113 
114     /**
115      * Create new serial port
116      *
117      * Throws: SerialPortSetupException
118      */
119     deprecated
120     this(string portName, uint speed = 9600, string parity = "none", uint timeOut = 0)
121     {
122         impl =  Impl(portName, speed, parity, timeOut);
123     }
124 
125 
126     /**
127      * Create new serial port
128      *
129      * Throws: SerialPortSetupException
130      * Throws: ValueNotFoundException, GlKeyNotFoundException, BundleException
131      * Throws: ConvException, ConvOverflowException
132      */
133     this(immutable Bundle bundle)
134     {
135         string extractName()
136         {
137             string name;
138             try
139             {
140                 name = bundle.value("port", "name");
141             }
142             catch(KeyNotFoundException ke)
143             {
144                 throw new BundleException("Not found port name: " ~ke.msg);
145             }
146             return name;
147         }
148 
149         uint extractSpeed()
150         {
151             uint portSpeed;
152             try
153             {
154                 portSpeed = bundle.value!uint("port", "speed");
155             }
156             catch(KeyNotFoundException ke)
157             {
158                 portSpeed = 9600;
159             }
160             return portSpeed;
161         }
162 
163         string extractParity()
164         {
165             string parity;
166             try
167             {
168                 parity = bundle.value("port", "parity");
169             }
170             catch(KeyNotFoundException ke)
171             {
172                 parity = "none";
173             }
174             return parity;
175         }
176 
177         uint extractTimeOut()
178         {
179             uint portTimeOut;
180             try
181             {
182                 portTimeOut = bundle.value!uint("port", "time_out");
183             }
184             catch(KeyNotFoundException ke)
185             {
186                 portTimeOut = 0;
187             }
188             return portTimeOut;
189         }
190 
191         impl =  Impl(extractName, extractSpeed, extractParity, extractTimeOut);
192     }
193 
194 
195     ~this()
196     {
197     	close();
198     }
199 
200 
201     /**
202      * Open serial port (and setup)
203      *
204      * Return true if port opened
205      *
206      * Throws: SerialPortOpenException, SerialPortSetupException
207      */
208     bool open()
209     {
210         scope(failure) close();
211         if (!impl.isOpen())
212         {
213             impl.open();
214             impl.setup();
215         }
216         else
217         {
218             throw new SerialPortOpenException(impl.name, " Port already opened");
219         }
220         return impl.isOpen();
221     }
222 
223 
224     /**
225      * Close serial port
226      *
227      * Return true if port closed
228      *
229      * Throws: SerialPortCloseException
230      */
231     bool close()
232     {
233         if (impl.isOpen())
234         {
235             impl.close();
236         }
237         return !impl.isOpen();
238     }
239 
240 
241     /**
242      * write data to port
243      *
244      * Throws: SerialPortIOException
245      */
246     void write(ubyte[] buf)
247     {
248         impl.write(buf);
249     }
250 
251 
252     /**
253      * read data from port
254      *
255      * Throws: SerialPortIOException, SerialPortTimeOutException
256      */
257     ubyte[] read(int byteCount, ReadMode readMode = ReadMode.waitForTimeout)
258     {
259         return impl.read(byteCount, readMode);
260     }
261 
262     /**
263      * read data from port
264      *
265      * Throws: SerialPortIOException, SerialPortTimeOutException
266      */
267     deprecated("Please use read(int, ReadMode) instead")
268     ubyte[] read(int byteCount, bool wait)
269     {
270         return impl.read(byteCount, wait ? ReadMode.waitForTimeout : ReadMode.noWait);
271     }
272 
273 
274     /**
275      * Check if port open
276      *
277      * Return true if port open
278      *
279      */
280     bool isOpen() nothrow
281     {
282         return impl.isOpen();
283     }
284 }
285 
286 
287 
288 /**
289  * Parity variants
290  *
291  */
292 enum Parity:string
293 {
294     none = "none",
295     odd = "odd",
296     even = "even",
297     //mark = "mark",
298     //space = "space",
299     error = "error"
300 }
301 
302 
303 
304 /**
305  * Generation Speed type
306  *
307  * Use example: Speed.s57600
308  */
309 const (char[]) genSpeed()
310 {
311     const (char)[] res = "enum Speed:uint {";
312     foreach (speed; speedsRange)
313     {
314         res = res ~ 'S' ~ speed ~ " = " ~ speed ~ ", ";
315     }
316     res ~= '}';
317     return res;
318 }
319 
320 /**
321  * Speed variants
322  *
323  */
324 version (linux)
325 {
326     const char[][] speedsRange = ["0", "50", "75", "110", "134", "150", "200", "300", "600", "1200", "1800", "2400",
327         "4800", "9600", "19200", "38400", "57600", "115200", "230400", "460800", "500000", "576000", "921600",
328         "1000000", "1152000", "1500000", "2000000", "2500000", "3000000", "3500000", "4000000"];
329 }
330 else version (OSX)
331 {
332     const char[][] speedsRange = ["0", "50", "75", "110", "134", "150", "200", "300", "600", "1200", "1800", "2400",
333         "4800", "9600", "19200", "38400", "7200", "14400", "28800", "57600", "76800", "115200", "230400"];
334 }
335 else version (FreeBSD)
336 {
337 
338     const char[][] speedsRange = ["0", "50", "75", "110", "134", "150", "200", "300", "600", "1200", "1800", "2400",
339         "4800", "9600", "19200", "38400", "7200", "14400", "28800", "57600", "76800", "115200", "230400", "460800", "921600"];
340 }
341 else version (Solaris)
342 {
343 
344     const char[][] speedsRange = ["0", "50", "75", "110", "134", "150", "200", "300", "600", "1200", "1800", "2400",
345         "4800", "9600", "19200", "38400", "57600", "76800", "115200", "153600", "230400", "307200", "460800", "921600"];
346 }
347 
348 
349 
350 /**
351  *	Variants for the read method
352  */
353 enum ReadMode
354 {
355     noWait,
356     waitForTimeout,
357     waitForData,
358     waitForAllData
359 }
360 
361 
362 version(Posix)
363 {
364     alias Impl = PosixImpl;
365 }
366 
367 
368 private struct PosixImpl
369 {
370 
371     import core.sys.posix.termios;
372     import core.sys.posix.fcntl;
373 
374     import core.sys.posix.poll;
375 
376     alias int Handle;
377 
378     private Handle handle = -1;
379 
380 
381     /**
382      * Port name.
383      * For posix systems as rule: /dev/tty*
384      */
385     private string _name;
386 
387     string name() pure nothrow
388     {
389         return _name;
390     }
391 
392 
393     /**
394      * Port speed.
395      * Must be from standard row: 0, 50, 75 .. 4_000_000
396      */
397     private Speed _speed;
398 
399     Speed speed() pure nothrow
400     {
401         return _speed;
402     }
403 
404 
405     /**
406      * Port parity.
407      * Must be from: "none, odd, even, mark, space"
408      */
409     private Parity _parity;
410 
411     Parity parity() pure nothrow
412     {
413         return _parity;
414     }
415 
416 
417     /**
418      * Port data read timeout.
419      * in msecs
420      */
421     private uint _readTimeOut;
422 
423     @property
424     uint readTimeOut() pure nothrow
425     {
426         return _readTimeOut;
427     }
428 
429 
430     /**
431      * Create serial port implementation
432      */
433     this(string name, Speed speed, Parity parity, uint timeOut) pure nothrow
434     {
435         _name = name;
436 
437         _speed = speed;
438 
439         _parity = parity;
440 
441         _readTimeOut = timeOut;
442     }
443 
444 
445 
446     /**
447      * Create serial port implementation
448      *
449      * Throws: SerialPortSetupException
450      */
451     this(string name, uint speed, string parity, uint timeOut)
452     {
453         _name = name;
454 
455         if (checkSpeed(speed))
456         {
457             _speed = getSpeedByNum(speed);
458         }
459         else
460         {
461             throw new SerialPortSetupException(name, "Invalid speed value: " ~ to!string(speed));
462         }
463 
464         if (checkParity(parity))
465         {
466             _parity = getParityByName(parity);
467         }
468         else
469         {
470             throw new SerialPortSetupException(name, "Invalid parity value: " ~ parity);
471         }
472 
473         _readTimeOut = timeOut;
474     }
475 
476 
477 
478 
479 
480     /**
481      * Open serial port
482      *
483      * Throws: SerialPortOpenException
484      */
485     void open()
486     {
487         Handle h = core.sys.posix.fcntl.open(name.toStringz, O_RDWR | O_NOCTTY | O_NONBLOCK);
488         if (h != -1)
489         {
490             handle = h;
491         }
492         else
493         {
494             throw new SerialPortOpenException(name, "Can't open serial port");
495         }
496     }
497 
498 
499     /**
500      * Close serial port
501      *
502      * Throws: SerialPortCloseException
503      */
504     void close()
505     {
506         static import core.sys.posix.unistd;
507         if (core.sys.posix.unistd.close(handle) == -1)
508         {
509             throw new SerialPortCloseException(name, "Can't close serial port");
510         }
511         handle = -1;
512     }
513 
514 
515     /**
516      * Setup serial port parameters
517      *
518      * Throws: SerialPortSetupException
519      */
520     void setup()
521     {
522         /* set flags */
523         setFlags();
524 
525         /* set speed */
526         setSpeed();
527         setParity();
528     }
529 
530 
531     /**
532      * Set port speed
533      *
534      * Throws: SerialPortSetupException
535      */
536     private void setSpeed()
537     {
538         auto set = getTermios();
539         speed_t baud = getBaudRateByNum(speed);
540 
541         if((cfsetispeed(&set, baud) < 0) || (cfsetospeed(&set, baud) < 0) || (tcsetattr(handle, TCSANOW, &set) == -1))
542         {
543             throw new SerialPortSetupException(name, "Can't set speed " ~ to!string(speed) ~ " for serial port");
544         }
545     }
546 
547 
548     /**
549      * Set port speed
550      *
551      * Throws: SerialPortSetupException
552      */
553     private void setParity()
554     {
555         auto set = getTermios();
556         /* clear parity bits */
557         set.c_cflag &= ~(PARENB | PARODD);
558         set.c_iflag &= ~INPCK;
559 
560         auto par = getParityByName(parity);
561         final switch(par)
562         {
563             case Parity.odd:
564                 set.c_cflag |= (PARENB | PARODD);
565                 set.c_iflag |= INPCK;
566                 break;
567             case Parity.even:
568                 set.c_cflag |= PARENB;
569                 set.c_iflag |= INPCK;
570                 break;
571             case Parity.none:
572                 break;
573             case Parity.error:
574         }
575         if (tcsetattr(handle, TCSANOW, &set) == -1)
576         {
577             throw new SerialPortSetupException(name, "Can't set parity " ~ par ~ " for serial port");
578         }
579     }
580 
581 
582     /**
583      * Set port flags
584      *
585      * Throws: SerialPortSetupException
586      */
587     private void setFlags()
588     {
589         auto set = getTermios();
590         /* set flags */
591         set.c_cflag |= (CREAD | CLOCAL);
592         set.c_cflag &= ~CRTSCTS;
593         set.c_lflag &= ~(ICANON | ECHO | ECHOE | ECHOK | ECHONL | ECHOCTL | ECHOPRT | ECHOKE | ISIG | IEXTEN);
594         set.c_iflag &= ~(IXON | IXOFF | IXANY | IGNPAR | PARMRK | ISTRIP | IGNBRK | BRKINT | INLCR | IGNCR| ICRNL);
595         set.c_oflag &= ~OPOST;
596 
597         /* Minimum number of characters as 0 and we don't want to use any timer */
598         set.c_cc[VMIN] = 0;
599         set.c_cc[VTIME] = 0;
600 
601         if (tcsetattr(handle, TCSANOW, &set) == -1)
602         {
603             throw new SerialPortSetupException(name, "Can't save setup for serial port");
604         }
605     }
606 
607 
608     /**
609      * get termios structure
610      *
611      * Throws: SerialPortSetupException
612      */
613     private termios getTermios()
614     {
615         termios set;
616         if (tcgetattr(handle, &set) < 0)
617         {
618             throw new SerialPortSetupException(name, "Can't setup serial port");
619         }
620         return set;
621     }
622 
623 
624 
625     /**
626      * open port check
627      */
628     bool isOpen() nothrow//@safe pure nothrow
629     {
630         return (handle > 0)?true:false;
631     }
632 
633 
634     /**
635      * write data to port
636      *
637      * Throws: SerialPortIOException
638      */
639     void write(ubyte[] buf)
640     {
641         static import core.sys.posix.unistd;
642         if (core.sys.posix.unistd.write(handle, cast(void *)buf.ptr, buf.length) == -1)
643         {
644             throw new SerialPortIOException(name, "Error Writing to serial port");
645         }
646     }
647 
648 
649     /**
650      * read data from port
651      *
652      * Throws: SerialPortIOException, SerialPortTimeOutException
653      */
654     ubyte[] read(uint byteCount, ReadMode readMode)
655     {
656         ubyte[] data = new ubyte[byteCount];
657 
658         size_t byteRemains = byteCount;
659 
660         enum timeOutTickMax = 10; // msecs
661         auto timeOutTick = cast(int)((readTimeOut >= timeOutTickMax)?timeOutTickMax:readTimeOut);
662 
663         /* start time in hnsecs */
664         import std.datetime;
665         auto startTime = Clock.currStdTime();
666         do
667         {
668             pollfd pfd = pollfd(handle, POLLIN, 0);
669 
670             int rc = poll(&pfd, 1, timeOutTick);
671             if ((rc > 0) && (pfd.revents & POLLIN))
672             {
673                 static import core.sys.posix.unistd;
674                 ssize_t chanck = core.sys.posix.unistd.read(handle, cast(void*)(data.ptr + byteCount - byteRemains), byteRemains);
675                 if (chanck == -1)
676                 {
677                     throw new SerialPortIOException(name, "Error reading from serial port");
678                 }
679                 byteRemains -= chanck;
680             }
681             if (byteRemains > 0)
682             {
683                 Thread.sleep( dur!("msecs")(timeOutTick));
684             }
685             else
686             {
687                 break;
688             }
689         }
690         while(	readMode == ReadMode.waitForTimeout && (readTimeOut > (Clock.currStdTime() - startTime)/(1000*10)) ||
691                 readMode == ReadMode.waitForAllData ||
692                 readMode == ReadMode.waitForData && byteRemains == byteCount);
693 
694         if (byteRemains == byteCount)
695             throw new SerialPortTimeOutException(name, "Port data read timeout. ");
696         data = data[0..(byteCount-byteRemains)];
697         return data;
698     }
699 
700 
701 
702     bool checkParity(string parity) nothrow pure
703     {
704         return (getParityByName(parity) != Parity.error)?true:false;
705     }
706 
707 
708 
709     Parity getParityByName(string strParity) nothrow pure
710     {
711         switch(strParity)
712         {
713             case Parity.none:
714                 return Parity.none;
715             case Parity.odd:
716                 return Parity.odd;
717             case Parity.even:
718                 return Parity.even;
719             //case Parity.mark:
720             //	return Parity.mark;
721             //case Parity.space:
722             //	return Parity.space;
723             default:
724                 return Parity.error;
725         }
726     }
727 
728 
729 
730     bool checkSpeed(uint speed) nothrow pure
731     {
732         return (getBaudRateByNum(speed) != -1)?true:false;
733     }
734 
735 
736 
737     static const (char[]) genGetSpeedByNumBody()
738     {
739         const (char)[] res = "switch(speedNum) {";
740         foreach (speed; speedsRange)
741         {
742             res = res ~ "case " ~ speed ~ ": return Speed.S" ~ speed ~ "; \n";
743         }
744         res ~= "default: return Speed.S0;";
745         res ~= '}';
746         return res;
747     }
748 
749 
750     /**
751      * Convert speed number to Speed
752      */
753     Speed getSpeedByNum(uint speedNum) nothrow pure
754     {
755         mixin (genGetSpeedByNumBody);
756     }
757 
758 
759     static const (char[]) genGetBaudRateByNumBody()
760     {
761         const (char)[] res = "switch(speedNum) {";
762         foreach (speed; speedsRange)
763         {
764             res = res ~ "case " ~ speed ~ ": return B" ~ speed ~ "; \n";
765         }
766         res ~= "default: return -1;";
767         res ~= '}';
768         return res;
769     }
770 
771 
772     speed_t getBaudRateByNum(uint speedNum) nothrow pure
773     {
774         mixin (genGetBaudRateByNumBody);
775     }
776 
777 
778     version(linux)
779     {
780         enum B57600  	= 0x1001;
781         enum B115200 	= 0x1002;
782         enum B230400 	= 0x1003;
783         enum B460800 	= 0x1004;
784         enum B500000 	= 0x1005;
785         enum B576000 	= 0x1006;
786         enum B921600 	= 0x1007;
787         enum B1000000	= 0x1008;
788         enum B1152000 	= 0x1009;
789         enum B1500000 	= 0x100A;
790         enum B2000000 	= 0x100B;
791         enum B2500000 	= 0x100C;
792         enum B3000000 	= 0x100D;
793         enum B3500000 	= 0x100E;
794         enum B4000000 	= 0x100F;
795 
796         enum CRTSCTS 	= 0x80000000;
797 
798         enum ECHOCTL 	= 0x200;
799         enum ECHOPRT 	= 0x400;
800         enum ECHOKE 	= 0x800;
801     }
802     else version (OSX)
803     {
804         enum B7200  	= 7200;
805         enum B14400  	= 14400;
806         enum B28800  	= 28800;
807         enum B57600  	= 57600;
808         enum B76800  	= 76800;
809         enum B115200 	= 115200;
810         enum B230400 	= 230400;
811 
812         enum CCTS_OFLOW	= 0x00010000; /* CTS flow control of output */
813         enum CRTS_IFLOW	= 0x00020000; /* RTS flow control of input */
814         enum CRTSCTS	= (CCTS_OFLOW | CRTS_IFLOW);
815 
816         enum ECHOKE 	= 0x00000001; /* visual erase for line kill */
817         enum ECHOPRT	= 0x00000020; /* visual erase mode for hardcopy */
818         enum ECHOCTL 	= 0x00000040; /* echo control chars as ^(Char) */
819     }
820     else version (FreeBSD)
821     {
822         enum B7200  	= 7200;
823         enum B14400  	= 14400;
824         enum B28800  	= 28800;
825         enum B57600  	= 57600;
826         enum B76800  	= 76800;
827         enum B115200 	= 115200;
828         enum B230400 	= 230400;
829         enum B460800 	= 460800;
830         enum B921600 	= 921600;
831 
832 
833         enum CCTS_OFLOW	= 0x00010000; /* CTS flow control of output */
834         enum CRTS_IFLOW	= 0x00020000; /* RTS flow control of input */
835         enum CRTSCTS	= (CCTS_OFLOW | CRTS_IFLOW);
836 
837         enum ECHOKE 	= 0x00000001; /* visual erase for line kill */
838         enum ECHOPRT	= 0x00000020; /* visual erase mode for hardcopy */
839         enum ECHOCTL 	= 0x00000040; /* echo control chars as ^(Char) */
840     }
841     else version (Solaris)
842     {
843         //enum CRTSCTS 	= 0x10000000;
844 
845         //enum ECHOCTL 	= 0x200;
846         //enum ECHOPRT 	= 0x400;
847         //enum ECHOKE 	= 0x800;
848     }
849 }
850 
851 
852 
853 
854 version (vOnyxSerialTest)
855 {
856     import std.stdio;
857     import std.conv;
858 
859     unittest
860     {
861         {
862             string[] s1 =
863                 ["[port]",
864                  "name = /dev/ttyS0",
865                  "speed = 57600",
866                  "data_bits = 8",
867                  "stop_bits = 1",
868                  "parity = none",
869                  "set_RTS = no",
870                  "set_DTR = no",
871                  "time_out = 1500"];
872 
873             string[] s2 =
874                 ["[port]",
875                  "name = /dev/ttyr07",
876                  "speed = 57600",
877                  "data_bits = 8",
878                  "stop_bits = 1",
879                  "parity = none",
880                  "set_RTS = no",
881                  "set_DTR = no",
882                  "time_out = 1500"];
883 
884 
885             auto port1 = new OxSerialPort(new immutable Bundle(s1));
886             //auto port2 = new OxSerialPort(new immutable Bundle(s2));
887 
888             port1.open();
889             //port2.open();
890 
891             ubyte[] data = [0x22, 0x33, 0xCC];
892 
893             port1.write(data);
894 
895             //ubyte[] buf = port2.read(3);
896 
897             //assert (buf == data);
898 
899             port1.close();
900             //port2.close();
901         }
902 
903         {
904 
905             //import std.stdio;
906             //writeln(sp);
907 
908             auto port1 = new OxSerialPort("/dev/ttyS0", Speed.S9600, Parity.none, 1000);
909 
910             port1.open;
911             ubyte[] data = [0x22, 0x33, 0xCC];
912             port1.write(data);
913             port1.close;
914         }
915     }
916 }