Multi-Platform Serial Interfacing using Boost: A GPS Sensor and OpenDDS Part I

Multi-Platform Serial Interfacing using Boost: A GPS Sensor and OpenDDS - Part I

By Charles Calkins, OCI Principal Software Engineer 

March 2013


Introduction

Since the early days of the IBM PC, serial ports supporting the RS-232 standard [1] have been used to connect external devices to computers. While physical serial ports may not be as commonplace on new hardware as they once were, many devices still provide serial connectivity by creating virtual serial ports in the operating system that are associated with USB interfaces.

External modems may be the most familiar serial device, but many devices communicate serially, such as bar code scanners, printers, LCD panels and the like [2] that are commonly used in the commercial world. Communicating with these devices is a necessity, and, as the machines that they connect to range in architecture, it is important to write code that is portable and can be used in different environments.

In this two-part article series, we will use a GPS receiver that supports the NMEA 0183 data specification [3] as an example of a serial device with which to communicate. In part I, we will show how to send and receive data over serial ports using the Boost C++ libraries in order to obtain sensor data. In part II, we will illustrate how to decode that data and publish it with OpenDDS [4].

The source code of the applications presented here is available in the code archive that accompanies this article. This code has been tested under 64-bit Windows 7 with Visual Studio 2010 and Boost 1.51, as well as under 64-bit Ubuntu 12.10 with gcc 4.7.2 (with the --std=c++11 option) and Boost 1.49. It uses MPC [5] for its build system.

Boost Asio

As serial interfaces are available on a large number of different computer architectures, it is useful to write code to read from and write to serial ports in a platform-independent way. For this, we will use the Boost.Asio library [6].

Boost [7] is a set of libraries written in C++ that works with many modern C++ compilers. Members of the C++ Standards Committee participate in Boost development [8], so it is common for features that are part of Boost to later become part of the C++ Standard Library.

As of this writing, the current version of Boost, 1.53.0, contains about 120 different libraries, covering a wide variety of features. We will use several of these libraries, and begin with Asio.

For more information on Asio, please see [9] and [10], as well as OCI's Boost training course [11]. In brief, Asio provides synchronous and asynchronous function execution. Functions are posted to a boost::asio::io_service object that implements the Proactor pattern [12][13] for asynchronous event dispatch. Asio allows user-defined functions to be posted, and provides its own functions for timers, signal handling, and for performing synchronous and asynchronous I/O to resources such as devices, files, and network ports. As timers expire, signals are raised, data is available for reading, or worker threads become idle, posted functions are serviced, and user-provided callback functions are invoked with the result of the function execution. A worker thread is a user-created thread that calls the run() method of the boost::asio::io_service object. This provides a very scalable system based on hardware resources — the more threads that call the run() method, the more threads are available to service functions that have been posted. If the number of threads is created based on automatic determination of hardware resources, the application will automatically scale, even without recompilation, as improved hardware is used.

We will encapsulate this functionality in the Executor class, in the library ASIOLib. The file Executor.h begins as follows:

  1. // ASIOLib/Executor.h
  2. #include <boost/asio.hpp>
  3. #include <boost/function.hpp>
  4. #include "ASIOLib_Export.h"

The header file for Asio is included, as well as for Function[14]. The Boost.Function library contains function object wrappers which we will use to store callback functions that will later be invoked.


SIDEBAR

Some libraries used in this article, such as Function, are now included as part of the C++ Standard Library. The equivalent Boost libraries are used in this article to illustrate the functionality that is available in Boost, as well as to aid in supporting older compilers.


ASIOLib_Export.h contains various symbols to assist with the exporting of symbols from shared libraries, and while the macros that it contains can be created by hand, this file was generated by the script:

generate_export_file.pl ASIOLib

The generate_export_file.pl script is part of the ACE and TAO distribution that is needed to build OpenDDS.

We define the class by specifying one of the generated export macros, and by inheriting from boost::noncopyable.

  1. namespace ASIOLib {
  2. class ASIOLib_Export Executor : private boost::noncopyable {

For ASIOLib_Export to represent an export operation, the ASIOLIB_BUILD_DLL symbol is defined in the build parameters for the ASIOLib project. When not defined, as with code that uses ASIOLibASIOLib_Export represents a library import.

boost::noncopyable provides private copy and assignment operators to ensure that this class, and anything derived from it, cannot be copied. As this class represents a thread pool, the copy operation is not appropriate.

Next, we provide the boost::asio::io_service object, and a method that will be called by Asio for each instance of run() that is invoked.

  1. protected:
  2. boost::asio::io_service _ioService;
  3. void WorkerThread(boost::asio::io_service &io_service);

Callback functions are provided which will be invoked at interesting points of the execution.

  1. public:
  2. boost::function<void (boost::asio::io_service &)>
  3. OnWorkerThreadStart;
  4. boost::function<void (boost::asio::io_service &)>
  5. OnWorkerThreadStop;
  6. boost::function<void (boost::asio::io_service &,
  7. boost::system::error_code)> OnWorkerThreadError;
  8. boost::function<void (boost::asio::io_service &,
  9. const std::exception &)> OnWorkerThreadException;
  10. boost::function<void (boost::asio::io_service &)> OnRun;

Finally, AddCtrlCHandling() is used to intercept the pressing of Ctrl-C, Run() to start the Executor, and an accessor is provided for the io_service object.

  1. boost::asio::io_service &GetIOService() { return _ioService; }
  2. void AddCtrlCHandling();
  3. void Run(unsigned int numThreads = -1);
  4. };
  5. }

