Commit 01a18d8c0ef8d66b0c1cff7ea8de100f325d1a5b

Authored by Geoffrey Hunter
1 parent efaea75e

Tidied up serial port API.

CHANGELOG.md
... ... @@ -7,11 +7,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
7 7 ## [Unreleased]
8 8  
9 9 ### Added
10   -- CMake build support.
11   -- Unit tests using gtest.
  10 +- Added CMake build support.
  11 +- Added basic, config and read/write unit tests using gtest.
12 12  
13 13 ### Changed
14 14 - Updated serial port to use C++14.
  15 +- Changed library name from serial-port to CppLinuxSerial.
  16 +- Updated Doxygen comments.
15 17  
16 18 ## [v1.0.1] - 2014-05-21
17 19  
... ...
include/CppLinuxSerial/Exception.hpp
  1 +///
  2 +/// \file Exception.hpp
  3 +/// \author Geoffrey Hunter (www.mbedded.ninja) <gbmhunter@gmail.com>
  4 +/// \edited n/a
  5 +/// \created 2017-11-09
  6 +/// \last-modified 2017-11-27
  7 +/// \brief Contains the Exception class. File originally from https://github.com/mbedded-ninja/CppUtils.
  8 +/// \details
  9 +/// See README.md in root dir for more info.
  10 +
  11 +#ifndef MN_CPP_LINUX_SERIAL_EXCEPTION_H_
  12 +#define MN_CPP_LINUX_SERIAL_EXCEPTION_H_
  13 +
  14 +// System includes
  15 +#include <iostream>
  16 +#include <sstream>
  17 +#include <stdexcept>
  18 +#include <string>
  19 +
  20 +namespace mn {
  21 + namespace CppLinuxSerial {
  22 +
  23 + class Exception : public std::runtime_error {
  24 +
  25 + public:
  26 + Exception(const char *file, int line, const std::string &arg) :
  27 + std::runtime_error(arg) {
  28 + msg_ = std::string(file) + ":" + std::to_string(line) + ": " + arg;
  29 + }
  30 +
  31 + ~Exception() throw() {}
  32 +
  33 + const char *what() const throw() override {
  34 + return msg_.c_str();
  35 + }
  36 +
  37 + private:
  38 + std::string msg_;
  39 + };
  40 +
  41 + } // namespace CppLinuxSerial
  42 +} // namespace mn
  43 +
  44 +#define THROW_EXCEPT(arg) throw Exception(__FILE__, __LINE__, arg);
  45 +
  46 +
  47 +#endif // MN_CPP_LINUX_SERIAL_EXCEPTION_H_
