Commit 2e35cebd53a35d3059c893d6d2a35b1e5b29daa4

Authored by Geoffrey Hunter
1 parent 9bfbe366

Working on basic read/write unit test using two virtual serial ports.

.gitignore
1   -build/
2 1 \ No newline at end of file
  2 +build/
  3 +cmake-build-debug/
  4 +.idea/workspace.xml
3 5 \ No newline at end of file
... ...
.idea/CppLinuxSerial.iml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<module classpath="CMake" type="CPP_MODULE" version="4" />
0 3 \ No newline at end of file
... ...
.idea/misc.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<project version="4">
  3 + <component name="CMakeWorkspace" PROJECT_DIR="$PROJECT_DIR$" />
  4 +</project>
0 5 \ No newline at end of file
... ...
.idea/modules.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<project version="4">
  3 + <component name="ProjectModuleManager">
  4 + <modules>
  5 + <module fileurl="file://$PROJECT_DIR$/.idea/CppLinuxSerial.iml" filepath="$PROJECT_DIR$/.idea/CppLinuxSerial.iml" />
  6 + </modules>
  7 + </component>
  8 +</project>
0 9 \ No newline at end of file
... ...
.idea/vcs.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<project version="4">
  3 + <component name="VcsDirectoryMappings">
  4 + <mapping directory="$PROJECT_DIR$" vcs="Git" />
  5 + </component>
  6 +</project>
0 7 \ No newline at end of file
... ...
CMakeLists.txt
1 1 cmake_minimum_required(VERSION 3.1.0)
2 2 project(CppLinuxSerial)
3 3  
4   -add_definitions(-std=c++11)
  4 +add_definitions(-std=c++14)
