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 }