2

I'm using boost::asio for serial communications and I'd like to listen for incoming data on a certain port. So, I register a ReadHandler using serialport::async_read_some() and then create a separate thread to process async handlers (calls io_service::run()). My ReadHandler re-registers itself at its end by again calling async_read_some(), which seems to be a common pattern.

This all works, and my example can print data to stdout as it's received - except that I've noticed that data received while the ReadHandler is running will not be 'read' until the ReadHandler is done executing and new data is received after that happens. That is to say, when data is received while ReadHandler is running, although async_read_some is called at the conclusion of ReadHandler, it will not immediately invoke ReadHandler again for that data. ReadHandler will only be called again if additional data is received after the initial ReadHandler is completed. At this point, the data received while ReadHandler was running will be correctly in the buffer, alongside the 'new' data.

Here's my minimum-viable-example - I had initially put it in Wandbox but realized it won't help to compile it online because it requires a serial port to run anyway.

// Include standard libraries
#include <iostream>
#include <string>
#include <memory>
#include <thread>

// Include ASIO networking library
#include <boost/asio.hpp>

class SerialPort
{
public:
    explicit SerialPort(const std::string& portName) :
        m_startTime(std::chrono::system_clock::now()),
        m_readBuf(new char[bufSize]),
        m_ios(),
        m_ser(m_ios)
    {
        m_ser.open(portName);
        m_ser.set_option(boost::asio::serial_port_base::baud_rate(115200));

        auto readHandler = [&](const boost::system::error_code& ec, std::size_t bytesRead)->void
        {
            // Need to pass lambda as an input argument rather than capturing because we're using auto storage class
            // so use trick mentioned here: http://pedromelendez.com/blog/2015/07/16/recursive-lambdas-in-c14/
            // and here: https://stackoverflow.com/a/40873505
            auto readHandlerImpl = [&](const boost::system::error_code& ec, std::size_t bytesRead, auto& lambda)->void
            {
                if (!ec)
                {
                    const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - m_startTime);

                    std::cout << elapsed.count() << "ms: " << std::string(m_readBuf.get(), m_readBuf.get() + bytesRead) << std::endl;

                    // Simulate some kind of intensive processing before re-registering our read handler
                    std::this_thread::sleep_for(std::chrono::seconds(5));

                    //m_ser.async_read_some(boost::asio::buffer(m_readBuf.get(), bufSize), lambda);
                    m_ser.async_read_some(boost::asio::buffer(m_readBuf.get(), bufSize), std::bind(lambda, std::placeholders::_1, std::placeholders::_2, lambda));
                }
            };

            readHandlerImpl(ec, bytesRead, readHandlerImpl);
        };

        m_ser.async_read_some(boost::asio::buffer(m_readBuf.get(), bufSize), readHandler);

        m_asioThread = std::make_unique<std::thread>([this]()
            {
                this->m_ios.run();
            });
    }

    ~SerialPort()
    {
        m_ser.cancel();
        m_asioThread->join();
    }

private:
    const std::chrono::system_clock::time_point m_startTime;
    static const std::size_t bufSize = 512u;
    std::unique_ptr<char[]> m_readBuf;
    boost::asio::io_service m_ios;
    boost::asio::serial_port m_ser;
    std::unique_ptr<std::thread> m_asioThread;
};


int main()
{
    std::cout << "Type q and press enter to quit" << std::endl;
    SerialPort port("COM1");

    while (std::cin.get() != 'q')
    {
        std::this_thread::sleep_for(std::chrono::milliseconds(200));
    }

    return 0;
}

