![]() |
![]() |
![]() |
![]() |
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.
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:
// ASIOLib/Executor.h #include <boost/asio.hpp> #include <boost/function.hpp> #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.
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
.
namespace ASIOLib { 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 ASIOLib
,
ASIOLib_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.
protected: boost::asio::io_service _ioService; void WorkerThread(boost::asio::io_service &io_service);
Callback functions are provided which will be invoked at interesting points of the execution.
public: boost::function<void (boost::asio::io_service &)> OnWorkerThreadStart; boost::function<void (boost::asio::io_service &)> OnWorkerThreadStop; boost::function<void (boost::asio::io_service &, boost::system::error_code)> OnWorkerThreadError; boost::function<void (boost::asio::io_service &, const std::exception &)> OnWorkerThreadException; 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.
boost::asio::io_service &GetIOService() { return _ioService; } void AddCtrlCHandling(); void Run(unsigned int numThreads = -1); }; }
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.
// ASIOLib/Executor.cpp #include "Executor.h" #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.
void ASIOLib::Executor::Run(unsigned int numThreads) { if (OnRun) OnRun(_ioService); boost::thread_group workerThreads; for (unsigned int i = 0; i < ((numThreads == (unsigned int)-1) ? (boost::thread::hardware_concurrency()) : numThreads); ++i) workerThreads.create_thread(boost::bind( &Executor::WorkerThread, this, boost::ref(_ioService))); workerThreads.join_all(); }
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 to
join_all()
on the thread group.
The WorkerThread()
method wraps the call to io_service.run()
.
void ASIOLib::Executor::WorkerThread(boost::asio::io_service &ios) { if (OnWorkerThreadStart) OnWorkerThreadStart(ios); while (true) { try { boost::system::error_code ec; ios.run(ec); if (ec && OnWorkerThreadError) OnWorkerThreadError(ios, ec); break; } catch(const std::exception &ex) { if (OnWorkerThreadException) OnWorkerThreadException(ios, ex); } } if (OnWorkerThreadStop) OnWorkerThreadStop(ios); }
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 exeception, 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.
void ASIOLib::Executor::AddCtrlCHandling() { boost::asio::signal_set sig_set(_ioService, SIGTERM, SIGINT); sig_set.async_wait(boost::bind( &boost::asio::io_service::stop, boost::ref(_ioService))); }
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.
// ASIOLib/SerialPort.h #include <boost/asio.hpp> #include <boost/tuple/tuple.hpp> #include <boost/thread/mutex.hpp> #include <boost/enable_shared_from_this.hpp> #include <boost/function.hpp> #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()
.
namespace ASIOLib { typedef boost::tuple< boost::asio::serial_port_base::character_size, boost::asio::serial_port_base::parity, boost::asio::serial_port_base::stop_bits> SerialParams; static const SerialParams SP_8N1 = boost::make_tuple( 8, boost::asio::serial_port_base::parity::none, boost::asio::serial_port_base::stop_bits::one); static const SerialParams SP_7E1 = boost::make_tuple( 7, boost::asio::serial_port_base::parity::even, 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.
class ASIOLib_Export SerialPort : private boost::noncopyable, public boost::enable_shared_from_this<SerialPort> { boost::asio::serial_port _serialPort; bool _isOpen; boost::system::error_code _errorCode; boost::mutex _errorCodeMutex; std::vector<unsigned char> _readBuffer; boost::function<void (boost::asio::io_service &, const std::vector<unsigned char> &, size_t)> _onRead; std::vector<unsigned char> _writeQueue, _writeBuffer; 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.
boost::system::error_code Flush(); void SetErrorCode(const boost::system::error_code &ec); void ReadBegin(); void ReadComplete(const boost::system::error_code &ec, size_t bytesTransferred); void WriteBegin(); 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.
public: SerialPort(boost::asio::io_service &ioService, const std::string &portName); ~SerialPort(); void Open( const boost::function<void (boost::asio::io_service &, const std::vector<unsigned char> &, size_t)> &onRead, unsigned int baudRate, SerialParams serialParams = SP_8N1, boost::asio::serial_port_base::flow_control flowControl = boost::asio::serial_port_base::flow_control( boost::asio::serial_port_base::flow_control::none) ); void Close(); void Write(const unsigned char *buffer, size_t bufferLength); void Write(const std::vector<unsigned char> &buffer); void Write(const std::string &buffer); }; };
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 analagous 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.
// ASIOLib/SerialPort.cpp #include "SerialPort.h" #include <boost/bind.hpp> boost::system::error_code ASIOLib::SerialPort::Flush() { boost::system::error_code ec; #if !defined(BOOST_WINDOWS) && !defined(__CYGWIN__) const bool isFlushed =! ::tcflush(_serialPort.native(), TCIOFLUSH); if (!isFlushed) ec = boost::system::error_code(errno, boost::asio::error::get_system_category()); #else const bool isFlushed = ::PurgeComm(_serialPort.native(), PURGE_RXABORT | PURGE_RXCLEAR | PURGE_TXABORT | PURGE_TXCLEAR); if (!isFlushed) ec = boost::system::error_code(::GetLastError(), boost::asio::error::get_system_category()); #endif return ec; }
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.
void ASIOLib::SerialPort::SetErrorCode( const boost::system::error_code &ec) { if (ec) { boost::mutex::scoped_lock lock(_errorCodeMutex); _errorCode = ec; } }
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.
void ASIOLib::SerialPort::ReadBegin() { _serialPort.async_read_some(boost::asio::buffer(_readBuffer), boost::bind(&SerialPort::ReadComplete, shared_from_this(), boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred)); }
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.
void ASIOLib::SerialPort::ReadComplete( const boost::system::error_code &ec, size_t bytesTransferred) { if (!ec) { if (_onRead && (bytesTransferred > 0)) _onRead(boost::ref(_serialPort.get_io_service()), boost::cref(_readBuffer), bytesTransferred); ReadBegin(); // queue another read } else { Close(); SetErrorCode(ec); } }
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.
void ASIOLib::SerialPort::Write(const unsigned char *buffer, size_t bufferLength) { { boost::mutex::scoped_lock lock(_writeQueueMutex); _writeQueue.insert(_writeQueue.end(), buffer, buffer+bufferLength); } _serialPort.get_io_service().post(boost::bind( &SerialPort::WriteBegin, shared_from_this())); }
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.
void ASIOLib::SerialPort::WriteBegin() { boost::mutex::scoped_lock writeBufferlock(_writeBufferMutex); if (_writeBuffer.size() != 0) return; // a write is in progress, so don't start another boost::mutex::scoped_lock writeQueuelock(_writeQueueMutex); if (_writeQueue.size() == 0) return; // nothing to write // allocate a larger buffer if needed const std::vector<unsigned char>::size_type writeQueueSize = _writeQueue.size(); if (writeQueueSize > _writeBuffer.size()) _writeBuffer.resize(writeQueueSize); // copy the queued bytes to the write buffer, // and clear the queued bytes std::copy(_writeQueue.begin(), _writeQueue.end(), _writeBuffer.begin()); _writeQueue.clear(); boost::asio::async_write(_serialPort, boost::asio::buffer(_writeBuffer, writeQueueSize), boost::bind(&SerialPort::WriteComplete, shared_from_this(), boost::asio::placeholders::error)); }
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.
void ASIOLib::SerialPort::WriteComplete( const boost::system::error_code &ec) { if (!ec) { { boost::mutex::scoped_lock lock(_writeBufferMutex); _writeBuffer.clear(); } // more bytes to send may have arrived while the write // was in progress, so check again WriteBegin(); } else { Close(); SetErrorCode(ec); } }
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.
ASIOLib::SerialPort::SerialPort(boost::asio::io_service &ioService, const std::string &portName) : _serialPort(ioService, portName), _isOpen(false) { _readBuffer.resize(128); }
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.
void ASIOLib::SerialPort::Open( const boost::function<void (boost::asio::io_service &, const std::vector<unsigned char> &, size_t)> &onRead, unsigned int baudRate, ASIOLib::SerialParams serialParams, boost::asio::serial_port_base::flow_control flowControl) { _onRead = onRead; _serialPort.set_option( boost::asio::serial_port_base::baud_rate(baudRate)); _serialPort.set_option(serialParams.get<0>()); _serialPort.set_option(serialParams.get<1>()); _serialPort.set_option(serialParams.get<2>()); _serialPort.set_option(flowControl); const boost::system::error_code ec = Flush(); if (ec) SetErrorCode(ec); _isOpen = true; if (_onRead) _serialPort.get_io_service().post(boost::bind( &SerialPort::ReadBegin, shared_from_this())); }
The destructor simply closes the serial port.
ASIOLib::SerialPort::~SerialPort() { Close(); }
To close a port, outstanding requests are cancelled first, and then the port itself is closed.
void ASIOLib::SerialPort::Close() { if (_isOpen) { _isOpen = false; boost::system::error_code ec; _serialPort.cancel(ec); SetErrorCode(ec); _serialPort.close(ec); SetErrorCode(ec); } }
Finally, two other variants of the Write()
method are given, to provide additional calling options
to the user.
void ASIOLib::SerialPort::Write( const std::vector<unsigned char> &buffer) { Write(&buffer[0], buffer.size()); } void ASIOLib::SerialPort::Write(const std::string &buffer) { Write(reinterpret_cast<const unsigned char *>(buffer.c_str()), buffer.size()); }
This completes ASIOLib
, and we can now use it to read data from a serial port.
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.
// SerialReader.cpp #include "../ASIOLib/Executor.h" #include "../ASIOLib/SerialPort.h" #include <boost/thread.hpp> #include <boost/date_time.hpp> #include <boost/program_options.hpp> #include <boost/archive/text_oarchive.hpp> #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.
boost::mutex cout_lock; void Log(const std::string &msg) { boost::mutex::scoped_lock lock(cout_lock); std::cout << "[" << boost::this_thread::get_id() << "] " << msg << std::endl; }
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.
int main(int argc, char *argv[]) { std::string portName, file; int baudRate; boost::program_options::options_description desc("Options"); desc.add_options() ("help,h", "help") ("port,p", boost::program_options::value<std::string>( &portName)->required(), "port name (required)") ("baud,b", boost::program_options::value<int>( &baudRate)->required(), "baud rate (required)") ("file,f", boost::program_options::value<std::string>( &file), "file to save to") ;
The parse_command_line()
function parses the command-line arguments and populates
a map which can later be queried.
boost::program_options::variables_map vm; boost::program_options::store( boost::program_options::parse_command_line( argc, argv, desc), vm);
The first option that is processed is help. If either help, h or no arguments are provided, a list of available options is displayed on the console, and the program exits.
if (vm.empty() || vm.count("help")) { std::cout << desc << "\n"; return -1; }
In order to detect errors, such as missing required parameters, boost::program_options::notify()
is called.
This must be called after processing the help option, 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.
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.
const boost::scoped_ptr<std::ostream> out( file.empty() ? 0 : new std::ofstream(file.c_str())); const boost::scoped_ptr<boost::archive::text_oarchive> archive( 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.
ASIOLib::Executor e; e.OnWorkerThreadError = [](boost::asio::io_service &, boost::system::error_code ec) { Log(std::string("SerialReader error (asio): ") + boost::lexical_cast<std::string>(ec)); }; e.OnWorkerThreadException = [](boost::asio::io_service &, const std::exception &ex) { Log(std::string("SerialReader exception (asio): ") + ex.what()); }; const boost::shared_ptr<SerialReader> sp(new SerialReader( portName, baudRate, archive)); e.OnRun = boost::bind(&SerialReader::Create, sp, _1); e.Run(); return 0; }
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.
class SerialReader : private boost::noncopyable, public boost::enable_shared_from_this<SerialReader> { boost::shared_ptr<ASIOLib::SerialPort> _serialPort; std::string _portName; unsigned int _baudRate; const boost::scoped_ptr<boost::archive::text_oarchive> &_oa; boost::posix_time::ptime _lastRead; void OnRead(boost::asio::io_service &ios, const std::vector<unsigned char> &buffer, size_t bytesRead); public: SerialReader(const std::string &portName, int baudRate, const boost::scoped_ptr<boost::archive::text_oarchive> &oa) : _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.
void Create(boost::asio::io_service &ios) { try { _serialPort.reset(new ASIOLib::SerialPort(ios, _portName)); _serialPort->Open(boost::bind(&SerialReader::OnRead, shared_from_this(), _1, _2, _3), _baudRate); } catch (const std::exception &e) { std::cout << e.what() << std::endl; } } };
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.
void SerialReader::OnRead(boost::asio::io_service &, const std::vector<unsigned char> &buffer, size_t bytesRead) { const boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time(); if (_lastRead == boost::posix_time::not_a_date_time) _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.
const std::vector<unsigned char> v(buffer.begin(), 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.
if (_oa) { const uint64_t offset = (now-_lastRead).total_milliseconds(); *_oa << offset << v; } _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.
std::copy(v.begin(), v.end(), std::ostream_iterator<unsigned char>(std::cout, "")); }
Under Windows, SerialReader
can be run as follows:
> SerialReader -p \\.\COM16 -b 4800
and under Linux as:
$ ./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.
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.
// SerialWriter/SerialWriter.cpp int main(int argc, char *argv[]) { try { std::string portName, message, file; int baudRate; bool noTimeOffsets = false; boost::program_options::options_description desc("Options"); desc.add_options() ("help,h", "help") ("port,p", boost::program_options::value<std::string>( &portName)->required(), "port name (required)") ("baud,b", boost::program_options::value<int>( &baudRate)->required(), "baud rate (required)") ("message,m", boost::program_options::value<std::string>( &message), "message to send") ("file,f", boost::program_options::value<std::string>( &file), "file to send") ("no_time_offsets,n", boost::program_options::value<bool>( &noTimeOffsets)->zero_tokens(), "ignore time offsets in files to send data " "as fast as possible") ; boost::program_options::variables_map vm; boost::program_options::store( boost::program_options::parse_command_line( argc, argv, desc), vm); if (vm.empty() || vm.count("help")) { std::cout << desc << "\n"; return -1; } boost::program_options::notify(vm);
The data to send is collected in a write buffer...
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.
typedef boost::tuple<boost::posix_time::time_duration::tick_type, 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).
if (!message.empty()) { std::vector<unsigned char> v; std::copy(message.begin(), message.end(), std::back_insert_iterator<std::vector<unsigned char>>(v)); writeBuffer.push_back(boost::make_tuple(0, v)); }
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.
if (!file.empty()) { std::ifstream in(file); boost::archive::text_iarchive ia(in); while (true) { try { WriteBufferElement e; uint64_t us; ia >> us >> e.get<1>(); e.get<0>() = us; writeBuffer.push_back(e); } catch (const std::exception &) { // end of archive - ignore exception break; } } }
The invocation of the Executor
is similar to that of SerialReader
, except that a SerialWriter
object is constructed.
ASIOLib::Executor e; e.OnWorkerThreadError = [](boost::asio::io_service &, boost::system::error_code ec) { Log(std::string("SerialWriter error (asio): ") + boost::lexical_cast<std::string>(ec)); }; e.OnWorkerThreadException = [](boost::asio::io_service &, const std::exception &ex) { Log(std::string("SerialWriter exception (asio): ") + ex.what()); }; const boost::shared_ptr<SerialWriter> spd(new SerialWriter( portName, baudRate, writeBuffer, noTimeOffsets)); e.OnRun = boost::bind(&SerialWriter::Create, spd, _1); e.Run(); } catch (const std::exception &e) { std::cout << "SerialWriter exception (main): " << e.what() << std::endl; return -1; } return 0; }
The constructor of SerialWriter
stores parameters from main()
, and the work occurs entirely
in the Create()
method.
class SerialWriter : private boost::noncopyable, public boost::enable_shared_from_this<SerialWriter> { boost::shared_ptr<ASIOLib::SerialPort> _serialPort; std::string _portName; unsigned int _baudRate; std::vector<WriteBufferElement> _writeBuffer; bool _noTimeOffsets; public: SerialWriter(const std::string &portName, int baudRate, const std::vector<WriteBufferElement> &writeBuffer, bool noTimeOffsets) : _portName(portName), _baudRate(baudRate), _writeBuffer(writeBuffer), _noTimeOffsets(noTimeOffsets) {}
In Create()
, a SerialPort
object is instantiated and opened.
void Create(boost::asio::io_service &ios) { _serialPort.reset(new ASIOLib::SerialPort(ios, _portName)); _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.
ios.post([=, &ios] { uint64_t startTime = 0; std::for_each(_writeBuffer.begin(), _writeBuffer.end(), [&](const WriteBufferElement &e) { // if noTimeOffsets, just write the buffer, // otherwise create a timer to write the buffer // in the future if (_noTimeOffsets) _serialPort->Write(e.get<1>()); else { startTime += e.get<0>(); const boost::shared_ptr< boost::asio::deadline_timer> timer( new boost::asio::deadline_timer(ios)); timer->expires_from_now( boost::posix_time::milliseconds(startTime)); timer->async_wait([=]( const boost::system::error_code &ec) { // keep the timer object alive boost::shared_ptr< boost::asio::deadline_timer> t(timer); _serialPort->Write(e.get<1>()); }); } } ); }); } };
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.
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.
[1] RS-232
http://en.wikipedia.org/wiki/RS-232
[2] Serial port
http://en.wikipedia.org/wiki/Serial_port
[3] NMEA 0183
http://en.wikipedia.org/wiki/NMEA_0183
[4] OpenDDS
http://www.ociweb.com/products/opendds
[5] MPC (The Makefile, Project, and Workspace Creator)
http://www.ociweb.com/products/mpc
[6] Boost.Asio
http://www.boost.org/doc/libs/1_53_0/doc/html/boost_asio.html
[7] Boost C++ Libraries
http://www.boost.org/
[8] Boost FAQ
http://www.boost.org/users/faq.html
[9] A guide to getting started with boost::asio
http://www.gamedev.net/blog/950/entry-2249317-a-guide-to-getting-started-with-boostasio
[10] Serial ports and C++
http://www.webalice.it/fede.tft/serial_port/serial_port.html
[11] C++ Programming using Boost
http://www.ociweb.com/training/C++-Programming-using-Boost
[12] The Proactor Design Pattern: Concurrency Without Threads
http://www.boost.org/doc/libs/1_53_0/doc/html/boost_asio/overview/core/async.html
[13] Proactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handlers for Asynchronous Events
http://www.cs.wustl.edu/~schmidt/PDF/proactor.pdf
[14] Boost.Function
http://www.boost.org/doc/libs/1_53_0/doc/html/function.html
[15] Boost.Thread
http://www.boost.org/doc/libs/1_53_0/doc/html/thread.html
[16] bind.hpp
http://www.boost.org/doc/libs/1_53_0/libs/bind/bind.html
[17] The Boost Tuple Library
http://www.boost.org/doc/libs/1_53_0/libs/tuple/doc/tuple_users_guide.html
[18] Smart Pointers
http://www.boost.org/doc/libs/1_53_0/libs/smart_ptr/smart_ptr.htm
[19] How for boost:: asio:: serial_port to make flush?
http://www.progtown.com/topic90228-how-for-boost-asio-serialport-to-make-flush.html
[20] Boost async_write problem
http://stackoverflow.com/questions/4994077/boost-async-write-problem
[21] Boost.Date_Time
http://www.boost.org/doc/libs/1_53_0/doc/html/date_time.html
[22] Boost.Program_options
http://www.boost.org/doc/libs/1_53_0/doc/html/program_options.html
[23] Serialization Overview
http://www.boost.org/doc/libs/1_53_0/doc/html/program_options.html
[24] Required and Optional Arguments Using Boost Library Program Options
http://stackoverflow.com/questions/5395503/required-and-optional-arguments-using-boost-library-program-options
[25] Boost.Lexical_Cast 1.0
http://www.boost.org/doc/libs/1_53_0/doc/html/boost_lexical_cast.html
[26] Null-modem emulator (com0com)
http://com0com.sourceforge.net/
[27] Null-modem emulator (com0com) ReadMe.txt
http://com0com.cvs.sourceforge.net/viewvc/com0com/com0com/ReadMe.txt?revision=RELEASED
[28] HowTo: Virtual Serial Ports on Linux using socat, and more
http://justcheckingonall.wordpress.com/2009/06/09/howto-vsp-socat/
[29] PuTTY Download Page
http://www.chiark.greenend.org.uk/~sgtatham/putty/download.html