The implementation of Executor is as follows. The headers included are the one above, and for the Boost.Thread library [15]. Boost.Thread provides portable threads and synchronization objects.

  1. // ASIOLib/Executor.cpp
  2. #include "Executor.h"
  3. #include <boost/thread.hpp>

The Run() method of Executor establishes a thread pool to call the run() method of io_service via the Executor::WorkerThread() method.

  1. void ASIOLib::Executor::Run(unsigned int numThreads) {
  2. if (OnRun)
  3. OnRun(_ioService);
  4.  
  5. boost::thread_group workerThreads;
  6. for (unsigned int i = 0;
  7. i < ((numThreads == (unsigned int)-1) ?
  8. (boost::thread::hardware_concurrency()) :
  9. numThreads);
  10. ++i)
  11. workerThreads.create_thread(boost::bind(
  12. &Executor::WorkerThread, this, boost::ref(_ioService)));
  13. workerThreads.join_all();
  14. }

Before the thread pool is created, the OnRun() callback function is invoked if it is supplied. The user of Executor can use this function to provide io_service initialization, such as to queue work to perform (functions to execute).

The user specifies the number of threads to create, but if the default of -1 is used, the number of threads is set to the number of physical execution units (typically the number of CPUs or cores) as obtained by boost::thread::hardware_concurrency(). boost::bind(), from the Boost.Bind library [16], is used to associate arguments with functions. Here, the function to call, WorkerThread() is a class member, so a pointer to an object (this, in our case) must be supplied. The remaining arguments to boost::bind() are arguments to pass to the function, which, in this case, is just the io_service object passed by reference.

After all, threads are created, Run() blocks, waiting for them to terminate, via the call tojoin_all() on the thread group.

The WorkerThread() method wraps the call to io_service.run().

  1. void ASIOLib::Executor::WorkerThread(boost::asio::io_service &ios) {
  2. if (OnWorkerThreadStart)
  3. OnWorkerThreadStart(ios);
  4.  
  5. while (true) {
  6. try
  7. {
  8. boost::system::error_code ec;
  9. ios.run(ec);
  10. if (ec && OnWorkerThreadError)
  11. OnWorkerThreadError(ios, ec);
  12. break;
  13. }
  14. catch(const std::exception &ex) {
  15. if (OnWorkerThreadException)
  16. OnWorkerThreadException(ios, ex);
  17. }
  18. }
  19.  
  20. if (OnWorkerThreadStop)
  21. OnWorkerThreadStop(ios);
  22. }

As with OnRun(), user-specified callbacks are invoked when WorkerThread() starts and stops, as well as when errors are trapped. Passing an boost::system::error_code object to run() causes Asio to return its own errors via the code, but work that is serviced through Asio may throw its own exceptions, so a try/catch block is necessary. Also, on error or exception, run() will exit, so must be invoked again to resume processing, otherwise the WokerThread() method will return and the worker thread will terminate. Here, run() is called again if an exception is raised by a work item so processing can continue for additional work items, but is not called again when an Asio error is generated. This also allows the loop to exit when run() is terminated normally by a call to boost::asio::io_service::stop().

The AddCtrlCHandling() method provides a mechanism for calling boost::asio::io_service::stop(). When the process receives a SIGTERM or SIGINT signal (such as when Ctrl-C is pressed), the boost::asio::signal_set will be triggered, causing its callback function to be executed. The callback function that will be executed is boost::asio::io_service::stop() itself, providing clean shutdown of Asio.

  1. void ASIOLib::Executor::AddCtrlCHandling() {
  2. boost::asio::signal_set sig_set(_ioService, SIGTERM, SIGINT);
  3. sig_set.async_wait(boost::bind(
  4. &boost::asio::io_service::stop, boost::ref(_ioService)));
  5. }

The Serial Port

With the Executor complete, we will now define interaction with a serial port. Asio provides boost::asio::serial_port which we will incorporate in our own SerialPort class. As before, a number of headers are included.

  1. // ASIOLib/SerialPort.h
  2. #include <boost/asio.hpp>
  3. #include <boost/tuple/tuple.hpp>
  4. #include <boost/thread/mutex.hpp>
  5. #include <boost/enable_shared_from_this.hpp>
  6. #include <boost/function.hpp>
  7. #include "ASIOLib_Export.h"

The Boost.Tuple library [17] provides an easy way to create of a fixed-size group of disparate elements. The Boost Smart Pointer library [18] provides reference-counted pointers that delete the pointed-to object when the last smart pointer that referred to it is destroyed. enable_shared_from_this is part of the Smart Pointer library which provides the ability to obtain a shared pointer to the current object from within a member function.

