diff --git a/.gitignore b/.gitignore index d163863..47eb61e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -build/ \ No newline at end of file +build/ +cmake-build-debug/ +.idea/workspace.xml \ No newline at end of file diff --git a/.idea/CppLinuxSerial.iml b/.idea/CppLinuxSerial.iml new file mode 100644 index 0000000..f08604b --- /dev/null +++ b/.idea/CppLinuxSerial.iml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..79b3c94 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..efb4d8f --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index b56b1e0..7d29aaa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.1.0) project(CppLinuxSerial) -add_definitions(-std=c++11) +add_definitions(-std=c++14) option(BUILD_TESTS "If set to true, unit tests will be build as part of make all." TRUE) if (BUILD_TESTS) diff --git a/include/CppLinuxSerial/SerialPort.hpp b/include/CppLinuxSerial/SerialPort.hpp index 3839c3c..20d1b4b 100644 --- a/include/CppLinuxSerial/SerialPort.hpp +++ b/include/CppLinuxSerial/SerialPort.hpp @@ -19,85 +19,92 @@ // User headers -namespace mn -{ -namespace CppLinuxSerial -{ +namespace mn { + namespace CppLinuxSerial { /// \brief Strongly-typed enumeration of baud rates for use with the SerialPort class -enum class BaudRate -{ - none, - b9600, - b57600 -}; + enum class BaudRate { + none, + b9600, + b57600 + }; + + enum class State { + UNCONFIGURED, + CLOSED, + OPEN + }; /// \brief SerialPort object is used to perform rx/tx serial communication. -class SerialPort -{ + class SerialPort { - public: - /// \brief Default constructor. - SerialPort(); + public: + /// \brief Default constructor. + SerialPort(); - /// \brief Constructor that sets up serial port with all required parameters. - SerialPort(const std::string& device, BaudRate baudRate); + /// \brief Constructor that sets up serial port with all required parameters. + SerialPort(const std::string &device, BaudRate baudRate); - //! @brief Destructor - virtual ~SerialPort(); + //! @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. - void SetDevice(const std::string& device); + //! @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. + void SetDevice(const std::string &device); - void SetBaudRate(BaudRate baudRate); + 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 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 Enables/disables echo. - //! param echoOn Pass in true to enable echo, false to disable echo. - void EnableEcho(bool echoOn); + //! @brief Enables/disables echo. + //! param echoOn Pass in true to enable echo, false to disable echo. + void EnableEcho(bool echoOn); - //! @brief Opens the COM port for use. - //! @throws {std::runtime_error} if filename has not been set. - //! {std::system_error} if system open() operation fails. - //! @note Must call this before you can configure the COM port. - void Open(); + //! @brief Opens the COM port for use. + //! @throws {std::runtime_error} if filename has not been set. + //! {std::system_error} if system open() operation fails. + //! @note Must call this before you can configure the COM port. + void Open(); - /// \brief Configures the tty device as a serial port. - void ConfigureDeviceAsSerialPort(); + /// \brief Configures the tty device as a serial port. + /// \warning Device must be open (valid file descriptor) when this is called. + void ConfigureDeviceAsSerialPort(); - //! @brief Closes the COM port. - void Close(); + //! @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. - void Write(std::string *str); + //! @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. + void Write(std::string *str); - //! @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. - void Read(std::string *str); + //! @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. + void Read(std::string *str); - private: - std::string device_; + private: - BaudRate baudRate_; + /// \brief Keeps track of the serial port's state. + State state_; - //! @brief The file descriptor for the open file. This gets written to when Open() is called. - int fileDesc; + std::string device_; - //! @brief Returns a populated termios structure for the passed in file descriptor. - termios GetTermios(); + BaudRate baudRate_; - void SetTermios(termios myTermios); -}; + //! @brief The file descriptor for the open file. This gets written to when Open() is called. + int fileDesc_; -} // namespace CppLinuxSerial + //! @brief Returns a populated termios structure for the passed in file descriptor. + termios GetTermios(); + + void SetTermios(termios myTermios); + }; + + } // namespace CppLinuxSerial } // namespace mn #endif // #ifndef SERIAL_PORT_SERIAL_PORT_H diff --git a/src/SerialPort.cpp b/src/SerialPort.cpp index 9828452..e0ebe67 100644 --- a/src/SerialPort.cpp +++ b/src/SerialPort.cpp @@ -1,6 +1,6 @@ //! //! @file SerialPort.cpp -//! @author Geoffrey Hunter () +//! @author Geoffrey Hunter (www.mbedded.ninja) //! @created 2014-01-07 //! @last-modified 2017-11-23 //! @brief The main serial port class. @@ -22,9 +22,7 @@ namespace mn { namespace CppLinuxSerial { - SerialPort::SerialPort() : - SerialPort("", BaudRate::none) - { + SerialPort::SerialPort() { } SerialPort::SerialPort(const std::string& device, BaudRate baudRate) { @@ -32,14 +30,18 @@ namespace CppLinuxSerial { baudRate_ = baudRate; } - SerialPort::~SerialPort() - { - + SerialPort::~SerialPort() { + try { + Close(); + } catch(...) { + // We can't do anything about this! + } } void SerialPort::SetDevice(const std::string& device) { device_ = device; + ConfigureDeviceAsSerialPort(); } void SerialPort::SetBaudRate(BaudRate baudRate) @@ -89,18 +91,19 @@ namespace CppLinuxSerial { // O_RDONLY for read-only, O_WRONLY for write only, O_RDWR for both read/write access // 3rd, optional parameter is mode_t mode - this->fileDesc = open(device_.c_str(), O_RDWR); + fileDesc_ = open(device_.c_str(), O_RDWR); // Check status - if (this->fileDesc == -1) - { + if(fileDesc_ == -1) { // Could not open COM port //this->sp->PrintError(SmartPrint::Ss() << "Unable to open " << this->filePath << " - " << strerror(errno)); //return false; - throw std::system_error(EFAULT, std::system_category()); + throw std::runtime_error("Could not open device " + device_ + ". Is the device name correct and do you have read/write permission?"); } + ConfigureDeviceAsSerialPort(); + std::cout << "COM port opened successfully." << std::endl; // If code reaches here, open and config must of been successful @@ -231,7 +234,7 @@ namespace CppLinuxSerial { void SerialPort::Write(std::string* str) { - if(this->fileDesc == 0) + if(this->fileDesc_ == 0) { //this->sp->PrintError(SmartPrint::Ss() << ); //return false; @@ -239,7 +242,7 @@ namespace CppLinuxSerial { throw std::runtime_error("SendMsg called but file descriptor (fileDesc) was 0, indicating file has not been opened."); } - int writeResult = write(this->fileDesc, str->c_str(), str->size()); + int writeResult = write(this->fileDesc_, str->c_str(), str->size()); // Check status if (writeResult == -1) @@ -256,7 +259,7 @@ namespace CppLinuxSerial { void SerialPort::Read(std::string* str) { - if(this->fileDesc == 0) + if(this->fileDesc_ == 0) { //this->sp->PrintError(SmartPrint::Ss() << "Read() was called but file descriptor (fileDesc) was 0, indicating file has not been opened."); //return false; @@ -268,7 +271,7 @@ namespace CppLinuxSerial { memset (&buf, '\0', sizeof buf); // Read from file - int n = read(this->fileDesc, &buf, sizeof(buf)); + int n = read(this->fileDesc_, &buf, sizeof(buf)); // Error Handling if(n < 0) @@ -295,11 +298,14 @@ namespace CppLinuxSerial { termios SerialPort::GetTermios() { + if(fileDesc_ == -1) + throw std::runtime_error("GetTermios() called but file descriptor was not valid."); + struct termios tty; memset(&tty, 0, sizeof(tty)); // Get current settings (will be stored in termios structure) - if(tcgetattr(this->fileDesc, &tty) != 0) + if(tcgetattr(fileDesc_, &tty) != 0) { // Error occurred std::cout << "Could not get terminal attributes for \"" << device_ << "\" - " << strerror(errno) << std::endl; @@ -313,9 +319,9 @@ namespace CppLinuxSerial { void SerialPort::SetTermios(termios myTermios) { // Flush port, then apply attributes - tcflush(this->fileDesc, TCIFLUSH); + tcflush(this->fileDesc_, TCIFLUSH); - if(tcsetattr(this->fileDesc, TCSANOW, &myTermios) != 0) + if(tcsetattr(this->fileDesc_, TCSANOW, &myTermios) != 0) { // Error occurred std::cout << "Could not apply terminal attributes for \"" << device_ << "\" - " << strerror(errno) << std::endl; @@ -326,5 +332,17 @@ namespace CppLinuxSerial { // Successful! } + void SerialPort::Close() { + if(fileDesc_ != -1) { + auto retVal = close(fileDesc_); + if(retVal != 0) + throw std::runtime_error("Tried to close serial port " + device_ + ", but close() failed."); + + fileDesc_ = -1; + } + + state_ = State::CLOSED; + } + } // namespace CppLinuxSerial } // namespace mn diff --git a/test/unit/BasicTests.cpp b/test/unit/BasicTests.cpp index 06a96e6..d16b617 100644 --- a/test/unit/BasicTests.cpp +++ b/test/unit/BasicTests.cpp @@ -1,7 +1,21 @@ +/// +/// \file BasicTests.cpp +/// \author Geoffrey Hunter (www.mbedded.ninja) +/// \created 2017-11-24 +/// \last-modified 2017-11-24 +/// \brief Basic tests for the SerialPort class. +/// \details +/// See README.rst in repo root dir for more info. + +// System includes #include "gtest/gtest.h" +// 3rd party includes #include "CppLinuxSerial/SerialPort.hpp" +// User includes +#include "TestUtil.hpp" + using namespace mn::CppLinuxSerial; namespace { @@ -9,11 +23,29 @@ namespace { class BasicTests : public ::testing::Test { protected: + static void SetUpTestCase() { + GetTestUtil().CreateVirtualSerialPortPair(); + } + + static void TearDownTestCase() { + std::cout << "Destroying virtual serial ports..." << std::endl; + GetTestUtil().CloseSerialPorts(); + } + + static TestUtil& GetTestUtil() { + static TestUtil testUtil; + return testUtil; + } + + BasicTests() { } virtual ~BasicTests() { } + + std::string device0_ = "/dev/ttyS10"; + std::string device1_ = "/dev/ttyS11"; }; TEST_F(BasicTests, CanBeConstructed) { @@ -22,9 +54,17 @@ namespace { } TEST_F(BasicTests, CanOpen) { - SerialPort serialPort; - serialPort.Open(); - EXPECT_EQ(true, true); + SerialPort serialPort0(device0_, BaudRate::b57600); + serialPort0.Open(); + } + + TEST_F(BasicTests, ReadWrite) { + SerialPort serialPort0(device0_, BaudRate::b57600); + serialPort0.Open(); + + SerialPort serialPort1(device1_, BaudRate::b57600); + serialPort1.Open(); + } } // namespace \ No newline at end of file diff --git a/test/unit/TestUtil.hpp b/test/unit/TestUtil.hpp new file mode 100644 index 0000000..1f79f64 --- /dev/null +++ b/test/unit/TestUtil.hpp @@ -0,0 +1,160 @@ +/// +/// \file TestUtil.hpp +/// \author Geoffrey Hunter (www.mbedded.ninja) +/// \created 2017-11-24 +/// \last-modified 2017-11-24 +/// \brief Contains utility methods to help with testing. +/// \details +/// See README.rst in repo root dir for more info. + +#ifndef MN_CPP_LINUX_SERIAL_TEST_UTIL_H_ +#define MN_CPP_LINUX_SERIAL_TEST_UTIL_H_ + +// System includes +#include +#include +#include +#include +#include +#include + +// 3rd party includes + + +using namespace std::literals; + +#define READ 0 +#define WRITE 1 +FILE * popen2(std::string command, std::string type, int & pid) +{ + pid_t child_pid; + int fd[2]; + pipe(fd); + + if((child_pid = fork()) == -1) + { + perror("fork"); + exit(1); + } + + /* child process */ + if (child_pid == 0) + { + if (type == "r") + { + close(fd[READ]); //Close the READ end of the pipe since the child's fd is write-only + dup2(fd[WRITE], 1); //Redirect stdout to pipe + } + else + { + close(fd[WRITE]); //Close the WRITE end of the pipe since the child's fd is read-only + dup2(fd[READ], 0); //Redirect stdin to pipe + } + + setpgid(child_pid, child_pid); //Needed so negative PIDs can kill children of /bin/sh + execl("/bin/sh", "/bin/sh", "-c", command.c_str(), NULL); + exit(0); + } + else + { + if (type == "r") + { + close(fd[WRITE]); //Close the WRITE end of the pipe since parent's fd is read-only + } + else + { + close(fd[READ]); //Close the READ end of the pipe since parent's fd is write-only + } + } + + pid = child_pid; + + if (type == "r") + { + return fdopen(fd[READ], "r"); + } + + return fdopen(fd[WRITE], "w"); +} + +int pclose2(FILE * fp, pid_t pid) +{ + int stat; + + fclose(fp); + while (waitpid(pid, &stat, 0) == -1) + { + if (errno != EINTR) + { + stat = -1; + break; + } + } + + return stat; +} + +struct ProcessInfo { + FILE* fp; + pid_t pid; +}; + + +namespace mn { + namespace CppLinuxSerial { + + class TestUtil { + + public: + /// \brief Executes a command on the Linux command-line. + /// \details Blocks until command is complete. + /// \throws std::runtime_error is popen() fails. + static std::string Exec(const std::string &cmd) { + std::array buffer; + std::string result; + std::shared_ptr pipe(popen(cmd.c_str(), "r"), pclose); + if (!pipe) throw std::runtime_error("popen() failed!"); + + while (!feof(pipe.get())) { + if (fgets(buffer.data(), 128, pipe.get()) != nullptr) + result += buffer.data(); + } + + return result; + } + + void StartProcess(const std::string &cmd) { + std::array buffer; + std::string result; + int pid; + FILE * fp = popen2(cmd, "r", pid); + ProcessInfo processInfo; + processInfo.fp = fp; + processInfo.pid = pid; + processes_.push_back(processInfo); + } + + void CreateVirtualSerialPortPair() { + std::cout << "Creating virtual serial port pair..." << std::endl; + StartProcess("sudo socat -d -d pty,raw,echo=0,link=/dev/ttyS10 pty,raw,echo=0,link=/dev/ttyS11"); + std::this_thread::sleep_for(1s); + StartProcess("sudo chmod a+rw /dev/ttyS10"); + StartProcess("sudo chmod a+rw /dev/ttyS11"); + std::this_thread::sleep_for(1s); + std::cout << "Finished creating virtual serial port pair." << std::endl; + } + + void CloseSerialPorts() { + for(const auto& filePointer : processes_) { + kill(filePointer.pid, SIGKILL); + pclose2(filePointer.fp, filePointer.pid); + } + } + + std::vector processes_; + + }; + } // namespace CppLinuxSerial +} // namespace mn + +#endif // #ifndef MN_CPP_LINUX_SERIAL_TEST_UTIL_H_