0 48 \ No newline at end of file
... ...
include/CppLinuxSerial/SerialPort.hpp
... ... @@ -32,7 +32,6 @@ namespace mn {
32 32 };
33 33  
34 34 enum class State {
35   - UNCONFIGURED,
36 35 CLOSED,
37 36 OPEN
38 37 };
... ... @@ -41,27 +40,32 @@ namespace mn {
41 40 class SerialPort {
42 41  
43 42 public:
44   - /// \brief Default constructor.
  43 + /// \brief Default constructor. You must specify at least the device before calling Open().
45 44 SerialPort();
46 45  
47   - /// \brief Constructor that sets up serial port with all required parameters.
  46 + /// \brief Constructor that sets up serial port with the basic (required) parameters.
48 47 SerialPort(const std::string &device, BaudRate baudRate);
49 48  
50 49 //! @brief Destructor. Closes serial port if still open.
51 50 virtual ~SerialPort();
52 51  
53   - //! @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 + /// \brief Sets the device to use for serial port communications.
  53 + /// \details Method can be called when serial port is in any state.
54 54 void SetDevice(const std::string &device);
55 55  
56 56 void SetBaudRate(BaudRate baudRate);
57 57  
58   - //! @brief Controls what happens when Read() is called.
59   - //! @param numOfCharToWait Minimum number of characters to wait for before returning. Set to 0 for non-blocking mode.
60   - void SetNumCharsToWait(uint32_t numCharsToWait);
  58 + /// \brief Sets the read timeout (in milliseconds)/blocking mode.
  59 + /// \details Only call when state != OPEN. This method manupulates VMIN and VTIME.
  60 + /// \param timeout_ms Set to -1 to infinite timeout, 0 to return immediately with any data (non
  61 + /// blocking, or >0 to wait for data for a specified number of milliseconds). Timeout will
  62 + /// be rounded to the nearest 100ms (a Linux API restriction). Maximum value limited to
  63 + /// 25500ms (another Linux API restriction).
  64 + void SetTimeout(int32_t timeout_ms);
61 65  
62   - //! @brief Enables/disables echo.
63   - //! param echoOn Pass in true to enable echo, false to disable echo.
64   - void EnableEcho(bool echoOn);
  66 + /// \brief Enables/disables echo.
  67 + /// \param value Pass in true to enable echo, false to disable echo.
  68 + void SetEcho(bool value);
65 69  
66 70 //! @brief Opens the COM port for use.
67 71 //! @throws {std::runtime_error} if filename has not been set.
... ... @@ -69,41 +73,52 @@ namespace mn {
69 73 //! @note Must call this before you can configure the COM port.
70 74 void Open();
71 75  
72   - /// \brief Configures the tty device as a serial port.
73   - /// \warning Device must be open (valid file descriptor) when this is called.
74   - void ConfigureTermios();
75   -
76   - //! @brief Closes the COM port.
  76 + /// \brief Closes the COM port.
77 77 void Close();
78 78  
79   - //! @brief Sends a message over the com port.
80   - //! @param str Reference to an string containing the characters to write to the COM port.
81   - //! @throws {std::runtime_error} if filename has not been set.
82   - //! {std::system_error} if system write() operation fails.
  79 + /// \brief Sends a message over the com port.
  80 + /// \param data The data that will be written to the COM port.
  81 + /// \throws CppLinuxSerial::Exception if state != OPEN.
83 82 void Write(const std::string& data);
84 83  
85   - //! @brief Use to read from the COM port.
86   - //! @param str Reference to a string that the read characters from the COM port will be saved to.
87   - //! @throws {std::runtime_error} if filename has not been set.
88   - //! {std::system_error} if system read() operation fails.
  84 + /// \brief Use to read from the COM port.
  85 + /// \param data The object the read characters from the COM port will be saved to.
  86 + /// \param wait_ms The amount of time to wait for data. Set to 0 for non-blocking mode. Set to -1
  87 + /// to wait indefinitely for new data.
  88 + /// \throws CppLinuxSerial::Exception if state != OPEN.
89 89 void Read(std::string& data);
90 90  
91 91 private:
92 92  
  93 + /// \brief Configures the tty device as a serial port.
  94 + /// \warning Device must be open (valid file descriptor) when this is called.
  95 + void ConfigureTermios();
  96 +
  97 + void SetTermios(termios myTermios);
  98 +
93 99 /// \brief Keeps track of the serial port's state.
94 100 State state_;
95 101  
  102 + /// \brief The file path to the serial port device (e.g. "/dev/ttyUSB0").
96 103 std::string device_;
97 104  
  105 + /// \brief The current baud rate.
98 106 BaudRate baudRate_;
99 107  
100   - //! @brief The file descriptor for the open file. This gets written to when Open() is called.
  108 + /// \brief The file descriptor for the open file. This gets written to when Open() is called.
101 109 int fileDesc_;
102 110  
103   - //! @brief Returns a populated termios structure for the passed in file descriptor.
  111 + bool echo_;
  112 +
  113 + int32_t timeout_ms_;
  114 +
  115 + /// \brief Returns a populated termios structure for the passed in file descriptor.
104 116 termios GetTermios();
105 117  
106   - void SetTermios(termios myTermios);
  118 + static constexpr BaudRate defaultBaudRate_ = BaudRate::B_57600;
  119 + static constexpr int32_t defaultTimeout_ms_ = -1;
  120 +
  121 +
107 122 };
108 123  
109 124 } // namespace CppLinuxSerial
... ...
src/SerialPort.cpp
... ... @@ -2,11 +2,12 @@
2 2 //! @file SerialPort.cpp
3 3 //! @author Geoffrey Hunter <gbmhunter@gmail.com> (www.mbedded.ninja)
4 4 //! @created 2014-01-07
5   -//! @last-modified 2017-11-23
  5 +//! @last-modified 2017-11-27
6 6 //! @brief The main serial port class.
7 7 //! @details
8 8 //! See README.rst in repo root dir for more info.
9 9  
  10 +// System includes
10 11 #include <iostream>
11 12 #include <sstream>
12 13 #include <stdio.h> // Standard input/output definitions
... ... @@ -17,17 +18,23 @@
17 18 #include <termios.h> // POSIX terminal control definitions (struct termios)
18 19 #include <system_error> // For throwing std::system_error
19 20  
  21 +// User includes
  22 +#include "CppLinuxSerial/Exception.hpp"
20 23 #include "CppLinuxSerial/SerialPort.hpp"
21 24  
22 25 namespace mn {
23 26 namespace CppLinuxSerial {
24 27  
25 28 SerialPort::SerialPort() {
  29 + echo_ = false;
  30 + timeout_ms_ = defaultTimeout_ms_;
  31 + baudRate_ = defaultBaudRate_;
26 32 }
27 33  
28   - SerialPort::SerialPort(const std::string& device, BaudRate baudRate) {
  34 + SerialPort::SerialPort(const std::string& device, BaudRate baudRate) :
  35 + SerialPort() {
29 36 device_ = device;
30   - baudRate_ = baudRate;
  37 + baudRate_ = baudRate;
31 38 }
32 39  
33 40 SerialPort::~SerialPort() {
... ... @@ -35,19 +42,22 @@ namespace CppLinuxSerial {
35 42 Close();
36 43 } catch(...) {
37 44 // We can't do anything about this!
  45 + // But we don't want to throw within destructor, so swallow
38 46 }
39 47 }
40 48  
41   - void SerialPort::SetDevice(const std::string& device)
42   - {
  49 + void SerialPort::SetDevice(const std::string& device) {
43 50 device_ = device;
  51 + if(state_ == State::OPEN)
  52 +
  53 +
44 54 ConfigureTermios();
45 55 }
46 56  
47   - void SerialPort::SetBaudRate(BaudRate baudRate)
48   - {
  57 + void SerialPort::SetBaudRate(BaudRate baudRate) {
49 58 baudRate_ = baudRate;
50   - ConfigureTermios();
  59 + if(state_ == State::OPEN)
  60 + ConfigureTermios();
51 61 }
52 62  
53 63 void SerialPort::Open()
... ... @@ -59,7 +69,7 @@ namespace CppLinuxSerial {
59 69 //this->sp->PrintError(SmartPrint::Ss() << "Attempted to open file when file path has not been assigned to.");
60 70 //return false;
61 71  
62   - throw std::runtime_error("Attempted to open file when file path has not been assigned to.");
  72 + THROW_EXCEPT("Attempted to open file when file path has not been assigned to.");
63 73 }
64 74  
65 75 // Attempt to open file
... ... @@ -75,25 +85,18 @@ namespace CppLinuxSerial {
75 85 //this->sp->PrintError(SmartPrint::Ss() << "Unable to open " << this->filePath << " - " << strerror(errno));
76 86 //return false;
77 87  
78   - throw std::runtime_error("Could not open device " + device_ + ". Is the device name correct and do you have read/write permission?");
  88 + THROW_EXCEPT("Could not open device " + device_ + ". Is the device name correct and do you have read/write permission?");
79 89 }
80 90  
81 91 ConfigureTermios();
82 92  
83 93 std::cout << "COM port opened successfully." << std::endl;
84   -
85   - // If code reaches here, open and config must of been successful
86   -
  94 + state_ = State::OPEN;
87 95 }
88 96  
89   - void SerialPort::EnableEcho(bool echoOn)
90   - {
91   - termios settings = this->GetTermios();
92   - settings.c_lflag = echoOn
93   - ? (settings.c_lflag | ECHO )
94   - : (settings.c_lflag & ~(ECHO));
95   - //tcsetattr( STDIN_FILENO, TCSANOW, &settings );
96   - this->SetTermios(settings);
  97 + void SerialPort::SetEcho(bool value) {
  98 + echo_ = value;
  99 + ConfigureTermios();
97 100 }
98 101  
99 102 void SerialPort::ConfigureTermios()
... ... @@ -147,14 +150,29 @@ namespace CppLinuxSerial {
147 150  
148 151 //================= CONTROL CHARACTERS (.c_cc[]) ==================//
149 152  
150   - // c_cc[WMIN] sets the number of characters to block (wait) for when read() is called.
151   - // Set to 0 if you don't want read to block. Only meaningful when port set to non-canonical mode
152   - //tty.c_cc[VMIN] = 1;
153   - SetNumCharsToWait(1);
154   -
155 153 // c_cc[VTIME] sets the inter-character timer, in units of 0.1s.
156 154 // Only meaningful when port is set to non-canonical mode
157   - tty.c_cc[VTIME] = 5; // 0.5 seconds read timeout
  155 + // VMIN = 0, VTIME = 0: No blocking, return immediately with what is available
  156 + // VMIN > 0, VTIME = 0: read() waits for VMIN bytes, could block indefinitely
  157 + // VMIN = 0, VTIME > 0: Block until any amount of data is available, OR timeout occurs
  158 + // VMIN > 0, VTIME > 0: Block until either VMIN characters have been received, or VTIME
  159 + // after first character has elapsed
  160 + // c_cc[WMIN] sets the number of characters to block (wait) for when read() is called.
  161 + // Set to 0 if you don't want read to block. Only meaningful when port set to non-canonical mode
  162 +
  163 + if(timeout_ms_ == -1) {
  164 + // Always wait for at least one byte, this could
  165 + // block indefinitely
  166 + tty.c_cc[VTIME] = 0;
  167 + tty.c_cc[VMIN] = 1;
  168 + } else if(timeout_ms_ == 0) {
  169 + // Setting both to 0 will give a non-blocking read
  170 + tty.c_cc[VTIME] = 0;
  171 + tty.c_cc[VMIN] = 0;
  172 + } else if(timeout_ms_ > 0) {
  173 + tty.c_cc[VTIME] = (cc_t)(timeout_ms_/100); // 0.5 seconds read timeout
  174 + tty.c_cc[VMIN] = 0;
  175 + }
158 176  
159 177  
160 178 //======================== (.c_iflag) ====================//
... ... @@ -162,16 +180,20 @@ namespace CppLinuxSerial {
162 180 tty.c_iflag &= ~(IXON | IXOFF | IXANY); // Turn off s/w flow ctrl
163 181 tty.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL);
164 182  
  183 +
  184 +
165 185 //=========================== LOCAL MODES (c_lflag) =======================//
166 186  
167 187 // Canonical input is when read waits for EOL or EOF characters before returning. In non-canonical mode, the rate at which
168 188 // read() returns is instead controlled by c_cc[VMIN] and c_cc[VTIME]
169 189 tty.c_lflag &= ~ICANON; // Turn off canonical input, which is suitable for pass-through
170   - tty.c_lflag &= ~ECHO; // Turn off echo
  190 + echo_ ? (tty.c_lflag | ECHO ) : (tty.c_lflag & ~(ECHO)); // Configure echo depending on echo_ boolean
171 191 tty.c_lflag &= ~ECHOE; // Turn off echo erase (echo erase only relevant if canonical input is active)
172 192 tty.c_lflag &= ~ECHONL; //
173 193 tty.c_lflag &= ~ISIG; // Disables recognition of INTR (interrupt), QUIT and SUSP (suspend) characters
174 194  
  195 +
  196 +
175 197 // Try and use raw function call
176 198 //cfmakeraw(&tty);
177 199  
... ... @@ -189,38 +211,21 @@ namespace CppLinuxSerial {
189 211 }*/
190 212 }
191 213  
192   - void SerialPort::SetNumCharsToWait(uint32_t numCharsToWait) {
193   - // Get current termios struct
194   - termios myTermios = GetTermios();
195   -
196   - // Save the number of characters to wait for
197   - // to the control register
198   - myTermios.c_cc[VMIN] = numCharsToWait;
199   -
200   - // Save termios back
201   - SetTermios(myTermios);
202   - }
203   -
204 214 void SerialPort::Write(const std::string& data) {
205   - if(fileDesc_ == 0) {
206   - //this->sp->PrintError(SmartPrint::Ss() << );
207   - //return false;
208 215  
209   - throw std::runtime_error("SendMsg called but file descriptor (fileDesc) was 0, indicating file has not been opened.");
  216 + if(state_ != State::OPEN)
  217 + THROW_EXCEPT(std::string() + __PRETTY_FUNCTION__ + " called but state != OPEN. Please call Open() first.");
  218 +
  219 + if(fileDesc_ < 0) {
  220 + THROW_EXCEPT(std::string() + __PRETTY_FUNCTION__ + " called but file descriptor < 0, indicating file has not been opened.");
210 221 }
211 222  
212 223 int writeResult = write(fileDesc_, data.c_str(), data.size());
213 224  
214 225 // Check status
215 226 if (writeResult == -1) {
216   - // Could not open COM port
217   - //this->sp->PrintError(SmartPrint::Ss() << "Unable to write to \"" << this->filePath << "\" - " << strerror(errno));
218   - //return false;
219   -
220 227 throw std::system_error(EFAULT, std::system_category());
221 228 }
222   -
223   - // If code reaches here than write must of been successful
224 229 }
225 230  
226 231 void SerialPort::Read(std::string& data)
... ... @@ -228,7 +233,7 @@ namespace CppLinuxSerial {
228 233 if(fileDesc_ == 0) {
229 234 //this->sp->PrintError(SmartPrint::Ss() << "Read() was called but file descriptor (fileDesc) was 0, indicating file has not been opened.");
230 235 //return false;
231   - throw std::runtime_error("Read() was called but file descriptor (fileDesc) was 0, indicating file has not been opened.");
  236 + THROW_EXCEPT("Read() was called but file descriptor (fileDesc) was 0, indicating file has not been opened.");
232 237 }
233 238  
234 239 // Allocate memory for read buffer
... ... @@ -236,7 +241,7 @@ namespace CppLinuxSerial {
236 241 memset (&buf, '\0', sizeof buf);
237 242  
238 243 // Read from file
239   - int n = read(fileDesc_, &buf, sizeof(buf));
  244 + ssize_t n = read(fileDesc_, &buf, sizeof(buf));
240 245  
241 246 // Error Handling
242 247 if(n < 0) {
... ... @@ -256,8 +261,7 @@ namespace CppLinuxSerial {
256 261 // If code reaches here, read must of been successful
257 262 }
258 263  
259   - termios SerialPort::GetTermios()
260   - {
  264 + termios SerialPort::GetTermios() {
261 265 if(fileDesc_ == -1)
262 266 throw std::runtime_error("GetTermios() called but file descriptor was not valid.");
263 267  
... ... @@ -279,9 +283,9 @@ namespace CppLinuxSerial {
279 283 void SerialPort::SetTermios(termios myTermios)
280 284 {
281 285 // Flush port, then apply attributes
282   - tcflush(this->fileDesc_, TCIFLUSH);
  286 + tcflush(fileDesc_, TCIFLUSH);
283 287  
284   - if(tcsetattr(this->fileDesc_, TCSANOW, &myTermios) != 0)
  288 + if(tcsetattr(fileDesc_, TCSANOW, &myTermios) != 0)
285 289 {
286 290 // Error occurred
287 291 std::cout << "Could not apply terminal attributes for \"" << device_ << "\" - " << strerror(errno) << std::endl;
... ... @@ -296,7 +300,7 @@ namespace CppLinuxSerial {
296 300 if(fileDesc_ != -1) {
297 301 auto retVal = close(fileDesc_);
298 302 if(retVal != 0)
299   - throw std::runtime_error("Tried to close serial port " + device_ + ", but close() failed.");
  303 + THROW_EXCEPT("Tried to close serial port " + device_ + ", but close() failed.");
300 304  
301 305 fileDesc_ = -1;
302 306 }
... ... @@ -304,5 +308,15 @@ namespace CppLinuxSerial {
304 308 state_ = State::CLOSED;
305 309 }
306 310  
  311 + void SerialPort::SetTimeout(int32_t timeout_ms) {
  312 + if(timeout_ms < -1)
  313 + THROW_EXCEPT(std::string() + "timeout_ms provided to " + __PRETTY_FUNCTION__ + " was < -1, which is invalid.");
  314 + if(timeout_ms > 25500)
  315 + THROW_EXCEPT(std::string() + "timeout_ms provided to " + __PRETTY_FUNCTION__ + " was > 25500, which is invalid.");
  316 + if(state_ == State::OPEN)
  317 + THROW_EXCEPT(std::string() + __PRETTY_FUNCTION__ + " called while state == OPEN.");
  318 + timeout_ms_ = timeout_ms;
  319 + }
  320 +
307 321 } // namespace CppLinuxSerial
308 322 } // namespace mn
... ...
test/unit/ConfigTests.cpp
... ... @@ -43,6 +43,10 @@ namespace {
43 43 EXPECT_NE(std::string::npos, sttyOutput_.find("speed 115200 baud"));
44 44 }
45 45  
  46 + //================================================================================================//
  47 + //======================================= LOCAL MODES (c_lflag) ==================================//
  48 + //================================================================================================//
  49 +
46 50 TEST_F(ConfigTests, CanonicalModeOff) {
47 51 EXPECT_NE(std::string::npos, sttyOutput_.find("-icanon"));
48 52 }
... ... @@ -53,4 +57,8 @@ namespace {
53 57 EXPECT_NE(std::string::npos, sttyOutput_.find("-echonl"));
54 58 }
55 59  
  60 + TEST_F(ConfigTests, InterruptQuitSuspCharsOff) {
  61 + EXPECT_NE(std::string::npos, sttyOutput_.find("-isig"));
  62 + }
  63 +
56 64 } // namespace
57 65 \ No newline at end of file
... ...