From 01a18d8c0ef8d66b0c1cff7ea8de100f325d1a5b Mon Sep 17 00:00:00 2001 From: Geoffrey Hunter Date: Mon, 27 Nov 2017 10:49:11 -0800 Subject: [PATCH] Tidied up serial port API. --- CHANGELOG.md | 6 ++++-- include/CppLinuxSerial/Exception.hpp | 47 +++++++++++++++++++++++++++++++++++++++++++++++ include/CppLinuxSerial/SerialPort.hpp | 67 +++++++++++++++++++++++++++++++++++++++++-------------------------- src/SerialPort.cpp | 128 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------------------------- test/unit/ConfigTests.cpp | 8 ++++++++ 5 files changed, 171 insertions(+), 85 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9aa566..bcc000d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] ### Added -- CMake build support. -- Unit tests using gtest. +- Added CMake build support. +- Added basic, config and read/write unit tests using gtest. ### Changed - Updated serial port to use C++14. +- Changed library name from serial-port to CppLinuxSerial. +- Updated Doxygen comments. ## [v1.0.1] - 2014-05-21 diff --git a/include/CppLinuxSerial/Exception.hpp b/include/CppLinuxSerial/Exception.hpp index e69de29..9bb57ae 100644 --- a/include/CppLinuxSerial/Exception.hpp +++ b/include/CppLinuxSerial/Exception.hpp @@ -0,0 +1,47 @@ +/// +/// \file Exception.hpp +/// \author Geoffrey Hunter (www.mbedded.ninja) +/// \edited n/a +/// \created 2017-11-09 +/// \last-modified 2017-11-27 +/// \brief Contains the Exception class. File originally from https://github.com/mbedded-ninja/CppUtils. +/// \details +/// See README.md in root dir for more info. + +#ifndef MN_CPP_LINUX_SERIAL_EXCEPTION_H_ +#define MN_CPP_LINUX_SERIAL_EXCEPTION_H_ + +// System includes +#include +#include +#include +#include + +namespace mn { + namespace CppLinuxSerial { + + class Exception : public std::runtime_error { + + public: + Exception(const char *file, int line, const std::string &arg) : + std::runtime_error(arg) { + msg_ = std::string(file) + ":" + std::to_string(line) + ": " + arg; + } + + ~Exception() throw() {} + + const char *what() const throw() override { + return msg_.c_str(); + } + + private: + std::string msg_; + }; + + } // namespace CppLinuxSerial +} // namespace mn + +#define THROW_EXCEPT(arg) throw Exception(__FILE__, __LINE__, arg); + + +#endif // MN_CPP_LINUX_SERIAL_EXCEPTION_H_ \ No newline at end of file diff --git a/include/CppLinuxSerial/SerialPort.hpp b/include/CppLinuxSerial/SerialPort.hpp index 1d92102..9e0a755 100644 --- a/include/CppLinuxSerial/SerialPort.hpp +++ b/include/CppLinuxSerial/SerialPort.hpp @@ -32,7 +32,6 @@ namespace mn { }; enum class State { - UNCONFIGURED, CLOSED, OPEN }; @@ -41,27 +40,32 @@ namespace mn { class SerialPort { public: - /// \brief Default constructor. + /// \brief Default constructor. You must specify at least the device before calling Open(). SerialPort(); - /// \brief Constructor that sets up serial port with all required parameters. + /// \brief Constructor that sets up serial port with the basic (required) parameters. SerialPort(const std::string &device, BaudRate baudRate); //! @brief Destructor. Closes serial port if still open. virtual ~SerialPort(); - //! @brief Sets the file path to use for communications. The file path must be set before Open() is called, otherwise Open() will return an error. + /// \brief Sets the device to use for serial port communications. + /// \details Method can be called when serial port is in any state. void SetDevice(const std::string &device); void SetBaudRate(BaudRate baudRate); - //! @brief Controls what happens when Read() is called. - //! @param numOfCharToWait Minimum number of characters to wait for before returning. Set to 0 for non-blocking mode. - void SetNumCharsToWait(uint32_t numCharsToWait); + /// \brief Sets the read timeout (in milliseconds)/blocking mode. + /// \details Only call when state != OPEN. This method manupulates VMIN and VTIME. + /// \param timeout_ms Set to -1 to infinite timeout, 0 to return immediately with any data (non + /// blocking, or >0 to wait for data for a specified number of milliseconds). Timeout will + /// be rounded to the nearest 100ms (a Linux API restriction). Maximum value limited to + /// 25500ms (another Linux API restriction). + void SetTimeout(int32_t timeout_ms); - //! @brief Enables/disables echo. - //! param echoOn Pass in true to enable echo, false to disable echo. - void EnableEcho(bool echoOn); + /// \brief Enables/disables echo. + /// \param value Pass in true to enable echo, false to disable echo. + void SetEcho(bool value); //! @brief Opens the COM port for use. //! @throws {std::runtime_error} if filename has not been set. @@ -69,41 +73,52 @@ namespace mn { //! @note Must call this before you can configure the COM port. void Open(); - /// \brief Configures the tty device as a serial port. - /// \warning Device must be open (valid file descriptor) when this is called. - void ConfigureTermios(); - - //! @brief Closes the COM port. + /// \brief Closes the COM port. void Close(); - //! @brief Sends a message over the com port. - //! @param str Reference to an string containing the characters to write to the COM port. - //! @throws {std::runtime_error} if filename has not been set. - //! {std::system_error} if system write() operation fails. + /// \brief Sends a message over the com port. + /// \param data The data that will be written to the COM port. + /// \throws CppLinuxSerial::Exception if state != OPEN. void Write(const std::string& data); - //! @brief Use to read from the COM port. - //! @param str Reference to a string that the read characters from the COM port will be saved to. - //! @throws {std::runtime_error} if filename has not been set. - //! {std::system_error} if system read() operation fails. + /// \brief Use to read from the COM port. + /// \param data The object the read characters from the COM port will be saved to. + /// \param wait_ms The amount of time to wait for data. Set to 0 for non-blocking mode. Set to -1 + /// to wait indefinitely for new data. + /// \throws CppLinuxSerial::Exception if state != OPEN. void Read(std::string& data); private: + /// \brief Configures the tty device as a serial port. + /// \warning Device must be open (valid file descriptor) when this is called. + void ConfigureTermios(); + + void SetTermios(termios myTermios); + /// \brief Keeps track of the serial port's state. State state_; + /// \brief The file path to the serial port device (e.g. "/dev/ttyUSB0"). std::string device_; + /// \brief The current baud rate. BaudRate baudRate_; - //! @brief The file descriptor for the open file. This gets written to when Open() is called. + /// \brief The file descriptor for the open file. This gets written to when Open() is called. int fileDesc_; - //! @brief Returns a populated termios structure for the passed in file descriptor. + bool echo_; + + int32_t timeout_ms_; + + /// \brief Returns a populated termios structure for the passed in file descriptor. termios GetTermios(); - void SetTermios(termios myTermios); + static constexpr BaudRate defaultBaudRate_ = BaudRate::B_57600; + static constexpr int32_t defaultTimeout_ms_ = -1; + + }; } // namespace CppLinuxSerial diff --git a/src/SerialPort.cpp b/src/SerialPort.cpp index 9d8fa8a..48c5984 100644 --- a/src/SerialPort.cpp +++ b/src/SerialPort.cpp @@ -2,11 +2,12 @@ //! @file SerialPort.cpp //! @author Geoffrey Hunter (www.mbedded.ninja) //! @created 2014-01-07 -//! @last-modified 2017-11-23 +//! @last-modified 2017-11-27 //! @brief The main serial port class. //! @details //! See README.rst in repo root dir for more info. +// System includes #include #include #include // Standard input/output definitions @@ -17,17 +18,23 @@ #include // POSIX terminal control definitions (struct termios) #include // For throwing std::system_error +// User includes +#include "CppLinuxSerial/Exception.hpp" #include "CppLinuxSerial/SerialPort.hpp" namespace mn { namespace CppLinuxSerial { SerialPort::SerialPort() { + echo_ = false; + timeout_ms_ = defaultTimeout_ms_; + baudRate_ = defaultBaudRate_; } - SerialPort::SerialPort(const std::string& device, BaudRate baudRate) { + SerialPort::SerialPort(const std::string& device, BaudRate baudRate) : + SerialPort() { device_ = device; - baudRate_ = baudRate; + baudRate_ = baudRate; } SerialPort::~SerialPort() { @@ -35,19 +42,22 @@ namespace CppLinuxSerial { Close(); } catch(...) { // We can't do anything about this! + // But we don't want to throw within destructor, so swallow } } - void SerialPort::SetDevice(const std::string& device) - { + void SerialPort::SetDevice(const std::string& device) { device_ = device; + if(state_ == State::OPEN) + + ConfigureTermios(); } - void SerialPort::SetBaudRate(BaudRate baudRate) - { + void SerialPort::SetBaudRate(BaudRate baudRate) { baudRate_ = baudRate; - ConfigureTermios(); + if(state_ == State::OPEN) + ConfigureTermios(); } void SerialPort::Open() @@ -59,7 +69,7 @@ namespace CppLinuxSerial { //this->sp->PrintError(SmartPrint::Ss() << "Attempted to open file when file path has not been assigned to."); //return false; - throw std::runtime_error("Attempted to open file when file path has not been assigned to."); + THROW_EXCEPT("Attempted to open file when file path has not been assigned to."); } // Attempt to open file @@ -75,25 +85,18 @@ namespace CppLinuxSerial { //this->sp->PrintError(SmartPrint::Ss() << "Unable to open " << this->filePath << " - " << strerror(errno)); //return false; - throw std::runtime_error("Could not open device " + device_ + ". Is the device name correct and do you have read/write permission?"); + THROW_EXCEPT("Could not open device " + device_ + ". Is the device name correct and do you have read/write permission?"); } ConfigureTermios(); std::cout << "COM port opened successfully." << std::endl; - - // If code reaches here, open and config must of been successful - + state_ = State::OPEN; } - void SerialPort::EnableEcho(bool echoOn) - { - termios settings = this->GetTermios(); - settings.c_lflag = echoOn - ? (settings.c_lflag | ECHO ) - : (settings.c_lflag & ~(ECHO)); - //tcsetattr( STDIN_FILENO, TCSANOW, &settings ); - this->SetTermios(settings); + void SerialPort::SetEcho(bool value) { + echo_ = value; + ConfigureTermios(); } void SerialPort::ConfigureTermios() @@ -147,14 +150,29 @@ namespace CppLinuxSerial { //================= CONTROL CHARACTERS (.c_cc[]) ==================// - // c_cc[WMIN] sets the number of characters to block (wait) for when read() is called. - // Set to 0 if you don't want read to block. Only meaningful when port set to non-canonical mode - //tty.c_cc[VMIN] = 1; - SetNumCharsToWait(1); - // c_cc[VTIME] sets the inter-character timer, in units of 0.1s. // Only meaningful when port is set to non-canonical mode - tty.c_cc[VTIME] = 5; // 0.5 seconds read timeout + // VMIN = 0, VTIME = 0: No blocking, return immediately with what is available + // VMIN > 0, VTIME = 0: read() waits for VMIN bytes, could block indefinitely + // VMIN = 0, VTIME > 0: Block until any amount of data is available, OR timeout occurs + // VMIN > 0, VTIME > 0: Block until either VMIN characters have been received, or VTIME + // after first character has elapsed + // c_cc[WMIN] sets the number of characters to block (wait) for when read() is called. + // Set to 0 if you don't want read to block. Only meaningful when port set to non-canonical mode + + if(timeout_ms_ == -1) { + // Always wait for at least one byte, this could + // block indefinitely + tty.c_cc[VTIME] = 0; + tty.c_cc[VMIN] = 1; + } else if(timeout_ms_ == 0) { + // Setting both to 0 will give a non-blocking read + tty.c_cc[VTIME] = 0; + tty.c_cc[VMIN] = 0; + } else if(timeout_ms_ > 0) { + tty.c_cc[VTIME] = (cc_t)(timeout_ms_/100); // 0.5 seconds read timeout + tty.c_cc[VMIN] = 0; + } //======================== (.c_iflag) ====================// @@ -162,16 +180,20 @@ namespace CppLinuxSerial { tty.c_iflag &= ~(IXON | IXOFF | IXANY); // Turn off s/w flow ctrl tty.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL); + + //=========================== LOCAL MODES (c_lflag) =======================// // Canonical input is when read waits for EOL or EOF characters before returning. In non-canonical mode, the rate at which // read() returns is instead controlled by c_cc[VMIN] and c_cc[VTIME] tty.c_lflag &= ~ICANON; // Turn off canonical input, which is suitable for pass-through - tty.c_lflag &= ~ECHO; // Turn off echo + echo_ ? (tty.c_lflag | ECHO ) : (tty.c_lflag & ~(ECHO)); // Configure echo depending on echo_ boolean tty.c_lflag &= ~ECHOE; // Turn off echo erase (echo erase only relevant if canonical input is active) tty.c_lflag &= ~ECHONL; // tty.c_lflag &= ~ISIG; // Disables recognition of INTR (interrupt), QUIT and SUSP (suspend) characters + + // Try and use raw function call //cfmakeraw(&tty); @@ -189,38 +211,21 @@ namespace CppLinuxSerial { }*/ } - void SerialPort::SetNumCharsToWait(uint32_t numCharsToWait) { - // Get current termios struct - termios myTermios = GetTermios(); - - // Save the number of characters to wait for - // to the control register - myTermios.c_cc[VMIN] = numCharsToWait; - - // Save termios back - SetTermios(myTermios); - } - void SerialPort::Write(const std::string& data) { - if(fileDesc_ == 0) { - //this->sp->PrintError(SmartPrint::Ss() << ); - //return false; - throw std::runtime_error("SendMsg called but file descriptor (fileDesc) was 0, indicating file has not been opened."); + if(state_ != State::OPEN) + THROW_EXCEPT(std::string() + __PRETTY_FUNCTION__ + " called but state != OPEN. Please call Open() first."); + + if(fileDesc_ < 0) { + THROW_EXCEPT(std::string() + __PRETTY_FUNCTION__ + " called but file descriptor < 0, indicating file has not been opened."); } int writeResult = write(fileDesc_, data.c_str(), data.size()); // Check status if (writeResult == -1) { - // Could not open COM port - //this->sp->PrintError(SmartPrint::Ss() << "Unable to write to \"" << this->filePath << "\" - " << strerror(errno)); - //return false; - throw std::system_error(EFAULT, std::system_category()); } - - // If code reaches here than write must of been successful } void SerialPort::Read(std::string& data) @@ -228,7 +233,7 @@ namespace CppLinuxSerial { if(fileDesc_ == 0) { //this->sp->PrintError(SmartPrint::Ss() << "Read() was called but file descriptor (fileDesc) was 0, indicating file has not been opened."); //return false; - throw std::runtime_error("Read() was called but file descriptor (fileDesc) was 0, indicating file has not been opened."); + THROW_EXCEPT("Read() was called but file descriptor (fileDesc) was 0, indicating file has not been opened."); } // Allocate memory for read buffer @@ -236,7 +241,7 @@ namespace CppLinuxSerial { memset (&buf, '\0', sizeof buf); // Read from file - int n = read(fileDesc_, &buf, sizeof(buf)); + ssize_t n = read(fileDesc_, &buf, sizeof(buf)); // Error Handling if(n < 0) { @@ -256,8 +261,7 @@ namespace CppLinuxSerial { // If code reaches here, read must of been successful } - termios SerialPort::GetTermios() - { + termios SerialPort::GetTermios() { if(fileDesc_ == -1) throw std::runtime_error("GetTermios() called but file descriptor was not valid."); @@ -279,9 +283,9 @@ namespace CppLinuxSerial { void SerialPort::SetTermios(termios myTermios) { // Flush port, then apply attributes - tcflush(this->fileDesc_, TCIFLUSH); + tcflush(fileDesc_, TCIFLUSH); - if(tcsetattr(this->fileDesc_, TCSANOW, &myTermios) != 0) + if(tcsetattr(fileDesc_, TCSANOW, &myTermios) != 0) { // Error occurred std::cout << "Could not apply terminal attributes for \"" << device_ << "\" - " << strerror(errno) << std::endl; @@ -296,7 +300,7 @@ namespace CppLinuxSerial { if(fileDesc_ != -1) { auto retVal = close(fileDesc_); if(retVal != 0) - throw std::runtime_error("Tried to close serial port " + device_ + ", but close() failed."); + THROW_EXCEPT("Tried to close serial port " + device_ + ", but close() failed."); fileDesc_ = -1; } @@ -304,5 +308,15 @@ namespace CppLinuxSerial { state_ = State::CLOSED; } + void SerialPort::SetTimeout(int32_t timeout_ms) { + if(timeout_ms < -1) + THROW_EXCEPT(std::string() + "timeout_ms provided to " + __PRETTY_FUNCTION__ + " was < -1, which is invalid."); + if(timeout_ms > 25500) + THROW_EXCEPT(std::string() + "timeout_ms provided to " + __PRETTY_FUNCTION__ + " was > 25500, which is invalid."); + if(state_ == State::OPEN) + THROW_EXCEPT(std::string() + __PRETTY_FUNCTION__ + " called while state == OPEN."); + timeout_ms_ = timeout_ms; + } + } // namespace CppLinuxSerial } // namespace mn diff --git a/test/unit/ConfigTests.cpp b/test/unit/ConfigTests.cpp index 94fe5fb..38315f7 100644 --- a/test/unit/ConfigTests.cpp +++ b/test/unit/ConfigTests.cpp @@ -43,6 +43,10 @@ namespace { EXPECT_NE(std::string::npos, sttyOutput_.find("speed 115200 baud")); } + //================================================================================================// + //======================================= LOCAL MODES (c_lflag) ==================================// + //================================================================================================// + TEST_F(ConfigTests, CanonicalModeOff) { EXPECT_NE(std::string::npos, sttyOutput_.find("-icanon")); } @@ -53,4 +57,8 @@ namespace { EXPECT_NE(std::string::npos, sttyOutput_.find("-echonl")); } + TEST_F(ConfigTests, InterruptQuitSuspCharsOff) { + EXPECT_NE(std::string::npos, sttyOutput_.find("-isig")); + } + } // namespace \ No newline at end of file -- libgit2 0.21.4