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_