The configuration of a serial port includes attributes such as character size (typically 7 or 8 bits), parity (odd, even or no parity bit used), and the number of stop bits. For convenience, we define common attribute sets as Boost tuples, and create them with boost::make_tuple().

  1. namespace ASIOLib {
  2. typedef boost::tuple<
  3. boost::asio::serial_port_base::character_size,
  4. boost::asio::serial_port_base::parity,
  5. boost::asio::serial_port_base::stop_bits> SerialParams;
  6.  
  7. static const SerialParams SP_8N1 = boost::make_tuple(
  8. 8,
  9. boost::asio::serial_port_base::parity::none,
  10. boost::asio::serial_port_base::stop_bits::one);
  11.  
  12. static const SerialParams SP_7E1 = boost::make_tuple(
  13. 7,
  14. boost::asio::serial_port_base::parity::even,
  15. boost::asio::serial_port_base::stop_bits::one);

The SerialPort class is noncopyable, and supports enable_shared_from_this. Two buffers are maintained: a write buffer for data being written to the serial port, and a read buffer for data being read from it. These are each protected by a boost::mutex, which is part of the Boost.Thread library.

  1. class ASIOLib_Export SerialPort : private boost::noncopyable,
  2. public boost::enable_shared_from_this<SerialPort> {
  3. boost::asio::serial_port _serialPort;
  4. bool _isOpen;
  5. boost::system::error_code _errorCode;
  6. boost::mutex _errorCodeMutex;
  7. std::vector<unsigned char> _readBuffer;
  8. boost::function<void (boost::asio::io_service &,
  9. const std::vector<unsigned char> &, size_t)> _onRead;
  10.  
  11. std::vector<unsigned char> _writeQueue, _writeBuffer;
  12. boost::mutex _writeQueueMutex, _writeBufferMutex;

We will implement asynchronous I/O to the serial port. The ReadBegin() method starts an asynchronous read, and, when data is available, ReadComplete() will be called. Writing is analagous.

  1. boost::system::error_code Flush();
  2. void SetErrorCode(const boost::system::error_code &ec);
  3. void ReadBegin();
  4. void ReadComplete(const boost::system::error_code &ec,
  5. size_t bytesTransferred);
  6. void WriteBegin();
  7. void WriteComplete(const boost::system::error_code &ec);

The public methods of SerialPort provide for its construction, opening and closing, and reading and writing of data.

  1. public:
  2. SerialPort(boost::asio::io_service &ioService,
  3. const std::string &portName);
  4. ~SerialPort();
  5. void Open(
  6. const boost::function<void (boost::asio::io_service &,
  7. const std::vector<unsigned char> &, size_t)> &onRead,
  8. unsigned int baudRate,
  9. SerialParams serialParams = SP_8N1,
  10. boost::asio::serial_port_base::flow_control flowControl =
  11. boost::asio::serial_port_base::flow_control(
  12. boost::asio::serial_port_base::flow_control::none)
  13. );
  14. void Close();
  15. void Write(const unsigned char *buffer, size_t bufferLength);
  16. void Write(const std::vector<unsigned char> &buffer);
  17. void Write(const std::string &buffer);
  18. };
  19. };

The implementation of the above class begins with the Flush() method. It can be useful to clear all characters pending on the serial port, especially when communication is first begun or upon an error condition. This does not appear to be provided by Asio, but can be implemented, as was inspired by this [19]. Under Windows, the Win32 PurgeComm() function is invoked, but otherwise, tcflush() is called to perform an analogous operation under Linux. Upon error, a boost::system::error_code is returned which wraps the system error that was returned. The number of compilation macros needed for various platforms and the alternative code paths code shows why one should rely on a library such as Boost rather than writing multi-platform code by hand.

  1. // ASIOLib/SerialPort.cpp
  2. #include "SerialPort.h"
  3. #include <boost/bind.hpp>
  4.  
  5. boost::system::error_code ASIOLib::SerialPort::Flush() {
  6. boost::system::error_code ec;
  7. #if !defined(BOOST_WINDOWS) && !defined(__CYGWIN__)
  8. const bool isFlushed =! ::tcflush(_serialPort.native(), TCIOFLUSH);
  9. if (!isFlushed)
  10. ec = boost::system::error_code(errno,
  11. boost::asio::error::get_system_category());
  12. #else
  13. const bool isFlushed = ::PurgeComm(_serialPort.native(),
  14. PURGE_RXABORT | PURGE_RXCLEAR | PURGE_TXABORT | PURGE_TXCLEAR);
  15. if (!isFlushed)
  16. ec = boost::system::error_code(::GetLastError(),
  17. boost::asio::error::get_system_category());
  18. #endif
  19. return ec;
  20. }

The method SetErrorCode() uses a mutex to protect the assignment of the boost::system::error_code, as it is a composite object, and SetErrorCode() can be called from multiple threads.

  1. void ASIOLib::SerialPort::SetErrorCode(
  2. const boost::system::error_code &ec) {
  3. if (ec) {
  4. boost::mutex::scoped_lock lock(_errorCodeMutex);
  5. _errorCode = ec;
  6. }
  7. }

