//| Copyright: (C) 2020-2024 Kevin Larke //| License: GNU GPL version 3.0 or above. See the accompanying LICENSE file. #include "cwCommon.h" #include "cwLog.h" #include "cwCommonImpl.h" #include "cwMem.h" #include "cwSerialPort.h" #include #include #include // ::close() #include // O_RDWR #include // TIOCEXCL namespace cw { namespace serialPort { typedef struct port_str { unsigned _userId; char* _deviceStr; int _deviceH; unsigned _baudRate; unsigned _cfgFlags; callbackFunc_t _cbFunc; void* _cbArg; struct termios _ttyAttrs; struct pollfd* _pollfd; struct port_str* _link; } port_t; typedef struct device_str { unsigned _recvBufByteN; void* _recvBuf; port_t* _portL; unsigned _pollfdN; struct pollfd* _pollfd; } device_t; inline device_t* _handleToPtr(handle_t h) { return handleToPtr(h); } bool _isPortOpen( port_t* p ) { return p->_deviceH != -1; } // Given a 'userId' return the assoc'd port record. port_t* _idToPort( device_t* d, unsigned userId, bool errorFl = true ) { port_t* p = d->_portL; while( p != nullptr ) { if( userId == p->_userId ) return p; p = p->_link; } if( errorFl ) cwLogError(kInvalidIdRC,"No port was found with id:%i.",userId); return nullptr; } rc_t _getAttributes( port_t* p, struct termios& attr ) { if( tcgetattr(p->_deviceH, &attr) == -1 ) return cwLogSysError(kGetAttrFailRC,errno,"Error getting tty attributes from %s.",p->_deviceStr); return kOkRC; } // Called to read the serial device when data is known to be waiting. rc_t _receive( device_t* d, port_t* p, unsigned& readN_Ref, void* buf=nullptr, unsigned bufByteN=0 ) { rc_t rc = kOkRC; void* b = buf; unsigned bN = bufByteN; int n = 0; readN_Ref = 0; if( !_isPortOpen(p) ) return cwLogWarningRC( kResourceNotAvailableRC, "An attempt was made to read from a closed serial port."); // if a buffer was not given if( b ==nullptr || bufByteN == 0 ) { b = d->_recvBuf; bN = d->_recvBufByteN; } // if attempt to read the port succeeded ... if((n =read( p->_deviceH, b, bN )) != -1 ) { readN_Ref = n; if( n>0 && p->_cbFunc != nullptr && (buf == nullptr || bufByteN == 0) ) p->_cbFunc( p->_cbArg, p->_userId, b, n ); } else { // ... or failed and it wasn't because the port was empty if( errno != EAGAIN) rc = cwLogSysError(kReadFailRC,errno,"An attempt to read the serial port '%s' failed.", p->_deviceStr ); } return rc; } // Block devices waiting for data on a port. If userId is valid then wait for data on a specific port otherwise // wait for data on all ports. rc_t _poll( device_t* d, unsigned timeOutMs, unsigned& readN_Ref, unsigned userId=kInvalidId, void* buf=nullptr, unsigned bufByteN=0 ) { rc_t rc = kOkRC; int sysRC; readN_Ref = 0; // if there are not ports then there is nothing to do if( d->_pollfdN == 0 ) return rc; struct pollfd* pfd = d->_pollfd; // first struct pollfd unsigned pfdN = d->_pollfdN; // count of pollfd's port_t* p = d->_portL; // port assoc'd with first pollfd // if only one port is to be read ... if( userId != kInvalidId ) { // ... then locate it if((p = _idToPort(d,userId)) == nullptr ) return kInvalidArgRC; pfd = p->_pollfd; pfdN = 1; } // A port to poll must exist if( p == nullptr ) return cwLogError(kInvalidArgRC,"The port with id %i could not be found.",userId); // block waiting for data on one of the ports if((sysRC = ::poll(pfd,pfdN,timeOutMs)) == 0) rc = kTimeOutRC; else { // ::poll() encountered a system exception if( sysRC < 0 ) return cwLogSysError(kReadFailRC,errno,"Poll failed on serial port."); // interate through the ports looking for the ones which have data waiting ... for(unsigned i=0; p!=nullptr; p=p->_link,++i) if( p->_pollfd->revents & POLLIN ) { unsigned actualReadN = 0; // ... then read the data if((rc = _receive( d, p, actualReadN, buf, bufByteN )) != kOkRC ) return rc; readN_Ref += actualReadN; } } return rc; } rc_t _closePort( device_t* d, port_t* p ) { rc_t rc = kOkRC; // if the port is already closed if( p->_deviceH != -1 ) { // Block until all written output has been sent from the device. // Note that this call is simply passed on to the serial device driver. // See tcsendbreak(3) ("man 3 tcsendbreak") for details. if (tcdrain(p->_deviceH) == -1) { rc = cwLogSysError(kFlushFailRC,errno,"Error waiting for serial device '%s' to drain.", p->_deviceStr ); goto errLabel; } // It is good practice to reset a serial port back to the state in // which you found it. This is why we saved the original termios struct // The constant TCSANOW (defined in termios.h) indicates that // the change should take effect immediately. if (tcsetattr(p->_deviceH, TCSANOW, &p->_ttyAttrs) == -1) { rc = cwLogSysError(kSetAttrFailRC,errno,"Error resetting tty attributes on serial device '%s'.",p->_deviceStr); goto errLabel; } if( ::close(p->_deviceH ) != 0 ) { rc = cwLogSysError(kCloseFailRC,errno,"Port close failed on serial dvice '%s'.", p->_deviceStr); goto errLabel; } } // reset the state of the port record mem::release(p->_deviceStr); p->_userId = kInvalidId; p->_deviceH = -1; p->_baudRate = 0; p->_cfgFlags = 0; p->_cbFunc = nullptr; p->_cbArg = nullptr; p->_pollfd->events = 0; p->_pollfd->fd = -1; errLabel: return rc; } // Destroy the manager object. rc_t _destroy( device_t* d ) { rc_t rc = kOkRC; port_t* p = d->_portL; while( p != nullptr ) { port_t* p0 = p->_link; if((rc = _closePort(d,p)) != kOkRC ) return rc; mem::release(p); p = p0; } mem::release(d->_recvBuf); mem::release(d); return rc; } } } cw::rc_t cw::serialPort::create( handle_t& h, unsigned recvBufByteN ) { rc_t rc; if((rc = destroy(h)) != kOkRC ) return rc; device_t* d = mem::allocZ(); if( recvBufByteN > 0 ) { d->_recvBuf = mem::allocZ( recvBufByteN ); d->_recvBufByteN = recvBufByteN; } h.set(d); return rc; } cw::rc_t cw::serialPort::destroy( handle_t& h ) { rc_t rc = kOkRC; if( !h.isValid() ) return rc; device_t* d = _handleToPtr(h); if((rc = _destroy(d)) != kOkRC ) return rc; h.clear(); return rc; } cw::rc_t cw::serialPort::createPort( handle_t h, unsigned userId, const char* deviceStr, unsigned baudRate, unsigned cfgFlags, callbackFunc_t cbFunc, void* cbArg ) { rc_t rc = kOkRC; device_t* d = _handleToPtr(h); struct termios options; port_t* p; // locate the port with the given user id if((p = _idToPort(d,userId,false)) != nullptr ) if((rc = _closePort(d,p)) != kOkRC ) return rc; // if a new port record must be allocated if( p == nullptr ) { unsigned portN = 0; // look for an available port and count the number of existing ports for(p=d->_portL; p!=nullptr; p=p->_link,++portN) if( !_isPortOpen(p) ) break; if( p == nullptr ) { // allocate and link in the new port desc. record p = mem::allocZ(); p->_deviceH = -1; // link in the new port as the first port p->_link = d->_portL; d->_portL = p; // A new port has been allocated reallocate the pollfd array mem::release(d->_pollfd); d->_pollfdN = portN + 1; d->_pollfd = mem::allocZ(d->_pollfdN); // link the port records to the their assoc'd pollfd record in d->_pollfd[] port_t* pp = d->_portL; for(unsigned i=0; pp!=nullptr; ++i,pp=pp->_link) { pp->_pollfd = d->_pollfd + i; pp->_pollfd->fd = pp->_deviceH; pp->_pollfd->events = POLLIN; } } } // open the port if( (p->_deviceH = ::open(deviceStr, O_RDWR | O_NOCTTY | O_NONBLOCK)) == -1 ) { rc = cwLogSysError(kOpenFailRC,errno,"Error opening serial '%s'",cwStringNullGuard(deviceStr)); goto errLabel;; } // Note that open() follows POSIX semantics: multiple open() calls to // the same file will succeed unless the TIOCEXCL ioctl is issued. // This will prevent additional opens except by root-owned processes. // See tty(4) ("man 4 tty") and ioctl(2) ("man 2 ioctl") for details. if( ioctl(p->_deviceH, TIOCEXCL) == -1 ) { rc = cwLogSysError(kResourceNotAvailableRC,errno,"The serial device '%s' is already in use.", cwStringNullGuard(deviceStr)); goto errLabel; } // Now that the device is open, clear the O_NONBLOCK flag so // subsequent I/O will block. // See fcntl(2) ("man 2 fcntl") for details. /* if (fcntl(_deviceH, F_SETFL, 0) == -1) { _error("Error clearing O_NONBLOCK %s - %s(%d).", pr.devFilePath.c_str(), strerror(errno), errno); goto errLabel; } */ // Get the current options and save them so we can restore the // default settings later. if (tcgetattr(p->_deviceH, &p->_ttyAttrs) == -1) { rc = cwLogSysError(kGetAttrFailRC,errno,"Error getting tty attributes from the device '%s'.",deviceStr); goto errLabel; } // The serial port attributes such as timeouts and baud rate are set by // modifying the termios structure and then calling tcsetattr to // cause the changes to take effect. Note that the // changes will not take effect without the tcsetattr() call. // See tcsetattr(4) ("man 4 tcsetattr") for details. options = p->_ttyAttrs; // Set raw input (non-canonical) mode, with reads blocking until either // a single character has been received or a 100ms timeout expires. // See tcsetattr(4) ("man 4 tcsetattr") and termios(4) ("man 4 termios") // for details. cfmakeraw(&options); options.c_cc[VMIN] = 1; options.c_cc[VTIME] = 1; // The baud rate, word length, and handshake options can be set as follows: // set baud rate cfsetspeed(&options, baudRate); options.c_cflag |= CREAD | CLOCAL; // ignore modem controls // set data word size cwClrBits(options.c_cflag, CSIZE); // clear the word size bits cwEnaBits(options.c_cflag, CS5, cwIsFlag(cfgFlags, kDataBits5Fl)); cwEnaBits(options.c_cflag, CS6, cwIsFlag(cfgFlags, kDataBits6Fl)); cwEnaBits(options.c_cflag, CS7, cwIsFlag(cfgFlags, kDataBits7Fl)); cwEnaBits(options.c_cflag, CS8, cwIsFlag(cfgFlags, kDataBits8Fl)); cwClrBits(options.c_cflag, PARENB); // assume no-parity // if the odd or even parity flag is set if( cwIsFlag( cfgFlags, kEvenParityFl) || cwIsFlag( cfgFlags, kOddParityFl ) ) { cwSetBits(options.c_cflag, PARENB); if( cwIsFlag(cfgFlags, kOddParityFl ) ) cwSetBits( options.c_cflag, PARODD); } // set two stop bits cwEnaBits( options.c_cflag, CSTOPB, cwIsFlag(cfgFlags, k2StopBitFl)); // set hardware flow control //cwEnaBits(options.c_cflag, CCTS_OFLOW, cwIsFlag(cfgFlags, kCTS_OutFlowCtlFl)); //cwEnaBits(options.c_cflag, CRTS_IFLOW, cwIsFlag(cfgFlags, kRTS_InFlowCtlFl)); //cwEnaBits(options.c_cflag, CDTR_IFLOW, cwIsFlag(cfgFlags, kDTR_InFlowCtlFl)); //cwEnaBits(options.c_cflag, CDSR_OFLOW, cwIsFlag(cfgFlags, kDSR_OutFlowCtlFl)); //cwEnaBits(options.c_cflag, CCAR_OFLOW, cwIsFlag(cfgFlags, kDCD_OutFlowCtlFl)); cwClrBits(options.c_cflag,CRTSCTS); // turn-off hardware flow control // 7 bit words, enable even parity, CTS out ctl flow, RTS in ctl flow // note: set PARODD and PARENB to enable odd parity) //options.c_cflag |= (CS7 | PARENB | CCTS_OFLOW | CRTS_IFLOW ); // Cause the new options to take effect immediately. if (tcsetattr(p->_deviceH, TCSANOW, &options) == -1) { rc = cwLogSysError(kSetAttrFailRC,errno,"Error setting tty attributes on serial device %.", deviceStr); goto errLabel; } p->_userId = userId; p->_deviceStr = cw::mem::allocStr( deviceStr ); p->_baudRate = baudRate; p->_cfgFlags = cfgFlags; p->_cbFunc = cbFunc; p->_cbArg = cbArg; p->_pollfd->fd = p->_deviceH; p->_pollfd->events = POLLIN; h.set(d); errLabel: if( rc != kOkRC ) _closePort(d,p); return rc; } cw::rc_t cw::serialPort::destroyPort(handle_t h, unsigned userId ) { rc_t rc = kOkRC; if( !isopen(h,userId) ) return rc; device_t* d = _handleToPtr(h); port_t* p; // find the port to close if((p = _idToPort(d,userId)) == nullptr ) return kInvalidArgRC; // Close the selected port // Note that closed ports are simply marked as closed (deviceH==-1) but not removed from the list. if((rc = _closePort(d,p)) != kOkRC ) return rc; h.clear(); return rc; } unsigned cw::serialPort::portCount( handle_t h ) { device_t* d = _handleToPtr(h); return d->_pollfdN; } unsigned cw::serialPort::portIndexToId( handle_t h, unsigned index ) { device_t* d = _handleToPtr(h); unsigned i = 0; for(port_t* p=d->_portL; p!=nullptr; p=p->_link,++i) if( i == index ) return p->_userId; return kInvalidId; } bool cw::serialPort::isopen( handle_t h, unsigned userId ) { device_t* d = _handleToPtr(h); port_t* p; if((p = _idToPort(d,userId)) == nullptr ) return false; return _isPortOpen(p); } cw::rc_t cw::serialPort::send( handle_t h, unsigned userId, const void* byteA, unsigned byteN ) { rc_t rc = kOkRC; device_t* d = _handleToPtr(h); port_t* p; if( byteN == 0 ) return rc; if((p = _idToPort(d,userId)) == nullptr ) return kInvalidArgRC; // implement a non blocking write - if less than all the bytes were written then iterate unsigned i = 0; do { int n = 0; if((n = write( p->_deviceH, ((char*)byteA)+i, byteN-i )) == -1 ) { rc = cwLogSysError(kWriteFailRC,errno,"Write failed on serial port '%s'.", p->_deviceStr ); break; } i += n; }while( i_deviceStr; } unsigned cw::serialPort::baudRate( handle_t h, unsigned userId ) { device_t* d = _handleToPtr(h); port_t* p; if((p = _idToPort(d,userId)) == NULL) return 0; return p->_baudRate; } unsigned cw::serialPort::cfgFlags( handle_t h, unsigned userId ) { device_t* d = _handleToPtr(h); port_t* p; if((p = _idToPort(d,userId)) == NULL) return 0; return p->_cfgFlags; } unsigned cw::serialPort::readInBaudRate( handle_t h, unsigned userId ) { struct termios attr; device_t* d = _handleToPtr(h); port_t* p; if((p = _idToPort(d,userId)) == NULL) return 0; if((_getAttributes(p,attr)) != kOkRC ) return 0; return cfgetispeed(&attr); } unsigned cw::serialPort::readOutBaudRate( handle_t h, unsigned userId ) { struct termios attr; device_t* d = _handleToPtr(h); port_t* p; if((p = _idToPort(d,userId)) == NULL) return 0; if((_getAttributes(p,attr)) != kOkRC ) return 0; return cfgetospeed(&attr); } unsigned cw::serialPort::readCfgFlags( handle_t h, unsigned userId ) { struct termios attr; unsigned result = 0; device_t* d = _handleToPtr(h); port_t* p; if((p = _idToPort(d,userId)) == NULL) return 0; if((_getAttributes(p,attr)) == false ) return 0; switch( attr.c_cflag & CSIZE ) { case CS5: cwSetBits( result, kDataBits5Fl); break; case CS6: cwSetBits( result, kDataBits6Fl ); break; case CS7: cwSetBits( result, kDataBits7Fl); break; case CS8: cwSetBits( result, kDataBits8Fl); break; } cwEnaBits( result, k2StopBitFl, cwIsFlag( attr.c_cflag, CSTOPB )); cwEnaBits( result, k1StopBitFl, !cwIsFlag( attr.c_cflag, CSTOPB )); if( cwIsFlag( attr.c_cflag, PARENB ) ) { cwEnaBits( result, kOddParityFl, cwIsFlag( attr.c_cflag, PARODD )); cwEnaBits( result, kEvenParityFl, !cwIsFlag( attr.c_cflag, PARODD )); } return result; }