5 5  
6 6 option(BUILD_TESTS "If set to true, unit tests will be build as part of make all." TRUE)
7 7 if (BUILD_TESTS)
... ...
include/CppLinuxSerial/SerialPort.hpp
... ... @@ -19,85 +19,92 @@
19 19  
20 20 // User headers
21 21  
22   -namespace mn
23   -{
24   -namespace CppLinuxSerial
25   -{
  22 +namespace mn {
  23 + namespace CppLinuxSerial {
26 24  
27 25 /// \brief Strongly-typed enumeration of baud rates for use with the SerialPort class
28   -enum class BaudRate
29   -{
30   - none,
31   - b9600,
32   - b57600
33   -};
  26 + enum class BaudRate {
  27 + none,
  28 + b9600,
  29 + b57600
  30 + };
  31 +
  32 + enum class State {
  33 + UNCONFIGURED,
  34 + CLOSED,
  35 + OPEN
  36 + };
34 37  
35 38 /// \brief SerialPort object is used to perform rx/tx serial communication.
36   -class SerialPort
37   -{
  39 + class SerialPort {
38 40  
39   - public:
40   - /// \brief Default constructor.
41   - SerialPort();
  41 + public:
  42 + /// \brief Default constructor.
  43 + SerialPort();
42 44  
43   - /// \brief Constructor that sets up serial port with all required parameters.
44   - SerialPort(const std::string& device, BaudRate baudRate);
  45 + /// \brief Constructor that sets up serial port with all required parameters.
  46 + SerialPort(const std::string &device, BaudRate baudRate);
45 47  
46   - //! @brief Destructor
47   - virtual ~SerialPort();
  48 + //! @brief Destructor. Closes serial port if still open.
  49 + virtual ~SerialPort();
48 50  
49   - //! @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.
50   - void SetDevice(const std::string& device);
  51 + //! @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.
  52 + void SetDevice(const std::string &device);
51 53  
52   - void SetBaudRate(BaudRate baudRate);
  54 + void SetBaudRate(BaudRate baudRate);
53 55  
54   - //! @brief Controls what happens when Read() is called.
55   - //! @param numOfCharToWait Minimum number of characters to wait for before returning. Set to 0 for non-blocking mode.
56   - void SetNumCharsToWait(uint32_t numCharsToWait);
  56 + //! @brief Controls what happens when Read() is called.
  57 + //! @param numOfCharToWait Minimum number of characters to wait for before returning. Set to 0 for non-blocking mode.
  58 + void SetNumCharsToWait(uint32_t numCharsToWait);
57 59  
58   - //! @brief Enables/disables echo.
59   - //! param echoOn Pass in true to enable echo, false to disable echo.
60   - void EnableEcho(bool echoOn);
  60 + //! @brief Enables/disables echo.
  61 + //! param echoOn Pass in true to enable echo, false to disable echo.
  62 + void EnableEcho(bool echoOn);
61 63  
62   - //! @brief Opens the COM port for use.
63   - //! @throws {std::runtime_error} if filename has not been set.
64   - //! {std::system_error} if system open() operation fails.
65   - //! @note Must call this before you can configure the COM port.
66   - void Open();
  64 + //! @brief Opens the COM port for use.
  65 + //! @throws {std::runtime_error} if filename has not been set.
  66 + //! {std::system_error} if system open() operation fails.
  67 + //! @note Must call this before you can configure the COM port.
  68 + void Open();
67 69  
68   - /// \brief Configures the tty device as a serial port.
69   - void ConfigureDeviceAsSerialPort();
  70 + /// \brief Configures the tty device as a serial port.
  71 + /// \warning Device must be open (valid file descriptor) when this is called.
  72 + void ConfigureDeviceAsSerialPort();
70 73  
71   - //! @brief Closes the COM port.
72   - void Close();
  74 + //! @brief Closes the COM port.
  75 + void Close();
73 76  
74   - //! @brief Sends a message over the com port.
75   - //! @param str Reference to an string containing the characters to write to the COM port.
76   - //! @throws {std::runtime_error} if filename has not been set.
77   - //! {std::system_error} if system write() operation fails.
78   - void Write(std::string *str);
  77 + //! @brief Sends a message over the com port.
  78 + //! @param str Reference to an string containing the characters to write to the COM port.
  79 + //! @throws {std::runtime_error} if filename has not been set.
  80 + //! {std::system_error} if system write() operation fails.
  81 + void Write(std::string *str);
79 82  
80   - //! @brief Use to read from the COM port.
81   - //! @param str Reference to a string that the read characters from the COM port will be saved to.
82   - //! @throws {std::runtime_error} if filename has not been set.
83   - //! {std::system_error} if system read() operation fails.
84   - void Read(std::string *str);
  83 + //! @brief Use to read from the COM port.
  84 + //! @param str Reference to a string that the read characters from the COM port will be saved to.
  85 + //! @throws {std::runtime_error} if filename has not been set.
  86 + //! {std::system_error} if system read() operation fails.
  87 + void Read(std::string *str);
85 88  
86   - private:
87   - std::string device_;
  89 + private:
88 90  
89   - BaudRate baudRate_;
  91 + /// \brief Keeps track of the serial port's state.
  92 + State state_;
90 93  
91   - //! @brief The file descriptor for the open file. This gets written to when Open() is called.
92   - int fileDesc;
  94 + std::string device_;
93 95  
94   - //! @brief Returns a populated termios structure for the passed in file descriptor.
95   - termios GetTermios();
  96 + BaudRate baudRate_;
96 97  
97   - void SetTermios(termios myTermios);
98   -};
  98 + //! @brief The file descriptor for the open file. This gets written to when Open() is called.
  99 + int fileDesc_;
99 100  
100   -} // namespace CppLinuxSerial
  101 + //! @brief Returns a populated termios structure for the passed in file descriptor.
  102 + termios GetTermios();
  103 +
  104 + void SetTermios(termios myTermios);
  105 + };
  106 +
  107 + } // namespace CppLinuxSerial
101 108 } // namespace mn
102 109  
103 110 #endif // #ifndef SERIAL_PORT_SERIAL_PORT_H
... ...
src/SerialPort.cpp
1 1 //!
2 2 //! @file SerialPort.cpp
3   -//! @author Geoffrey Hunter <gbmhunter@gmail.com> ()
  3 +//! @author Geoffrey Hunter <gbmhunter@gmail.com> (www.mbedded.ninja)
4 4 //! @created 2014-01-07
5 5 //! @last-modified 2017-11-23
6 6 //! @brief The main serial port class.
... ... @@ -22,9 +22,7 @@
22 22 namespace mn {
23 23 namespace CppLinuxSerial {
24 24  
25   - SerialPort::SerialPort() :
26   - SerialPort("", BaudRate::none)
27   - {
  25 + SerialPort::SerialPort() {
28 26 }
29 27  
30 28 SerialPort::SerialPort(const std::string& device, BaudRate baudRate) {
... ... @@ -32,14 +30,18 @@ namespace CppLinuxSerial {
32 30 baudRate_ = baudRate;
33 31 }
34 32  
35   - SerialPort::~SerialPort()
36   - {
37   -
  33 + SerialPort::~SerialPort() {
  34 + try {
  35 + Close();
  36 + } catch(...) {
  37 + // We can't do anything about this!
  38 + }
38 39 }
39 40  
40 41 void SerialPort::SetDevice(const std::string& device)
41 42 {
42 43 device_ = device;
  44 + ConfigureDeviceAsSerialPort();
43 45 }
44 46  
45 47 void SerialPort::SetBaudRate(BaudRate baudRate)
... ... @@ -89,18 +91,19 @@ namespace CppLinuxSerial {
89 91  
90 92 // O_RDONLY for read-only, O_WRONLY for write only, O_RDWR for both read/write access
91 93 // 3rd, optional parameter is mode_t mode
92   - this->fileDesc = open(device_.c_str(), O_RDWR);
  94 + fileDesc_ = open(device_.c_str(), O_RDWR);
93 95  
94 96 // Check status
95   - if (this->fileDesc == -1)
96   - {
  97 + if(fileDesc_ == -1) {
97 98 // Could not open COM port
98 99 //this->sp->PrintError(SmartPrint::Ss() << "Unable to open " << this->filePath << " - " << strerror(errno));
99 100 //return false;
100 101  
101   - throw std::system_error(EFAULT, std::system_category());
  102 + throw std::runtime_error("Could not open device " + device_ + ". Is the device name correct and do you have read/write permission?");
102 103 }
103 104  
  105 + ConfigureDeviceAsSerialPort();
  106 +
104 107 std::cout << "COM port opened successfully." << std::endl;
105 108  
106 109 // If code reaches here, open and config must of been successful
... ... @@ -231,7 +234,7 @@ namespace CppLinuxSerial {
231 234  
232 235 void SerialPort::Write(std::string* str)
233 236 {
234   - if(this->fileDesc == 0)
  237 + if(this->fileDesc_ == 0)
235 238 {
236 239 //this->sp->PrintError(SmartPrint::Ss() << );
237 240 //return false;
... ... @@ -239,7 +242,7 @@ namespace CppLinuxSerial {
239 242 throw std::runtime_error("SendMsg called but file descriptor (fileDesc) was 0, indicating file has not been opened.");
240 243 }
241 244  
242   - int writeResult = write(this->fileDesc, str->c_str(), str->size());
  245 + int writeResult = write(this->fileDesc_, str->c_str(), str->size());
243 246  
244 247 // Check status
245 248 if (writeResult == -1)
... ... @@ -256,7 +259,7 @@ namespace CppLinuxSerial {
256 259  
257 260 void SerialPort::Read(std::string* str)
258 261 {
259   - if(this->fileDesc == 0)
  262 + if(this->fileDesc_ == 0)
260 263 {
261 264 //this->sp->PrintError(SmartPrint::Ss() << "Read() was called but file descriptor (fileDesc) was 0, indicating file has not been opened.");
262 265 //return false;
... ... @@ -268,7 +271,7 @@ namespace CppLinuxSerial {
268 271 memset (&buf, '\0', sizeof buf);
269 272  
270 273 // Read from file
271   - int n = read(this->fileDesc, &buf, sizeof(buf));
  274 + int n = read(this->fileDesc_, &buf, sizeof(buf));
272 275  
273 276 // Error Handling
274 277 if(n < 0)
... ... @@ -295,11 +298,14 @@ namespace CppLinuxSerial {
295 298  
296 299 termios SerialPort::GetTermios()
297 300 {
  301 + if(fileDesc_ == -1)
  302 + throw std::runtime_error("GetTermios() called but file descriptor was not valid.");
  303 +
298 304 struct termios tty;
299 305 memset(&tty, 0, sizeof(tty));
300 306  
301 307 // Get current settings (will be stored in termios structure)
302   - if(tcgetattr(this->fileDesc, &tty) != 0)
  308 + if(tcgetattr(fileDesc_, &tty) != 0)
303 309 {
304 310 // Error occurred
305 311 std::cout << "Could not get terminal attributes for \"" << device_ << "\" - " << strerror(errno) << std::endl;
... ... @@ -313,9 +319,9 @@ namespace CppLinuxSerial {
313 319 void SerialPort::SetTermios(termios myTermios)
314 320 {
315 321 // Flush port, then apply attributes
316   - tcflush(this->fileDesc, TCIFLUSH);
  322 + tcflush(this->fileDesc_, TCIFLUSH);
317 323  
318   - if(tcsetattr(this->fileDesc, TCSANOW, &myTermios) != 0)
  324 + if(tcsetattr(this->fileDesc_, TCSANOW, &myTermios) != 0)
319 325 {
320 326 // Error occurred
321 327 std::cout << "Could not apply terminal attributes for \"" << device_ << "\" - " << strerror(errno) << std::endl;
... ... @@ -326,5 +332,17 @@ namespace CppLinuxSerial {
326 332 // Successful!
327 333 }
328 334  
  335 + void SerialPort::Close() {
  336 + if(fileDesc_ != -1) {
  337 + auto retVal = close(fileDesc_);
  338 + if(retVal != 0)
  339 + throw std::runtime_error("Tried to close serial port " + device_ + ", but close() failed.");
  340 +
  341 + fileDesc_ = -1;
  342 + }
  343 +
  344 + state_ = State::CLOSED;
  345 + }
  346 +
329 347 } // namespace CppLinuxSerial
330 348 } // namespace mn
... ...
test/unit/BasicTests.cpp
  1 +///
  2 +/// \file BasicTests.cpp
  3 +/// \author Geoffrey Hunter <gbmhunter@gmail.com> (www.mbedded.ninja)
  4 +/// \created 2017-11-24
  5 +/// \last-modified 2017-11-24
  6 +/// \brief Basic tests for the SerialPort class.
  7 +/// \details
  8 +/// See README.rst in repo root dir for more info.
  9 +
  10 +// System includes
1 11 #include "gtest/gtest.h"
2 12  
  13 +// 3rd party includes
3 14 #include "CppLinuxSerial/SerialPort.hpp"
4 15  
  16 +// User includes
  17 +#include "TestUtil.hpp"
  18 +
5 19 using namespace mn::CppLinuxSerial;
6 20  
7 21 namespace {
... ... @@ -9,11 +23,29 @@ namespace {
9 23 class BasicTests : public ::testing::Test {
10 24 protected:
11 25  
  26 + static void SetUpTestCase() {
  27 + GetTestUtil().CreateVirtualSerialPortPair();
  28 + }
  29 +
  30 + static void TearDownTestCase() {
  31 + std::cout << "Destroying virtual serial ports..." << std::endl;
  32 + GetTestUtil().CloseSerialPorts();
  33 + }
  34 +
  35 + static TestUtil& GetTestUtil() {
  36 + static TestUtil testUtil;
  37 + return testUtil;
  38 + }
  39 +
  40 +
12 41 BasicTests() {
13 42 }
14 43  
15 44 virtual ~BasicTests() {
16 45 }
  46 +
  47 + std::string device0_ = "/dev/ttyS10";
  48 + std::string device1_ = "/dev/ttyS11";
17 49 };
18 50  
19 51 TEST_F(BasicTests, CanBeConstructed) {
... ... @@ -22,9 +54,17 @@ namespace {
22 54 }
23 55  
24 56 TEST_F(BasicTests, CanOpen) {
25   - SerialPort serialPort;
26   - serialPort.Open();
27   - EXPECT_EQ(true, true);
  57 + SerialPort serialPort0(device0_, BaudRate::b57600);
  58 + serialPort0.Open();
  59 + }
  60 +
  61 + TEST_F(BasicTests, ReadWrite) {
  62 + SerialPort serialPort0(device0_, BaudRate::b57600);
  63 + serialPort0.Open();
  64 +
  65 + SerialPort serialPort1(device1_, BaudRate::b57600);
  66 + serialPort1.Open();
  67 +
28 68 }
29 69  
30 70 } // namespace
31 71 \ No newline at end of file
... ...
test/unit/TestUtil.hpp 0 → 100644
  1 +///
  2 +/// \file TestUtil.hpp
  3 +/// \author Geoffrey Hunter <gbmhunter@gmail.com> (www.mbedded.ninja)
  4 +/// \created 2017-11-24
  5 +/// \last-modified 2017-11-24
  6 +/// \brief Contains utility methods to help with testing.
  7 +/// \details
  8 +/// See README.rst in repo root dir for more info.
  9 +
  10 +#ifndef MN_CPP_LINUX_SERIAL_TEST_UTIL_H_
  11 +#define MN_CPP_LINUX_SERIAL_TEST_UTIL_H_
  12 +
  13 +// System includes
  14 +#include <string>
  15 +#include <array>
  16 +#include <memory>
  17 +#include <iostream>
  18 +#include <thread>
  19 +#include <chrono>
  20 +
  21 +// 3rd party includes
  22 +
  23 +
  24 +using namespace std::literals;
  25 +
  26 +#define READ 0
  27 +#define WRITE 1
  28 +FILE * popen2(std::string command, std::string type, int & pid)
  29 +{
  30 + pid_t child_pid;
  31 + int fd[2];
  32 + pipe(fd);
  33 +
  34 + if((child_pid = fork()) == -1)
  35 + {
  36 + perror("fork");
  37 + exit(1);
  38 + }
  39 +
  40 + /* child process */
  41 + if (child_pid == 0)
  42 + {
  43 + if (type == "r")
  44 + {
  45 + close(fd[READ]); //Close the READ end of the pipe since the child's fd is write-only
  46 + dup2(fd[WRITE], 1); //Redirect stdout to pipe
  47 + }
  48 + else
  49 + {
  50 + close(fd[WRITE]); //Close the WRITE end of the pipe since the child's fd is read-only
  51 + dup2(fd[READ], 0); //Redirect stdin to pipe
  52 + }
  53 +
  54 + setpgid(child_pid, child_pid); //Needed so negative PIDs can kill children of /bin/sh
  55 + execl("/bin/sh", "/bin/sh", "-c", command.c_str(), NULL);
  56 + exit(0);
  57 + }
  58 + else
  59 + {
  60 + if (type == "r")
  61 + {
  62 + close(fd[WRITE]); //Close the WRITE end of the pipe since parent's fd is read-only
  63 + }
  64 + else
  65 + {
  66 + close(fd[READ]); //Close the READ end of the pipe since parent's fd is write-only
  67 + }
  68 + }
  69 +
  70 + pid = child_pid;
  71 +
  72 + if (type == "r")
  73 + {
  74 + return fdopen(fd[READ], "r");
  75 + }
  76 +
  77 + return fdopen(fd[WRITE], "w");
  78 +}
  79 +
  80 +int pclose2(FILE * fp, pid_t pid)
  81 +{
  82 + int stat;
  83 +
  84 + fclose(fp);
  85 + while (waitpid(pid, &stat, 0) == -1)
  86 + {
  87 + if (errno != EINTR)
  88 + {
  89 + stat = -1;
  90 + break;
  91 + }
  92 + }
  93 +
  94 + return stat;
  95 +}
  96 +
  97 +struct ProcessInfo {
  98 + FILE* fp;
  99 + pid_t pid;
  100 +};
  101 +
  102 +
  103 +namespace mn {
  104 + namespace CppLinuxSerial {
  105 +
  106 + class TestUtil {
  107 +
  108 + public:
  109 + /// \brief Executes a command on the Linux command-line.
  110 + /// \details Blocks until command is complete.
  111 + /// \throws std::runtime_error is popen() fails.
  112 + static std::string Exec(const std::string &cmd) {
  113 + std::array<char, 128> buffer;
  114 + std::string result;
  115 + std::shared_ptr<FILE> pipe(popen(cmd.c_str(), "r"), pclose);
  116 + if (!pipe) throw std::runtime_error("popen() failed!");
  117 +
  118 + while (!feof(pipe.get())) {
  119 + if (fgets(buffer.data(), 128, pipe.get()) != nullptr)
  120 + result += buffer.data();
  121 + }
  122 +
  123 + return result;
  124 + }
  125 +
  126 + void StartProcess(const std::string &cmd) {
  127 + std::array<char, 128> buffer;
  128 + std::string result;
  129 + int pid;
  130 + FILE * fp = popen2(cmd, "r", pid);
  131 + ProcessInfo processInfo;
  132 + processInfo.fp = fp;
  133 + processInfo.pid = pid;
  134 + processes_.push_back(processInfo);
  135 + }
  136 +
  137 + void CreateVirtualSerialPortPair() {
  138 + std::cout << "Creating virtual serial port pair..." << std::endl;
  139 + StartProcess("sudo socat -d -d pty,raw,echo=0,link=/dev/ttyS10 pty,raw,echo=0,link=/dev/ttyS11");
  140 + std::this_thread::sleep_for(1s);
  141 + StartProcess("sudo chmod a+rw /dev/ttyS10");
  142 + StartProcess("sudo chmod a+rw /dev/ttyS11");
  143 + std::this_thread::sleep_for(1s);
  144 + std::cout << "Finished creating virtual serial port pair." << std::endl;
  145 + }
  146 +
  147 + void CloseSerialPorts() {
  148 + for(const auto& filePointer : processes_) {
  149 + kill(filePointer.pid, SIGKILL);
  150 + pclose2(filePointer.fp, filePointer.pid);
  151 + }
  152 + }
  153 +
  154 + std::vector<ProcessInfo> processes_;
  155 +
  156 + };
  157 + } // namespace CppLinuxSerial
  158 +} // namespace mn
  159 +
  160 +#endif // #ifndef MN_CPP_LINUX_SERIAL_TEST_UTIL_H_
... ...