(Don't mind the weird lambda stuff going on)

This program just prints data to stdout as it's received, along with a timestamp (milliseconds since program started). By connecting a virtual serial device to a virtual serial port pair, I can send data to the program (just typing in RealTerm, really). I can see the problem when I type a short string.

In this case, I typed 'hi', and the 'h' was printed immediately. I had typed the 'i' very shortly after, but at computer speeds it was quite a while, so it wasn't part of the initial data read into the buffer. At this point, the ReadHandler executes, which takes 5 seconds. During that time, the 'i' was received by the OS. But the 'i' does not get printed after the 5 seconds is up - the next async_read_some ignores it until I then type a 't', at which point it suddenly prints both the 'i' and the 't'. Example program output

Here's a clearer description of this test and what I want:

Test: Start program, wait 1 second, type hi, wait 9 seconds, type t
What I want to happen (printed to stdout by this program):
1000ms: h
6010ms: i
11020ms: t

What actually happens:
1000ms: h
10000ms: it

It seems very important that the program has a way to recognize data that was received between reads. I know there is no way to check if data is available (in the OS buffer) using ASIO serial ports (without using the native_handle, anyway). But I don't really need to, as long as the read call returns. One solution to this issue might be to just make sure ReadHandler finishes running as quickly as possible - obviously the 5-second delay in this example is contrived. But that doesn't strike me as a good solution; no matter how fast I make ReadHandler, it will still be possible to 'miss' data (in that it will not be seen until some new data is received later). Is there any way to ensure that my handler will read all data within some short time of it being received, without depending on the receipt of further data?

I've done a lot of searching on SO and elsewhere, but everything I've found so far is just discussing other pitfalls that cause the system to not work at all.

As an extreme measure, it looks like it may be possible to have my worker thread call io_service::run_for() with a timeout, rather than run(), and then every short while have that thread somehow trigger a manual read. I'm not sure what form that would take yet - it could just call serial_port::cancel() I suppose, and then re-call async_read_some. But this sounds hacky to me, even if it might work - and it would require a newer version of boost, to boot.

I'm building with boost 1.65.1 on Windows 10 with VS2019, but I really hope that's not relevant to this question.

snoof378
  • 35
  • 1
  • 6
  • Your question is basically meaningless. You will receive all data that has already arrived in the first read-handler, and you can't receive any more data until that handler has exited and the next one is invoked. If it is slow, speed it up. – user207421 Nov 06 '19 at 22:25
  • I don't think I phrased the question in the title correctly, but my concern is that the next one that's invoked will not call my ReadHandler until new data is received, beyond whatever was received by the OS during the execution of the first ReadHandler. It's not that the data is lost, but that I won't know it was received until further data is received at a later point. That is, the interim data will be impossible for me to access until new data is receiived. Does that make sense? – snoof378 Nov 07 '19 at 14:40

1 Answers1

2

Answering the question in the title: You can't. By the nature of async_read_some you're asking for a partial read and a call to your handler as soon as anything is read. You're then sleeping for a long time before another async_read_some is called.

no matter how fast I make ReadHandler, it will still be possible to 'miss' data (in that it will not be seen until some new data is received later)

If I'm understanding your concern correctly, it doesn't matter - you won't miss anything. The data is still there waiting on a socket/port buffer until next you read it.

If you only want to begin processing once a read is complete, you need one of the async_read overloads instead. This will essentially perform multiple read_somes on the stream until some condition is met. That could just mean everything on the port/socket, or you can provide some custom CompletionCondition. This is called on each read_some until it returns 0, at which point the read is considered complete and the ReadHandler is then called.

nnog
  • 1,607
  • 16
  • 23
  • Thanks! My concern is that even if I don't miss anything, I won't know that data was received until _more_ data is received afterward. I tried using ```async_read``` with ```boost::asio::transfer_at_least(1)``` as my CompletionCondition, but the behavior is the same. – snoof378 Nov 06 '19 at 20:43
  • I added some clarification to my question under 'Here's a clearer description...'. Does that make sense? – snoof378 Nov 06 '19 at 20:49
  • I guess you're right - there doesn't seem to be any way to do what I want with async_read_some. Maybe the thinking in their design is that most people are looking for complete messages in some protocol, so they can use async_read like you said. But I still don't understand how someone would implement a basic serial console (a la RealTerm), in which you don't know how much data is coming or what form it takes, and you need to display all data to the screen immediately after it's received. Thank you for taking the time to answer! – snoof378 Nov 07 '19 at 15:05