ReadBegin() invokes the Asio async_read_some() method, passing a buffer to fill with incoming bytes from the port (the buffer is allocated in the constructor — Asio requires the buffer to have a non-zero length in order to be used), and a callback function, ReadComplete(), to invoke when data has been read. The ReadComplete method takes two parameters: a boost::system::error_code and the number of bytes that were copied into the buffer. We pass a pointer to the current object via a shared pointer (as is needed because ReadComplete() is a class member function) as created by shared_from_this() to ensure that the current object still exists at the time that the callback function will be invoked on it.

  1. void ASIOLib::SerialPort::ReadBegin() {
  2. _serialPort.async_read_some(boost::asio::buffer(_readBuffer),
  3. boost::bind(&SerialPort::ReadComplete, shared_from_this(),
  4. boost::asio::placeholders::error,
  5. boost::asio::placeholders::bytes_transferred));
  6. }

When data has arrived on the serial port, ReadComplete() is called. If no error was generated, and the user-provided OnRead() callback was provided, the callback is invoked with the data that was received. It is presumed that the implementation of OnRead() will copy or process the buffer data before it returns, as the next asynchronous read, begun by the call to ReadBegin() following the callback, will overwrite the buffer data. The application will invoke ReadBegin() only once, and as it is the same thread that begins the next read via the call to ReadBegin() here, it is ensured that only one thread could be accessing the read buffer, so the buffer does not require synchronization.

  1. void ASIOLib::SerialPort::ReadComplete(
  2. const boost::system::error_code &ec, size_t bytesTransferred) {
  3. if (!ec) {
  4. if (_onRead && (bytesTransferred > 0))
  5. _onRead(boost::ref(_serialPort.get_io_service()),
  6. boost::cref(_readBuffer), bytesTransferred);
  7. ReadBegin(); // queue another read
  8. } else {
  9. Close();
  10. SetErrorCode(ec);
  11. }
  12. }

On the other hand, data can be written from multiple threads, so a synchronized, two-buffer approach is used. The write buffer contains the data that is currently being written to the serial port. A serial port is not a shareable device — if a write is in progress, another cannot be started until the first has completed [20]. The write queue contains pending data that will be written. Data is appended to the write queue, and then moved to the write buffer, when the serial port is available.

The public method Write() obtains a lock on the write queue, copies its data into it, and then invokes WriteBegin() asynchronously by posting it to the io_service object to perform the write. The caller of Write() can then continue processing and not block while the write is in progress.

  1. void ASIOLib::SerialPort::Write(const unsigned char *buffer,
  2. size_t bufferLength) {
  3. {
  4. boost::mutex::scoped_lock lock(_writeQueueMutex);
  5. _writeQueue.insert(_writeQueue.end(), buffer,
  6. buffer+bufferLength);
  7. }
  8. _serialPort.get_io_service().post(boost::bind(
  9. &SerialPort::WriteBegin, shared_from_this()));
  10. }

The WriteBegin() method first checks to see if a write is currently in progress, or if no pending bytes remain to be written. In either case, the method exits. Otherwise, the bytes in the write queue are copied into the write buffer, the write queue is emptied, and boost::asio::async_write() is called to write the contents of the write buffer to the serial port. When the write completes, WriteComplete() is invoked with the result of the write.

  1. void ASIOLib::SerialPort::WriteBegin() {
  2. boost::mutex::scoped_lock writeBufferlock(_writeBufferMutex);
  3. if (_writeBuffer.size() != 0)
  4. return; // a write is in progress, so don't start another
  5.  
  6. boost::mutex::scoped_lock writeQueuelock(_writeQueueMutex);
  7. if (_writeQueue.size() == 0)
  8. return; // nothing to write
  9.  
  10. // allocate a larger buffer if needed
  11. const std::vector<unsigned char>::size_type writeQueueSize =
  12. _writeQueue.size();
  13. if (writeQueueSize > _writeBuffer.size())
  14. _writeBuffer.resize(writeQueueSize);
  15.  
  16. // copy the queued bytes to the write buffer,
  17. // and clear the queued bytes
  18. std::copy(_writeQueue.begin(), _writeQueue.end(),
  19. _writeBuffer.begin());
  20. _writeQueue.clear();
  21.  
  22. boost::asio::async_write(_serialPort,
  23. boost::asio::buffer(_writeBuffer, writeQueueSize),
  24. boost::bind(&SerialPort::WriteComplete, shared_from_this(),
  25. boost::asio::placeholders::error));
  26. }

If the write completed without error, then the write buffer is cleared. As shown above, WriteBegin() uses a non-zero size of the write buffer to indicate that a write is still in progress. WriteBegin() is then called in case any bytes had been added to the write queue while the previous write was in progress, so they, too, can be written to the serial port.

  1. void ASIOLib::SerialPort::WriteComplete(
  2. const boost::system::error_code &ec) {
  3. if (!ec) {
  4. {
  5. boost::mutex::scoped_lock lock(_writeBufferMutex);
  6. _writeBuffer.clear();
  7. }
  8. // more bytes to send may have arrived while the write
  9. // was in progress, so check again
  10. WriteBegin();
  11. } else {
  12. Close();
  13. SetErrorCode(ec);
  14. }
  15. }

The constructor of SerialPort constructs the boost::asio::serial_port, plus allocates the read buffer. The name of the port is provided in a system-dependent syntax. For example, under Windows, \\.\COM1 would be appropriate, but under Linux, /dev/ttyS0 could be used.

  1. ASIOLib::SerialPort::SerialPort(boost::asio::io_service &ioService,
  2. const std::string &portName) :
  3. _serialPort(ioService, portName), _isOpen(false) {
  4. _readBuffer.resize(128);
  5. }

