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 }