1 /++
2     Crossplatform work with serialport.
3 
4     See also `example/monitor`
5  +/
6 module serialport;
7 
8 version (Posix) {} else version (Windows) {}
9 else static assert(0, "unsupported platform");
10 
11 public
12 {
13     import serialport.base;
14     import serialport.config;
15     import serialport.block;
16     import serialport.fiberready;
17     import serialport.nonblock;
18     import serialport.exception;
19     import serialport.types;
20 }
21 
22 version (unittest): private:
23 
24 import std.range;
25 import std.concurrency;
26 import std.exception;
27 import std.datetime;
28 import std.conv;
29 import std.string;
30 import std.stdio;
31 import std.random;
32 import std.process;
33 import core.thread;
34 
35 enum BUFFER_SIZE = 1024;
36 
37 interface ComPipe
38 {
39     void open();
40     void close();
41     string command() const @property;
42     string[2] ports() const @property;
43 }
44 
45 class SocatPipe : ComPipe
46 {
47     int bufferSize;
48     ProcessPipes pipe;
49     string[2] _ports;
50     string _command;
51 
52     this(int bs)
53     {
54         bufferSize = bs;
55         _command = ("socat -d -d -b%d pty,raw,"~
56                     "echo=0 pty,raw,echo=0").format(bufferSize);
57     }
58 
59     static string parsePort(string ln)
60     {
61         auto ret = ln.split[$-1];
62         enforce(ret.startsWith("/dev/"),
63         "unexpected last word in output line '%s'".format(ln));
64         return ret;
65     }
66 
67     override void close()
68     {
69         if (pipe.pid is null) return;
70         kill(pipe.pid);
71     }
72 
73     override void open()
74     {
75         pipe = pipeShell(_command);
76         _ports[0] = parsePort(pipe.stderr.readln.strip);
77         _ports[1] = parsePort(pipe.stderr.readln.strip);
78     }
79     
80     override const @property
81     {
82         string command() { return _command; }
83         string[2] ports() { return _ports; }
84     }
85 }
86 
87 unittest
88 {
89     enum socat_out_ln = "2018/03/08 02:56:58 socat[30331] N PTY is /dev/pts/1";
90     assert(SocatPipe.parsePort(socat_out_ln) == "/dev/pts/1");
91     assertThrown(SocatPipe.parsePort("some string"));
92 }
93 
94 class DefinedPorts : ComPipe
95 {
96     string[2] env;
97     string[2] _ports;
98 
99     this(string[2] envNames = ["SERIALPORT_TEST_PORT1", "SERIALPORT_TEST_PORT2"])
100     { env = envNames; }
101 
102 override:
103 
104     void open()
105     {
106         import std.process : environment;
107         import std.range : lockstep;
108         import std.algorithm : canFind;
109 
110         auto lst = SerialPort.listAvailable;
111 
112         foreach (ref e, ref p; lockstep(env[], _ports[]))
113         {
114             p = environment[e];
115             enforce(lst.canFind(p), new Exception("unknown port '%s' in env var '%s'".format(p, e)));
116         }
117     }
118 
119     void close() { }
120 
121     string command() const @property
122     {
123         return "env: %s=%s, %s=%s".format(
124             env[0], _ports[0],
125             env[1], _ports[1]
126         );
127     }
128 
129     string[2] ports() const @property { return _ports; }
130 }
131 
132 ComPipe getPlatformComPipe(int bufsz)
133 {
134     stderr.writeln("available ports count: ", SerialPort.listAvailable.length);
135 
136     try
137     {
138         auto ret = new DefinedPorts;
139         ret.open();
140         return ret;
141     }
142     catch (Exception e)
143     {
144         stderr.writeln();
145         stderr.writeln("error while open predefined ports: ", e.msg);
146 
147         version (Posix) return new SocatPipe(bufsz);
148         else return null;
149     }
150 }
151 
152 // real test main
153 //version (realtest)
154 unittest
155 {
156     stderr.writeln("=== start real test ===\n");
157     scope (success) stderr.writeln("=== finish real test ===");
158     scope (failure) stderr.writeln("!!!  fail real test  !!!");
159     auto cp = getPlatformComPipe(BUFFER_SIZE);
160     if (cp is null)
161     {
162         stderr.writeln("platform doesn't support real test");
163         return;
164     }
165 
166     stderr.writefln("port source `%s`\n", cp.command);
167 
168     void reopen()
169     {
170         cp.close();
171         Thread.sleep(30.msecs);
172         cp.open();
173         stderr.writefln("pipe ports: %s <=> %s", cp.ports[0], cp.ports[1]);
174     }
175 
176     reopen();
177 
178     utCall!(threadTest!SerialPortFR)("thread test for fiber ready", cp.ports);
179     utCall!(threadTest!SerialPortBlk)("thread test for block", cp.ports);
180     utCall!testNonBlock("test non block", cp.ports);
181     utCall!fiberTest("fiber test", cp.ports);
182     utCall!fiberTest2("fiber test 2", cp.ports);
183     utCall!readTimeoutTest("read timeout test", cp.ports);
184     alias rttc = readTimeoutTestConfig;
185     alias rttc2 = readTimeoutTestConfig2;
186     utCall!(rttc!SerialPortFR)( "read timeout test for FR  cr=zero", cp.ports, SerialPort.CanRead.zero);
187     utCall!(rttc!SerialPortBlk)("read timeout test for Blk cr=zero", cp.ports, SerialPort.CanRead.zero);
188     utCall!(rttc!SerialPortFR)( "read timeout test for FR  cr=anyNonZero", cp.ports, SerialPort.CanRead.anyNonZero);
189     utCall!(rttc!SerialPortBlk)("read timeout test for Blk cr=anyNonZero", cp.ports, SerialPort.CanRead.anyNonZero);
190     utCall!(rttc!SerialPortFR)( "read timeout test for FR  cr=allOrNothing", cp.ports, SerialPort.CanRead.allOrNothing);
191     utCall!(rttc!SerialPortBlk)("read timeout test for Blk cr=allOrNothing", cp.ports, SerialPort.CanRead.allOrNothing);
192     utCall!(rttc2!SerialPortFR)( "read timeout test 2 for FR  cr=zero", cp.ports, SerialPort.CanRead.zero);
193     utCall!(rttc2!SerialPortBlk)("read timeout test 2 for Blk cr=zero", cp.ports, SerialPort.CanRead.zero);
194     utCall!(rttc2!SerialPortFR)( "read timeout test 2 for FR  cr=anyNonZero", cp.ports, SerialPort.CanRead.anyNonZero);
195     utCall!(rttc2!SerialPortBlk)("read timeout test 2 for Blk cr=anyNonZero", cp.ports, SerialPort.CanRead.anyNonZero);
196     utCall!(rttc2!SerialPortFR)( "read timeout test 2 for FR  cr=allOrNothing", cp.ports, SerialPort.CanRead.allOrNothing);
197     utCall!(rttc2!SerialPortBlk)("read timeout test 2 for Blk cr=allOrNothing", cp.ports, SerialPort.CanRead.allOrNothing);
198     utCall!(fiberSleepFuncTest)("fiber sleep func test", cp.ports);
199 }
200 
201 unittest
202 {
203     enum name = "/some/path/to/notexisting/device";
204     auto e = enforce(collectException(new SerialPortBlk(name, 19200)), "exception not thrown");
205     auto sce = cast(SysCallException)e;
206     assert (sce !is null);
207     assert (sce.port == name, "wrong name");
208     version (Posix)
209     {
210         assert(sce.fnc == "open", "'" ~ sce.fnc ~ "' is not 'open'");
211         assert(sce.err == 2, "unexpectable errno %d".format(sce.err));
212     }
213     auto exp = format!"call '%s' (%s) failed: error %d"(sce.fnc, name, sce.err);
214     if (!e.msg.startsWith(exp))
215     {
216         import std.stdio;
217         stderr.writeln("exp: ", exp);
218         stderr.writeln("msg: ", e.msg);
219         assert(0, "wrong msg");
220     }
221 }
222 
223 void testPrint(Args...)(Args args) { stderr.write("    "); stderr.writeln(args); }
224 void testPrintf(Args...)(Args args) { stderr.write("    "); stderr.writefln(args); }
225 
226 auto utCall(alias fnc, Args...)(string name, Args args)
227 {
228     stderr.writefln(">>> run %s", name);
229     scope (success) stderr.writefln("<<< success %s\n", name);
230     scope (failure) stderr.writefln("!!! failure %s\n", name);
231     return fnc(args);
232 }
233 
234 void threadTest(SPT)(string[2] ports)
235 {
236     assert(SerialPort.listAvailable.length != 0);
237 
238     static struct ExcStruct { string msg, type; }
239 
240     static void echoThread(string port)
241     {
242         void[BUFFER_SIZE] buffer = void;
243         auto com = new SPT(port, "2400:8N1");
244         scope (exit) com.close();
245         com.flush();
246 
247         com.set(1200);
248         assert(com.config.baudRate == 1200);
249 
250         com.baudRate = 38_400;
251         assert(com.config.baudRate == 38_400);
252 
253         bool work = true;
254         com.readTimeout = 1000.msecs;
255 
256         bool needRead;
257 
258         while (work)
259         {
260             try
261             {
262                 if (needRead)
263                 {
264                     Thread.sleep(500.msecs);
265                     auto data = com.read(buffer, com.CanRead.zero);
266 
267                     if (data.length)
268                     {
269                         testPrint("child readed: ", cast(string)(data.idup));
270                         send(ownerTid, cast(string)(data.idup));
271                     }
272                 }
273 
274                 receiveTimeout(500.msecs,
275                     (SPConfig cfg)
276                     {
277                         com.config = cfg;
278                         testPrint("child get cfg: ", cfg.mode);
279                     },
280                     (bool nr)
281                     {
282                         if (nr) needRead = true;
283                         else
284                         {
285                             work = false;
286                             needRead = false;
287                         }
288                         testPrint("get needRead ", nr);
289                     },
290                     (OwnerTerminated e) { work = false; }
291                 );
292             }
293             catch (Throwable e)
294             {
295                 work = false;
296                 testPrint("exception in child: ", e);
297                 send(ownerTid, ExcStruct(e.msg, e.classinfo.stringof));
298             }
299         }
300     }
301 
302     auto t = spawnLinked(&echoThread, ports[1]);
303 
304     auto com = new SPT(ports[0], 19_200);
305     com.flush();
306 
307     assert(com.baudRate == 19_200);
308     assert(com.dataBits == DataBits.data8);
309     assert(com.parity == Parity.none);
310     assert(com.stopBits == StopBits.one);
311 
312     assert(com.config.baudRate == 19_200);
313     assert(com.config.dataBits == DataBits.data8);
314     assert(com.config.parity == Parity.none);
315     assert(com.config.stopBits == StopBits.one);
316 
317     scope (exit) com.close();
318 
319     string[] list;
320 
321     const sets = [
322         SPConfig(38_400),
323         SPConfig(2400),
324         SPConfig.parse("19200:8N2"),
325     ];
326 
327     auto cfg = SPConfig(38_400);
328     com.config = cfg;
329     send(t, cfg);
330 
331     Thread.sleep(1000.msecs);
332 
333     string msg = sets.front.mode;
334     com.write(msg);
335 
336     bool work = true;
337     send(t, true);
338     while (work)
339     {
340         receive(
341             (string rec)
342             {
343                 enforce(rec == msg, "break message: '%s' != '%s'".format(msg, rec));
344 
345                 if (list.empty)
346                 {
347                     testPrint("owner send data finish");
348                     send(t, false);
349                 }
350                 else
351                 {
352                     msg = list.front;
353                     list.popFront();
354                 }
355 
356                 com.write(msg);
357                 testPrint("owner write msg to com: ", msg);
358             },
359             (ExcStruct e) { throw new Exception("%s:%s".format(e.type, e.msg)); },
360             (LinkTerminated e)
361             {
362                 work = false;
363                 testPrintf("link terminated for %s, child tid %s", e.tid, t);
364                 //assert(e.tid == t);
365             }
366         );
367     }
368 }
369 
370 void testNonBlock(string[2] ports)
371 {
372     import std.datetime.stopwatch;
373     enum mode = "38400:8N1";
374 
375     const data = "1234567890987654321qazxswedcvfrtgbnhyujm,ki";
376 
377     static void thfunc(string port)
378     {
379         auto com = new SerialPortNonBlk(port, mode);
380         scope (exit) com.close();
381 
382         void[1024] buffer = void;
383         size_t readed;
384 
385         const sw = StopWatch(AutoStart.yes);
386 
387         // flush
388         while (sw.peek < 10.msecs)
389         {
390             com.read(buffer);
391             Thread.sleep(1.msecs);
392         }
393 
394         while (sw.peek < 1.seconds)
395             readed += com.read(buffer[readed..$]).length;
396 
397         send(ownerTid, buffer[0..readed].idup);
398 
399         Thread.sleep(200.msecs);
400     }
401 
402     auto com = new SerialPortNonBlk(ports[0], 38_400, "8N1");
403     scope (exit) com.close();
404 
405     spawnLinked(&thfunc, ports[1]);
406 
407     Thread.sleep(100.msecs);
408 
409     size_t written;
410     while (written < data.length)
411         written += com.write(data[written..$]);
412 
413     receive((immutable(void)[] readed)
414     {
415         testPrint("readed: ", cast(string)readed);
416         testPrint("  data: ", data);
417         assert(cast(string)readed == data);
418     });
419 
420     receive((LinkTerminated e) { });
421 }
422 
423 class CF : Fiber
424 {
425     void[] data;
426 
427     SerialPortFR com;
428 
429     this(SerialPortFR com, size_t bufsize)
430     {
431         this.com = com;
432         this.com.flush();
433         this.data = new void[bufsize];
434         foreach (ref v; cast(ubyte[])data)
435             v = cast(ubyte)uniform(0, 128);
436         super(&run);
437     }
438 
439     abstract void run();
440 }
441 
442 class CFSlave : CF
443 {
444     void[] result;
445 
446     Duration readTimeout = 40.msecs;
447     Duration readGapTimeout = 100.msecs;
448 
449     this(SerialPortFR com, size_t bufsize)
450     { super(com, bufsize); }
451 
452     override void run()
453     {
454         testPrint("start read loop");
455         result = com.readContinues(data, readTimeout, readGapTimeout);
456         testPrint("finish read loop ("~result.length.to!string~")");
457     }
458 }
459 
460 class CFMaster : CF
461 {
462     CFSlave slave;
463 
464     Duration writeTimeout = 20.msecs;
465 
466     this(SerialPortFR com, size_t bufsize)
467     { super(com, bufsize); }
468 
469     override void run()
470     {
471         testPrint("start write loop ("~data.length.to!string~")");
472         com.writeTimeout = writeTimeout;
473         com.write(data);
474         testPrint("finish write loop");
475     }
476 }
477 
478 void fiberTest(string[2] ports)
479 {
480     auto slave = new CFSlave(new SerialPortFR(ports[0]), BUFFER_SIZE);
481     scope (exit) slave.com.close();
482     auto master = new CFMaster(new SerialPortFR(ports[1]), BUFFER_SIZE);
483     scope (exit) master.com.close();
484 
485     bool work = true;
486     int step;
487     while (work)
488     {
489         alias TERM = Fiber.State.TERM;
490         if (master.state != TERM) master.call;
491         if (slave.state != TERM) slave.call;
492 
493         step++;
494         Thread.sleep(30.msecs);
495         if (master.state == TERM && slave.state == TERM)
496         {
497             if (slave.result.length == master.data.length)
498             {
499                 import std.algorithm : equal;
500                 enforce(equal(cast(ubyte[])slave.result, cast(ubyte[])master.data));
501                 work = false;
502                 testPrint("basic loop steps: ", step);
503             }
504             else throw new Exception(text(slave.result, " != ", master.data));
505         }
506     }
507 }
508 
509 void fiberTest2(string[2] ports)
510 {
511     string mode = "9600:8N1";
512 
513     auto scom = new SerialPortFR(ports[0], 9600, "8N1");
514     auto mcom = new SerialPortFR(ports[1], "19200:8N1");
515     scope (exit) scom.close();
516     scope (exit) mcom.close();
517 
518     version (Posix)
519         assertThrown!UnsupportedException(scom.baudRate = 9200);
520 
521     scom.reopen(ports[0], SPConfig.parse(mode));
522     mcom.reopen(ports[1], SPConfig.parse(mode));
523     scom.flush();
524     mcom.flush();
525 
526     scom.config = mcom.config;
527 
528     scom.readTimeout = 1000.msecs;
529     mcom.writeTimeout = 100.msecs;
530 
531     version (OSX) enum BS = BUFFER_SIZE / 2;
532     else          enum BS = BUFFER_SIZE * 4;
533 
534     auto slave  = new CFSlave(scom,  BS);
535     auto master = new CFMaster(mcom, BS);
536 
537     void run()
538     {
539         bool work = true;
540         int step;
541         alias TERM = Fiber.State.TERM;
542         while (work)
543         {
544             if (master.state != TERM) master.call;
545             Thread.sleep(5.msecs);
546             if (slave.state != TERM) slave.call;
547 
548             step++;
549             if (master.state == TERM && slave.state == TERM)
550             {
551                 assert(slave.result.length == master.data.length);
552                 import std.algorithm : equal;
553                 enforce(equal(cast(ubyte[])slave.result, cast(ubyte[])master.data));
554                 work = false;
555                 testPrint("basic loop steps: ", step);
556             }
557         }
558     }
559 
560     run();
561 }
562 
563 void readTimeoutTest(string[2] ports)
564 {
565     void[1024] buffer = void;
566 
567     auto comA = new SerialPortFR(ports[0], 19_200);
568     scope (exit) comA.close();
569     comA.flush();
570     assertThrown!TimeoutException(comA.readContinues(buffer[], 1.msecs, 1.msecs));
571     assertThrown!TimeoutException(comA.read(buffer[]));
572     assertThrown!TimeoutException(comA.read(buffer[], comA.CanRead.anyNonZero));
573 
574     auto comB = new SerialPortBlk(ports[1], 19_200, "8N1");
575     scope (exit) comB.close();
576     comB.flush();
577     comB.readTimeout = 1.msecs;
578     assertThrown!TimeoutException(comB.read(buffer[]));
579     assertThrown!TimeoutException(comB.read(buffer[], comB.CanRead.anyNonZero));
580 }
581 
582 void readTimeoutTestConfig(SP : SerialPort)(string[2] ports, SerialPort.CanRead cr)
583 {
584     enum mode = "38400:8N1";
585 
586     enum FULL = 100;
587     enum SEND = "helloworld";
588 
589     static void thfunc(string port)
590     {
591         auto com = new SP(port, mode);
592         com.flush();
593         scope (exit) com.close();
594         com.write(SEND);
595     }
596 
597     auto com = new SP(ports[0], mode);
598     scope (exit) com.close();
599     auto rt = 300.msecs;
600     com.readTimeout = rt;
601     com.flush();
602     assert(com.readTimeout == rt);
603 
604     void[FULL] buffer = void;
605     void[] data;
606 
607     spawnLinked(&thfunc, ports[1]);
608 
609     Thread.sleep(rt);
610 
611     if (cr == SerialPort.CanRead.anyNonZero)
612     {
613         assertNotThrown(data = com.read(buffer, cr));
614         assert(cast(string)data == SEND);
615         assertThrown!TimeoutException(data = com.read(buffer, cr));
616     }
617     else if (cr == SerialPort.CanRead.allOrNothing)
618         assertThrown!TimeoutException(data = com.read(buffer));
619     else if (cr == SerialPort.CanRead.zero)
620     {
621         assertNotThrown(data = com.read(buffer, cr));
622         assertNotThrown(data = com.read(buffer, cr));
623         assertNotThrown(data = com.read(buffer, cr));
624     }
625     else assert(0, "not tested variant of CanRead");
626 
627     receive((LinkTerminated e) { });
628 }
629 
630 void readTimeoutTestConfig2(SP : SerialPort)(string[2] ports, SerialPort.CanRead cr)
631 {
632     enum mode = "38400:8N1";
633 
634     static void thfunc(string port)
635     {
636         auto com = new SP(port, mode);
637         scope (exit) com.close();
638         com.flush();
639         Thread.sleep(200.msecs);
640         com.write("one");
641         Thread.sleep(200.msecs);
642         com.write("two");
643     }
644 
645     auto com = new SP(ports[0], mode);
646     scope (exit) com.close();
647     com.readTimeout = cr == SerialPort.CanRead.zero ? 10.msecs : 300.msecs;
648     com.flush();
649 
650     void[6] buffer = void;
651     void[] data;
652 
653     spawnLinked(&thfunc, ports[1]);
654 
655     if (cr == SerialPort.CanRead.anyNonZero)
656     {
657         assertNotThrown(data = com.read(buffer, cr));
658         assert(cast(string)data == "one");
659         assertNotThrown(data = com.read(buffer, cr));
660         assert(cast(string)data == "two");
661     }
662     else if (cr == SerialPort.CanRead.allOrNothing)
663         assertThrown!TimeoutException(data = com.read(buffer));
664     else if (cr == SerialPort.CanRead.zero)
665     {
666         assertNotThrown(data = com.read(buffer, cr));
667         assert(cast(string)data == "");
668         Thread.sleep(300.msecs);
669         assertNotThrown(data = com.read(buffer, cr));
670         assert(cast(string)data == "one");
671         assertNotThrown(data = com.read(buffer, cr));
672         assert(cast(string)data == "");
673         Thread.sleep(200.msecs);
674         assertNotThrown(data = com.read(buffer, cr));
675         assert(cast(string)data == "two");
676         assertNotThrown(data = com.read(buffer, cr));
677         assert(cast(string)data == "");
678     }
679     else assert(0, "not tested variant of CanRead");
680 
681     receive((LinkTerminated e) { });
682 }
683 
684 void fiberSleepFuncTest(string[2] ports)
685 {
686     import std.datetime.stopwatch;
687 
688     static void sf(Duration d) @nogc
689     {
690         const sw = StopWatch(AutoStart.yes);
691         if (auto f = Fiber.getThis)
692             while (sw.peek < d) f.yield();
693         else Thread.sleep(d);
694     }
695 
696     CFMaster master;
697 
698     size_t sf2_cnt;
699     void sf2(Duration d) @nogc
700     {
701         const sw = StopWatch(AutoStart.yes);
702         if (auto f = Fiber.getThis)
703             while (sw.peek < d)
704             {
705                 master.yield();
706                 sf2_cnt++;
707             }
708         else Thread.sleep(d);
709     }
710 
711     auto slave = new CFSlave(new SerialPortFR(ports[0], &sf), BUFFER_SIZE);
712     scope (exit) slave.com.close();
713     master = new CFMaster(new SerialPortFR(ports[1], &sf2), BUFFER_SIZE);
714     scope (exit) master.com.close();
715 
716     bool work = true;
717     int step;
718     while (work)
719     {
720         alias TERM = Fiber.State.TERM;
721         if (master.state != TERM) master.call;
722         if (slave.state != TERM) slave.call;
723 
724         step++;
725         Thread.sleep(30.msecs);
726         if (master.state == TERM && slave.state == TERM)
727         {
728             if (slave.result.length == master.data.length)
729             {
730                 import std.algorithm : equal;
731                 enforce(equal(cast(ubyte[])slave.result, cast(ubyte[])master.data));
732                 work = false;
733                 testPrint("basic loop steps: ", step);
734             }
735             else throw new Exception(text(slave.result, " != ", master.data));
736         }
737     }
738 }