Opening a serial port consists of setting parameters such as baud rate, flow control, parity, and the like. The port is then flushed so any current characters are discarded, and, if the user provided an OnRead() callback, an asynchronous read is started. If no OnRead() callback is supplied, the serial port can still be used for writing.

  1. void ASIOLib::SerialPort::Open(
  2. const boost::function<void (boost::asio::io_service &,
  3. const std::vector<unsigned char> &, size_t)> &onRead,
  4. unsigned int baudRate,
  5. ASIOLib::SerialParams serialParams,
  6. boost::asio::serial_port_base::flow_control flowControl) {
  7. _onRead = onRead;
  8. _serialPort.set_option(
  9. boost::asio::serial_port_base::baud_rate(baudRate));
  10. _serialPort.set_option(serialParams.get<0>());
  11. _serialPort.set_option(serialParams.get<1>());
  12. _serialPort.set_option(serialParams.get<2>());
  13. _serialPort.set_option(flowControl);
  14.  
  15. const boost::system::error_code ec = Flush();
  16. if (ec)
  17. SetErrorCode(ec);
  18.  
  19. _isOpen = true;
  20.  
  21. if (_onRead)
  22. _serialPort.get_io_service().post(boost::bind(
  23. &SerialPort::ReadBegin, shared_from_this()));
  24. }

The destructor simply closes the serial port.

  1. ASIOLib::SerialPort::~SerialPort() {
  2. Close();
  3. }

To close a port, outstanding requests are cancelled first, and then the port itself is closed.

  1. void ASIOLib::SerialPort::Close() {
  2. if (_isOpen) {
  3. _isOpen = false;
  4. boost::system::error_code ec;
  5. _serialPort.cancel(ec);
  6. SetErrorCode(ec);
  7. _serialPort.close(ec);
  8. SetErrorCode(ec);
  9. }
  10. }

Finally, two other variants of the Write() method are given, to provide additional calling options to the user.

  1. void ASIOLib::SerialPort::Write(
  2. const std::vector<unsigned char> &buffer) {
  3. Write(&buffer[0], buffer.size());
  4. }
  5.  
  6. void ASIOLib::SerialPort::Write(const std::string &buffer) {
  7. Write(reinterpret_cast<const unsigned char *>(buffer.c_str()),
  8. buffer.size());
  9. }

This completes ASIOLib, and we can now use it to read data from a serial port.

Reading from a GPS Receiver

We can now use ASIOLib to implement an application that reads data from the serial port, displays it to the user, and optionally saves it to a file for later playback. A Pharos GPS-360 receiver will be used to generate serial data.

The headers included in SerialReader show additional Boost libraries that will be used.

  1. // SerialReader.cpp
  2. #include "../ASIOLib/Executor.h"
  3. #include "../ASIOLib/SerialPort.h"
  4. #include <boost/thread.hpp>
  5. #include <boost/date_time.hpp>
  6. #include <boost/program_options.hpp>
  7. #include <boost/archive/text_oarchive.hpp>
  8. #include <boost/serialization/vector.hpp>

These Boost libraries include Date_Time [21] which provides time handling and calendar functionality, Program_options [22] to parse command-line options, and Serialization [23] to write data to portable archive files.

A logging helper is handy to display messages from threads, to ensure the output is not interleaved on the console.

  1. boost::mutex cout_lock;
  2. void Log(const std::string &msg) {
  3. boost::mutex::scoped_lock lock(cout_lock);
  4. std::cout << "[" << boost::this_thread::get_id() << "] " <<
  5. msg << std::endl;
  6. }

