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 }