1 /// 2 module serialport.port; 3 4 import std.algorithm; 5 import std.array; 6 import std.conv : to, text, octal; 7 import std.exception; 8 import std.experimental.logger; 9 import std.path; 10 import std.string; 11 import core.time; 12 import std.datetime : StopWatch; 13 14 import serialport.types; 15 import serialport.exception; 16 17 version (Posix) {} else version (Windows) {} 18 else static assert(0, "unsupported platform"); 19 20 /// 21 class SerialPort 22 { 23 protected: 24 /// 25 string port; 26 27 /// 28 version (Posix) int handle = -1; 29 /// 30 version (Windows) HANDLE handle = null; 31 32 /// preform pause 33 void yield() 34 { 35 if (yieldFunc !is null) yieldFunc(); 36 else 37 { 38 import core.thread; 39 if (Fiber.getThis is null) Thread.yield(); 40 else Fiber.yield(); 41 } 42 } 43 44 public: 45 46 /// 47 static struct Config 48 { 49 /// 50 uint baudRate=9600; 51 /// 52 Parity parity=Parity.none; 53 /// 54 DataBits dataBits=DataBits.data8; 55 /// 56 StopBits stopBits=StopBits.one; 57 58 auto set(Parity v) { parity = v; return this; } 59 auto set(uint v) { baudRate = v; return this; } 60 auto set(DataBits v) { dataBits = v; return this; } 61 auto set(StopBits v) { stopBits = v; return this; } 62 } 63 64 /// extended delegate for yielding 65 void delegate() yieldFunc; 66 67 /// 68 this(string port, Config conf=Config.init, void delegate() yh=null) 69 { 70 this.port = port; 71 this.yieldFunc = yh; 72 setup(conf); 73 } 74 75 /// 76 this(string port, uint baudRate, void delegate() yh=null) 77 { this(port, Config(baudRate), yh); } 78 79 /// 80 this(string port, uint baudRate, Parity parity, void delegate() yh=null) 81 { this(port, Config(baudRate, parity), yh); } 82 83 /// 84 this(string port, uint baudRate, Parity parity, 85 DataBits dataBits, 86 StopBits stopBits, void delegate() yh=null) 87 { this(port, Config(baudRate, parity, dataBits, stopBits), yh); } 88 89 ~this() { close(); } 90 91 /// close handle 92 void close() 93 { 94 if (closed) return; 95 version(Windows) 96 { 97 CloseHandle(handle); 98 handle = null; 99 } 100 version(Posix) 101 { 102 posixClose(handle); 103 handle = -1; 104 } 105 } 106 107 /// 108 override string toString() { return port; } 109 110 /// 111 SerialPort set(Parity p) { config = config.set(p); return this; } 112 /// 113 SerialPort set(uint br) { config = config.set(br); return this; } 114 /// 115 SerialPort set(DataBits db) { config = config.set(db); return this; } 116 /// 117 SerialPort set(StopBits sb) { config = config.set(sb); return this; } 118 119 @property 120 { 121 /// 122 bool closed() const 123 { 124 version(Posix) return handle == -1; 125 version(Windows) return handle is null; 126 } 127 128 /// 129 Config config() 130 { 131 enforce(!closed, new PortClosedException(port)); 132 133 Config ret; 134 135 version (Posix) 136 { 137 termios opt; 138 enforce(tcgetattr(handle, &opt) != -1, 139 new SerialPortException(format("Failed while call tcgetattr: %d", errno))); 140 141 ret.baudRate = getUintBaudRate(); 142 143 if (opt.c_cflag.hasFlag(PARODD)) ret.parity = Parity.odd; 144 else if (!(opt.c_cflag & PARENB)) ret.parity = Parity.none; 145 else ret.parity = Parity.even; 146 147 if (opt.c_cflag.hasFlag(CS8)) ret.dataBits = DataBits.data8; 148 else if (opt.c_cflag.hasFlag(CS7)) ret.dataBits = DataBits.data7; 149 else if (opt.c_cflag.hasFlag(CS6)) ret.dataBits = DataBits.data6; 150 else ret.dataBits = DataBits.data5; 151 152 ret.stopBits = opt.c_cflag.hasFlag(CSTOPB) ? StopBits.two : StopBits.one; 153 } 154 version (Windows) 155 { 156 DCB cfg; 157 GetCommState(handle, &cfg); 158 159 ret.baudRate = cast(uint)cfg.BaudRate; 160 161 ret.parity = [NOPARITY: Parity.none, ODDPARITY: Parity.odd, 162 EVENPARITY: Parity.even][cfg.Parity]; 163 164 ret.dataBits = [5: DataBits.data5, 6: DataBits.data6, 165 7: DataBits.data7, 8: DataBits.data8] 166 .get(cfg.ByteSize, DataBits.data8); 167 168 ret.stopBits = cfg.StopBits == ONESTOPBIT ? StopBits.one : StopBits.two; 169 } 170 171 return ret; 172 } 173 174 /// 175 void config(Config c) 176 { 177 if (closed) throw new PortClosedException(port); 178 179 version (Posix) 180 { 181 setUintBaudRate(c.baudRate); 182 183 termios opt; 184 enforce(tcgetattr(handle, &opt) != -1, 185 new SerialPortException(format("Failed while call tcgetattr: %d", errno))); 186 187 final switch (c.parity) 188 { 189 case Parity.none: 190 opt.c_cflag &= ~PARENB; 191 break; 192 case Parity.odd: 193 opt.c_cflag |= (PARENB | PARODD); 194 break; 195 case Parity.even: 196 opt.c_cflag &= ~PARODD; 197 opt.c_cflag |= PARENB; 198 break; 199 } 200 201 final switch (c.stopBits) 202 { 203 case StopBits.one: 204 opt.c_cflag &= ~CSTOPB; 205 break; 206 case StopBits.onePointFive: 207 case StopBits.two: 208 opt.c_cflag |= CSTOPB; 209 break; 210 } 211 212 opt.c_cflag &= ~CSIZE; 213 switch (c.dataBits) { 214 case DataBits.data5: opt.c_cflag |= CS5; break; 215 case DataBits.data6: opt.c_cflag |= CS6; break; 216 case DataBits.data7: opt.c_cflag |= CS7; break; 217 case DataBits.data8: opt.c_cflag |= CS8; break; 218 default: 219 errorf("config dataBits is setted as %d, set default CS8", 220 c.dataBits); 221 opt.c_cflag |= CS8; 222 break; 223 } 224 225 enforce(tcsetattr(handle, TCSANOW, &opt) != -1, 226 new SerialPortException(format("Failed while call tcsetattr: %d", errno))); 227 228 auto test = config; 229 230 enforce(test.baudRate == c.baudRate, 231 new BaudRateUnsupportedException(c.baudRate)); 232 enforce(test.parity == c.parity, 233 new ParityUnsupportedException(c.parity)); 234 enforce(test.stopBits == c.stopBits, 235 new StopBitsUnsupportedException(c.stopBits)); 236 enforce(test.dataBits == c.dataBits, 237 new DataBitsUnsupportedException(c.dataBits)); 238 } 239 version (Windows) 240 { 241 DCB cfg; 242 GetCommState(handle, &cfg); 243 244 if (cfg.BaudRate != cast(DWORD)c.baudRate) 245 { 246 cfg.BaudRate = cast(DWORD)c.baudRate; 247 enforce(SetCommState(handle, &cfg), 248 new BaudRateUnsupportedException(c.baudRate)); 249 } 250 251 auto tmpParity = [Parity.none: NOPARITY, Parity.odd: ODDPARITY, 252 Parity.even: EVENPARITY][c.parity]; 253 if (cfg.Parity != tmpParity) 254 { 255 cfg.Parity = cast(ubyte)tmpParity; 256 enforce(SetCommState(handle, &cfg), 257 new ParityUnsupportedException(c.parity)); 258 } 259 260 auto tmpStopBits = [StopBits.one: ONESTOPBIT, 261 StopBits.onePointFive: ONESTOPBIT, 262 StopBits.two: TWOSTOPBITS][c.stopBits]; 263 264 if (cfg.StopBits != tmpStopBits) 265 { 266 cfg.StopBits = cast(ubyte)tmpStopBits; 267 enforce(SetCommState(handle, &cfg), 268 new StopBitsUnsupportedException(c.stopBits)); 269 } 270 271 if (cfg.ByteSize != cast(typeof(cfg.ByteSize))c.dataBits) 272 { 273 cfg.ByteSize = cast(typeof(cfg.ByteSize))c.dataBits; 274 enforce(SetCommState(handle, &cfg), 275 new DataBitsUnsupportedException(c.dataBits)); 276 } 277 } 278 } 279 280 /// 281 Parity parity() { return config.parity; } 282 /// 283 uint baudRate() { return config.baudRate; } 284 /// 285 DataBits dataBits() { return config.dataBits; } 286 /// 287 StopBits stopBits() { return config.stopBits; } 288 289 /// 290 Parity parity(Parity v) { config = config.set(v); return v; } 291 /// 292 uint baudRate(uint v) { config = config.set(v); return v; } 293 /// 294 DataBits dataBits(DataBits v) { config = config.set(v); return v; } 295 /// 296 StopBits stopBits(StopBits v) { config = config.set(v); return v; } 297 298 /// 299 static string[] ports() 300 { 301 version (Posix) 302 { 303 bool onlyComPorts(string n) 304 { 305 static bool isInRange(T, U)(T v, U a, U b) 306 { return a <= v && v <= b; } 307 308 version(linux) return n.startsWith("ttyUSB") || 309 n.startsWith("ttyS"); 310 version(darwin) return n.startsWith("cu"); 311 version(FreeBSD) return n.startsWith("cuaa") || 312 n.startsWith("cuad"); 313 version(openbsd) return n.startsWith("tty"); 314 version(solaris) return n.startsWith("tty") && 315 isInRange(n[$-1],'a','z'); 316 } 317 318 return dirEntries("/dev/", SpanMode.shallow) 319 .map!(a=>a.name.baseName) 320 .filter!onlyComPorts 321 .map!(a=>"/dev/" ~ a) 322 .array; 323 } 324 version (Windows) 325 { 326 string[] ret; 327 enum pre = `\\.\COM`; 328 foreach (int n; 0 .. 255) 329 { 330 auto i = n+1; 331 HANDLE p = CreateFileA(text(pre, i).toStringz, 332 GENERIC_READ | GENERIC_WRITE, 0, null, 333 OPEN_EXISTING, 0, null); 334 if (p != INVALID_HANDLE_VALUE) 335 { 336 ret ~= text("COM", i); 337 CloseHandle(p); 338 } 339 } 340 return ret; 341 } 342 } 343 } 344 345 /// 346 void write(const(void[]) arr, Duration timeout=500.dur!"usecs") 347 { 348 if (closed) throw new PortClosedException(port); 349 350 size_t written = 0; 351 352 StopWatch full; 353 354 full.start(); 355 while (written < arr.length) 356 { 357 ptrdiff_t res; 358 auto ptr = arr.ptr + written; 359 auto len = arr.length - written; 360 361 version (Posix) 362 { 363 res = posixWrite(handle, ptr, len); 364 enforce(res >= 0, new WriteException(port, text("errno ", errno))); 365 } 366 version (Windows) 367 { 368 uint sres; 369 auto wfr = WriteFile(handle, ptr, cast(uint)len, &sres, null); 370 if (!wfr) 371 { 372 auto err = GetLastError(); 373 if (err == ERROR_IO_PENDING) { /+ asynchronously +/ } 374 else throw new WriteException(port, text("error ", err)); 375 } 376 res = sres; 377 } 378 379 written += res; 380 381 if (full.peek().to!Duration > timeout) 382 throw new TimeoutException(port); 383 384 yield(); 385 } 386 } 387 388 /// 389 void[] read(void[] arr, Duration timeout=1.dur!"seconds", 390 Duration frameGap=4.dur!"msecs") 391 { 392 if (closed) throw new PortClosedException(port); 393 394 size_t readed = 0; 395 396 StopWatch silence, full; 397 398 full.start(); 399 while (true) 400 { 401 enforce(readed < arr.length, 402 new SerialPortException("read more what can")); 403 size_t res; 404 405 auto ptr = arr.ptr + readed; 406 auto len = arr.length - readed; 407 408 version (Posix) 409 { 410 auto sres = posixRead(handle, ptr, len); 411 412 if (sres < 0 && errno == EAGAIN) // no bytes for read, it's ok 413 sres = 0; 414 else 415 { 416 enforce(sres >= 0, 417 new ReadException(port, text("errno ", errno))); 418 res = sres; 419 } 420 } 421 version (Windows) 422 { 423 uint sres; 424 auto rfr = ReadFile(handle, ptr, cast(uint)len, &sres, null); 425 if (!rfr) 426 { 427 auto err = GetLastError(); 428 if (err == ERROR_IO_PENDING) { /+ asynchronously +/ } 429 else throw new ReadException(port, text("error ", err)); 430 } 431 res = sres; 432 } 433 434 readed += res; 435 436 if (res == 0) 437 { 438 if (readed > 0 && silence.peek().to!Duration > frameGap) 439 return arr[0..readed]; 440 441 if (!silence.running) silence.start(); 442 } 443 else 444 { 445 silence.stop(); 446 silence.reset(); 447 } 448 449 if (readed == 0 && full.peek().to!Duration > timeout) 450 throw new TimeoutException(port); 451 452 yield(); 453 } 454 } 455 456 protected: 457 458 version (Posix) 459 { 460 void setUintBaudRate(uint br) 461 { 462 version(usetermios2) 463 { 464 enum CBAUD = octal!10017; 465 enum BOTHER = octal!10000; 466 467 termios2 opt2; 468 enforce(ioctl(handle, TCGETS2, &opt2) != -1, 469 new SetupFailException(port, "can't get termios2 options")); 470 opt2.c_cflag &= ~CBAUD; //Remove current BAUD rate 471 opt2.c_cflag |= BOTHER; //Allow custom BAUD rate using int input 472 opt2.c_ispeed = br; //Set the input BAUD rate 473 opt2.c_ospeed = br; //Set the output BAUD rate 474 ioctl(handle, TCSETS2, &opt2); 475 } 476 else 477 { 478 enforce(br in unixBaudList, 479 new BaudRateUnsupportedException(br)); 480 481 auto baud = unixBaudList[br]; 482 483 termios opt; 484 enforce(tcgetattr(handle, &opt) != -1, 485 new SerialPortException(port, "can't get termios options")); 486 487 //cfsetispeed(&opt, B0); 488 cfsetospeed(&opt, baud); 489 490 enforce(tcsetattr(handle, TCSANOW, &opt) != -1, 491 new SerialPortException("Failed while call tcsetattr")); 492 } 493 } 494 495 uint getUintBaudRate() 496 { 497 version (usetermios2) 498 { 499 termios2 opt2; 500 enforce(ioctl(handle, TCGETS2, &opt2) != -1, 501 new SetupFailException(port, "can't get termios2 options")); 502 return opt2.c_ospeed; 503 } 504 else 505 { 506 termios opt; 507 enforce(tcgetattr(handle, &opt) != -1, 508 new SerialPortException(port, "can't get termios options")); 509 auto b = cfgetospeed(&opt); 510 if (b !in unixUintBaudList) 511 { 512 warningf("unknown baud speed setted: %s", b); 513 return 0; 514 } 515 return unixUintBaudList[b]; 516 } 517 } 518 } 519 520 /// open handler, set new config 521 final void setup(Config conf) 522 { 523 enforce(port.length, new SetupFailException(port, "zero length name")); 524 525 version (Posix) 526 { 527 handle = open(port.toStringz(), O_RDWR | O_NOCTTY | O_NONBLOCK); 528 enforce(handle != -1, 529 new SetupFailException(port, 530 format("Can't open port (errno %d)", errno))); 531 532 termios opt; 533 enforce(tcgetattr(handle, &opt) != -1, 534 new SetupFailException(port, "can't get termios options")); 535 536 // make raw 537 opt.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | 538 INLCR | IGNCR | ICRNL | IXON); 539 opt.c_oflag &= ~OPOST; 540 opt.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); 541 opt.c_cflag &= ~(CSIZE | PARENB); 542 opt.c_cflag |= CS8; 543 544 enforce(tcsetattr(handle, TCSANOW, &opt) != -1, 545 new SetupFailException(format("Failed while" ~ 546 " call tcsetattr (errno %d)", errno))); 547 } 548 else version (Windows) 549 { 550 auto fname = `\\.\` ~ port; 551 handle = CreateFileA(fname.toStringz, 552 GENERIC_READ | GENERIC_WRITE, 0, null, 553 OPEN_EXISTING, 0, null); 554 555 if(handle is INVALID_HANDLE_VALUE) 556 { 557 auto err = GetLastError(); 558 throw new SetupFailException(port, 559 format("can't CreateFileA '%s' with error: %d", fname, err)); 560 } 561 562 SetupComm(handle, 4096, 4096); 563 PurgeComm(handle, PURGE_TXABORT | PURGE_TXCLEAR | 564 PURGE_RXABORT | PURGE_RXCLEAR); 565 566 COMMTIMEOUTS tm; 567 tm.ReadIntervalTimeout = DWORD.max; 568 tm.ReadTotalTimeoutMultiplier = 0; 569 tm.ReadTotalTimeoutConstant = 0; 570 tm.WriteTotalTimeoutMultiplier = 0; 571 tm.WriteTotalTimeoutConstant = 0; 572 573 if (SetCommTimeouts(handle, &tm) == 0) 574 throw new SetupFailException(port, 575 format("can't SetCommTimeouts with error: %d", GetLastError())); 576 } 577 578 config = conf; 579 } 580 } 581 582 private bool hasFlag(A,B)(A a, B b) @property { return (a & b) == b; }