The main() function uses Program_options to parse the command-line options. The available options are specified in a boost::program_options::options_description object. Each option has both a long name, e.g., help, as well as a single letter alias, e.g., h, and a text description of the purpose of the option. Each option definition that requires an argument has an additional parameter, boost::program_options::value<>, to indicate the data type of the option, as well as additional flags, such as if the option must be provided.

  1. int main(int argc, char *argv[]) {
  2. std::string portName, file;
  3. int baudRate;
  4. boost::program_options::options_description desc("Options");
  5. desc.add_options()
  6. ("help,h", "help")
  7. ("port,p", boost::program_options::value<std::string>(
  8. &portName)->required(), "port name (required)")
  9. ("baud,b", boost::program_options::value<int>(
  10. &baudRate)->required(), "baud rate (required)")
  11. ("file,f", boost::program_options::value<std::string>(
  12. &file), "file to save to")
  13. ;

The parse_command_line() function parses the command-line arguments and populates a map which can later be queried.

  1. boost::program_options::variables_map vm;
  2. boost::program_options::store(
  3. boost::program_options::parse_command_line(
  4. argc, argv, desc), vm);

The first option that is processed is help. If either helph or no arguments are provided, a list of available options is displayed on the console, and the program exits.

  1. if (vm.empty() || vm.count("help")) {
  2. std::cout << desc << "\n";
  3. return -1;
  4. }

In order to detect errors, such as missing required parameters, boost::program_options::notify() is called. This must be called after processing the helpoption, however, as it will generate missing required parameter errors even when they aren't required by the context [24] — if the user asks for help, they likely do not know what options are available to supply, so a help message should be displayed and required options not enforced.

  1. boost::program_options::notify(vm);

By associating variables in the option definition, they are automatically populated by the argument processing. If the file argument was specified, we create a file stream, and, using it, a Boost Serialization archive, to store the data read from the serial port. These are created as Boost shared pointers as they will not exist when the file option is not specified.

  1. const boost::scoped_ptr<std::ostream> out(
  2. file.empty() ? 0 : new std::ofstream(file.c_str()));
  3. const boost::scoped_ptr<boost::archive::text_oarchive> archive(
  4. file.empty() ? 0 : new boost::archive::text_oarchive(*out));

We now create an instance of ASIOLib::Executor, and supply lambda functions to handle error callbacks. The Boost.Lexical_Cast [25] library provides data type conversion to and from strings. We are using it here to convert the Boost error code object to a string that can be appended to the log message.

We create a SerialReader object as a shared pointer to ensure that it exists for the life of the Asio execution, and to support the use of shared_from_this() — if the object is not contained within a Boost shared pointer, there isn't anything for shared_from_this() to reference. We associate its Create() method with the OnRun() method of the Executor, and then call Run() to start the system.

  1. ASIOLib::Executor e;
  2. e.OnWorkerThreadError = [](boost::asio::io_service &,
  3. boost::system::error_code ec) {
  4. Log(std::string("SerialReader error (asio): ") +
  5. boost::lexical_cast<std::string>(ec));
  6. };
  7. e.OnWorkerThreadException = [](boost::asio::io_service &,
  8. const std::exception &ex) {
  9. Log(std::string("SerialReader exception (asio): ") +
  10. ex.what());
  11. };
  12.  
  13. const boost::shared_ptr<SerialReader> sp(new SerialReader(
  14. portName, baudRate, archive));
  15. e.OnRun = boost::bind(&SerialReader::Create, sp, _1);
  16. e.Run();
  17.  
  18. return 0;
  19. }

The real work of the SerialReader application is performed by the class SerialReader. The constructor stores the port name and baud rate, as well as a pointer to the archive to write.

  1. class SerialReader : private boost::noncopyable,
  2. public boost::enable_shared_from_this<SerialReader> {
  3. boost::shared_ptr<ASIOLib::SerialPort> _serialPort;
  4. std::string _portName;
  5. unsigned int _baudRate;
  6. const boost::scoped_ptr<boost::archive::text_oarchive> &_oa;
  7. boost::posix_time::ptime _lastRead;
  8.  
  9. void OnRead(boost::asio::io_service &ios,
  10. const std::vector<unsigned char> &buffer,
  11. size_t bytesRead);
  12.  
  13. public:
  14. SerialReader(const std::string &portName, int baudRate,
  15. const boost::scoped_ptr<boost::archive::text_oarchive> &oa) :
  16. _portName(portName), _baudRate(baudRate), _oa(oa) {}

The Create() method creates a new SerialPort object, and opens it. The OnRead() method of SerialReader is used for the OnRead() callback of the SerialPort object.

  1. void Create(boost::asio::io_service &ios) {
  2. try {
  3. _serialPort.reset(new ASIOLib::SerialPort(ios, _portName));
  4. _serialPort->Open(boost::bind(&SerialReader::OnRead,
  5. shared_from_this(), _1, _2, _3), _baudRate);
  6. } catch (const std::exception &e) {
  7. std::cout << e.what() << std::endl;
  8. }
  9. }
  10. };

When OnRead() is called with data that has been read from the port, the current time is obtained by using the Boost.Date_Time library. The _lastRead variable stores the last time data was read from the port, but is automatically set to the special value boost::posix_time::not_a_date_time if it has not yet been initialized. If this is the case, the current time is set as the last read time.

  1. void SerialReader::OnRead(boost::asio::io_service &,
  2. const std::vector<unsigned char> &buffer, size_t bytesRead) {
  3. const boost::posix_time::ptime now =
  4. boost::posix_time::microsec_clock::universal_time();
  5.  
  6. if (_lastRead == boost::posix_time::not_a_date_time)
  7. _lastRead = now;

Next, a local copy of the data read is created. As a fixed buffer size is used in SerialPort, the number of bytes read may be less than the total buffer size, so only the bytes that are read are copied.

  1. const std::vector<unsigned char> v(buffer.begin(),
  2. buffer.begin()+bytesRead);

If an archive was provided, the time between the current and previous reads is written to it, followed by the data. The Serialization library allows the standard insertion and extraction operators to be used with archives in the same manner as they are used with streams.

  1. if (_oa) {
  2. const uint64_t offset = (now-_lastRead).total_milliseconds();
  3. *_oa << offset << v;
  4. }
  5. _lastRead = now;

Lastly, the characters that were read from the port are displayed on the screen. In the general case, this is not feasible, as the data from a serial port is not guaranteed to be ASCII, but it works in our context, as the NMEA 0183 data specification guarantees ASCII strings will be sent from the GPS device.

  1. std::copy(v.begin(), v.end(),
  2. std::ostream_iterator<unsigned char>(std::cout, ""));
  3. }

Under Windows, SerialReader can be run as follows:

  1. > SerialReader -p \\.\COM16 -b 4800

and under Linux as:

  1. $ ./SerialReader -p /dev/ttyUSB0 -b 4800

as the GPS receiver is connected via USB, rather than to a physical serial port.

When running, text such as the following is displayed on the console:

...
$GPGGA,191644.608,3848.3643,N,09018.2853,W,1,05,2.8,128.2,M,-33.7,M,0.0,0000*44
$GPGLL,3848.3643,N,09018.2853,W,191644.608,A*2E
$GPGSA,A,3,18,15,21,06,09,,,,,,,,3.7,2.8,2.3*3C
$GPRMC,191644.608,A,3848.3643,N,09018.2853,W,34.909700,55.51,150113,,*1A
$GPGGA,191645.608,3848.3698,N,09018.2749,W,1,06,1.3,127.9,M,-33.7,M,0.0,0000*48
$GPGLL,3848.3698,N,09018.2749,W,191645.608,A*2D
$GPGSA,A,3,18,15,29,21,06,09,,,,,,,2.2,1.3,1.8*33
$GPGSV,3,1,10,18,62,311,37,15,47,49,40,14,17,219,26,29,11,186,30*44
$GPGSV,3,2,10,22,30,282,0,21,85,221,35,24,34,119,23,6,24,295,33*7C
...

The file Tests/gps_2013-01-15_0106, included in the code archive that is associated with this article, is an archive of a serial port capture from this GPS receiver that spans about 10 minutes of time, from device initialization through driving on several miles of city streets.

Simulating the GPS Receiver

For reproducibility in testing, it is useful to be able to play back a capture from the GPS receiver. We next develop an application, SerialWriter, which reads the Boost.Serialization archive and writes it out to the serial port.

As before, command-line arguments are processed by the Program_options library. Also as before, the port name and baud rate must be specified. Either a literal message, or an archive file, needs to also be provided to be used for the data to send over the port. When a file is specified, by default, data from an archive file is replayed at the rate that it was captured, but the no_time_offsets option will cause all of the data to be sent at one time. As this command-line option is a flag and does not take parameters, the zero_tokens() attribute is specified.

  1. // SerialWriter/SerialWriter.cpp
  2. int main(int argc, char *argv[]) {
  3. try {
  4. std::string portName, message, file;
  5. int baudRate;
  6. bool noTimeOffsets = false;
  7.  
  8. boost::program_options::options_description desc("Options");
  9. desc.add_options()
  10. ("help,h", "help")
  11. ("port,p", boost::program_options::value<std::string>(
  12. &portName)->required(), "port name (required)")
  13. ("baud,b", boost::program_options::value<int>(
  14. &baudRate)->required(), "baud rate (required)")
  15. ("message,m", boost::program_options::value<std::string>(
  16. &message), "message to send")
  17. ("file,f", boost::program_options::value<std::string>(
  18. &file), "file to send")
  19. ("no_time_offsets,n", boost::program_options::value<bool>(
  20. &noTimeOffsets)->zero_tokens(),
  21. "ignore time offsets in files to send data "
  22. "as fast as possible")
  23. ;
  24.  
  25. boost::program_options::variables_map vm;
  26. boost::program_options::store(
  27. boost::program_options::parse_command_line(
  28. argc, argv, desc), vm);
  29.  
  30. if (vm.empty() || vm.count("help")) {
  31. std::cout << desc << "\n";
  32. return -1;
  33. }
  34.  
  35. boost::program_options::notify(vm);

The data to send is collected in a write buffer...

  1. std::vector<WriteBufferElement> writeBuffer;

...where each element of the buffer is a tuple pairing the time between writes and the data to be written, as defined elsewhere in the file.

  1. typedef boost::tuple<boost::posix_time::time_duration::tick_type,
  2. std::vector<unsigned char>> WriteBufferElement;

If a message to send is provided on the command line, it is set as the one entry in the write buffer to be sent immediately (at a time of 0).

  1. if (!message.empty()) {
  2. std::vector<unsigned char> v;
  3. std::copy(message.begin(), message.end(),
  4. std::back_insert_iterator<std::vector<unsigned char>>(v));
  5. writeBuffer.push_back(boost::make_tuple(0, v));
  6. }

If an archive file is supplied, it is read and each entry in the file becomes one element in the write buffer. An exception is thrown when the end of the archive is reached, so it is caught and ignored.

  1. if (!file.empty()) {
  2. std::ifstream in(file);
  3. boost::archive::text_iarchive ia(in);
  4. while (true) {
  5. try {
  6. WriteBufferElement e;
  7. uint64_t us;
  8. ia >> us >> e.get<1>();
  9. e.get<0>() = us;
  10. writeBuffer.push_back(e);
  11. } catch (const std::exception &) {
  12. // end of archive - ignore exception
  13. break;
  14. }
  15. }
  16. }

The invocation of the Executor is similar to that of SerialReader, except that a SerialWriter object is constructed.

  1. ASIOLib::Executor e;
  2. e.OnWorkerThreadError = [](boost::asio::io_service &,
  3. boost::system::error_code ec) {
  4. Log(std::string("SerialWriter error (asio): ") +
  5. boost::lexical_cast<std::string>(ec)); };
  6. e.OnWorkerThreadException = [](boost::asio::io_service &,
  7. const std::exception &ex) {
  8. Log(std::string("SerialWriter exception (asio): ") +
  9. ex.what()); };
  10. const boost::shared_ptr<SerialWriter> spd(new SerialWriter(
  11. portName, baudRate, writeBuffer, noTimeOffsets));
  12. e.OnRun = boost::bind(&SerialWriter::Create, spd, _1);
  13. e.Run();
  14. } catch (const std::exception &e) {
  15. std::cout << "SerialWriter exception (main): " << e.what() <<
  16. std::endl;
  17. return -1;
  18. }
  19. return 0;
  20. }

The constructor of SerialWriter stores parameters from main(), and the work occurs entirely in the Create() method.

  1. class SerialWriter : private boost::noncopyable,
  2. public boost::enable_shared_from_this<SerialWriter> {
  3. boost::shared_ptr<ASIOLib::SerialPort> _serialPort;
  4. std::string _portName;
  5. unsigned int _baudRate;
  6. std::vector<WriteBufferElement> _writeBuffer;
  7. bool _noTimeOffsets;
  8. public:
  9. SerialWriter(const std::string &portName, int baudRate,
  10. const std::vector<WriteBufferElement> &writeBuffer,
  11. bool noTimeOffsets) :
  12. _portName(portName), _baudRate(baudRate),
  13. _writeBuffer(writeBuffer), _noTimeOffsets(noTimeOffsets) {}

In Create(), a SerialPort object is instantiated and opened.

  1. void Create(boost::asio::io_service &ios) {
  2. _serialPort.reset(new ASIOLib::SerialPort(ios, _portName));
  3. _serialPort->Open(0, _baudRate);

Next, we define a function to post to io_service to execute. Each of the items in the write buffer is written to the serial port. If the no_time_offsets option is specified, the buffer is written immediately. Otherwise, we maintain a running sum of the differences between buffer entries to obtain the time in the future that any given sequence of bytes is to be written. Asio provides deadline timers which expire at a specified time and, upon expiration, execute a callback function. That callback function we also define as a lambda, which simply writes the particular byte sequence to the port.

  1. ios.post([=, &ios] {
  2. uint64_t startTime = 0;
  3. std::for_each(_writeBuffer.begin(), _writeBuffer.end(),
  4. [&](const WriteBufferElement &e) {
  5. // if noTimeOffsets, just write the buffer,
  6. // otherwise create a timer to write the buffer
  7. // in the future
  8. if (_noTimeOffsets)
  9. _serialPort->Write(e.get<1>());
  10. else {
  11. startTime += e.get<0>();
  12. const boost::shared_ptr<
  13. boost::asio::deadline_timer> timer(
  14. new boost::asio::deadline_timer(ios));
  15. timer->expires_from_now(
  16. boost::posix_time::milliseconds(startTime));
  17. timer->async_wait([=](
  18. const boost::system::error_code &ec) {
  19. // keep the timer object alive
  20. boost::shared_ptr<
  21. boost::asio::deadline_timer> t(timer);
  22. _serialPort->Write(e.get<1>());
  23. });
  24. }
  25. }
  26. );
  27. });
  28. }
  29. };

The SerialWriter can be demonstrated by using a virtual serial port pair. Under Windows, the com0com null-modem emulator [26] can be used. After installation, by default two virtual ports are created named \\.\CNCA0 and \\.\CNCB0. Serial data that is written to one port is read from the other. While this works well, it should be noted that to install it under 64-bit Windows requires Windows to be placed into a driver testing mode that removes security checks. This may not be suitable for all environments. As described in the installation instructions [27] for the version current as of this writing (3.0.0.0), installation requires enabling the use of test-signed drivers by executing the following at a command prompt:

> bcdedit -set TESTSIGNING ON

and rebooting the computer.

Under Linux, one can use socat in a similar manner, but as it is a user-mode process, does not pose a potential security risk [28]. Specifically, executing the following at a prompt creates two virtual serial ports that exist as long as socat remains running. The particular ports created may be different from run to run. In this example, the created ports are /dev/pts/4 and /dev/pts/6.

$ socat -d -d pty,raw,echo=0 pty,raw,echo=0
2013/01/18 13:29:42 socat[3378] N PTY is /dev/pts/4
2013/01/18 13:29:42 socat[3378] N PTY is /dev/pts/6
2013/01/18 13:29:42 socat[3378] N starting data transfer loop with 
    FDs [4,4] and [6,6]

Opening a terminal connection to one port, such as with putty [29] under Windows, and executing SerialWriter to write to the other will show the data played back in the terminal window.

Conclusion

This article has shown that writing platform-independent code that interacts with the serial port is possible using Boost, and that Boost Asio can be used as the core of a highly scalable application. In part II of this article series, we will show how to decode the GPS data, and publish it as OpenDDS samples.

References

The Middleware News Brief is a periodic newsletter. The purpose and intent of this publication is to advance and inform on features, news, and technical information about Open Source, middleware technologies (e.g., TAO, OpenDDS, JacORB), including case studies and other technical content.

© Copyright Object Computing, Inc. 1993, 2016. All rights reserved

Subscribe

secret