c++ - Boost asio serial port premature completion on async_read - Stack Overflow

I have a class that wraps a boost::asio::serial_port on a Windows system. I use it to start asynchronou

I have a class that wraps a boost::asio::serial_port on a Windows system. I use it to start asynchronous reads on a supplied buffer.

It works well in most cases when there is data in the queue before I start the async_read call (I tried to poll this with the function).

However, when no data is present, my async call completes with bytes_read = 0 and error_code = success.

I tried to debug the issue, and when I do the exact same call, but wait for the bytes to arrive for reading I read all the expected bytes.

Is there any way this can happen that is documented properly, or might this be a bug?

Here is the offending function:

bool stream_channel_serial_device::async_read_bytes(
    std::span<uint8_t>   buffer,
    completion_handler&& handler
)
{
    // this is called with a buffer size of 536 bytes
    boost::asio::async_read(
        serial_port_,
        boost::asio::buffer(buffer), // the size of the buffer is 536 bytes
        [this,
         handler = std::move(handler)]
        (boost::system::error_code const& ec, std::size_t bytes_read) {
            if (ec) {
                // THIS IS NOT TRIGGERED (i.e., ec is success),
                // BUT bytes_read is 0!
                log_error("Error reading data package: " + ec.message());
            }

            handler(rx_status{ec, bytes_read});
        }
    );

    return true;
}

Note that: The serial port is a member variable: boost::asio::serial_port serial_port_;

Also, the serial port is a virtual com port, but this has not caused any other issues (and really should fully emulate a COM port).

EDIT:

Added the output from using .html

It clearly shows a premature entering of the handler after reading 0 bytes and with no error code (handler 432).

@asio|1732123199.067050|<428|
@asio|1732123199.067050|>429|ec=system:0,bytes_transferred=536
@asio|1732123199.071049|429^430|in 'async_read' (.\build\default\vcpkg_installed\x64-windows\include\bo:373)
@asio|1732123199.071049|429*430|[email protected]_read_some
@asio|1732123199.098571|<429|
@asio|1732123199.098571|>430|ec=system:0,bytes_transferred=536
@asio|1732123199.101569|430^431|in 'async_read' (.\build\default\vcpkg_installed\x64-windows\include\bo:373)
@asio|1732123199.101569|430*431|[email protected]_read_some
@asio|1732123199.130571|<430|
@asio|1732123199.130571|>431|ec=system:0,bytes_transferred=536
@asio|1732123199.131573|431|[email protected]
@asio|1732123199.139613|431^432|in 'async_read' (.\build\default\vcpkg_installed\x64-windows\include\bo:373)
@asio|1732123199.139613|431*432|[email protected]_read_some
@asio|1732123199.140618|<431|
@asio|1732123199.161532|>432|ec=system:0,bytes_transferred=0

Edit 2

It seems only to happen if data has been written to the serial port first. If I do not write first, it will wait for data.

Edit 3

If I set a timeout on the serial port via the native_handle() on Windows, I do not get the issue (but then I'm relying on my data coming within a specified period and my code is not platform independent anymore).

I have a class that wraps a boost::asio::serial_port on a Windows system. I use it to start asynchronous reads on a supplied buffer.

It works well in most cases when there is data in the queue before I start the async_read call (I tried to poll this with the https://learn.microsoft/en-us/windows/win32/api/winbase/nf-winbase-clearcommerror function).

However, when no data is present, my async call completes with bytes_read = 0 and error_code = success.

I tried to debug the issue, and when I do the exact same call, but wait for the bytes to arrive for reading I read all the expected bytes.

Is there any way this can happen that is documented properly, or might this be a bug?

Here is the offending function:

bool stream_channel_serial_device::async_read_bytes(
    std::span<uint8_t>   buffer,
    completion_handler&& handler
)
{
    // this is called with a buffer size of 536 bytes
    boost::asio::async_read(
        serial_port_,
        boost::asio::buffer(buffer), // the size of the buffer is 536 bytes
        [this,
         handler = std::move(handler)]
        (boost::system::error_code const& ec, std::size_t bytes_read) {
            if (ec) {
                // THIS IS NOT TRIGGERED (i.e., ec is success),
                // BUT bytes_read is 0!
                log_error("Error reading data package: " + ec.message());
            }

            handler(rx_status{ec, bytes_read});
        }
    );

    return true;
}

Note that: The serial port is a member variable: boost::asio::serial_port serial_port_;

Also, the serial port is a virtual com port, but this has not caused any other issues (and really should fully emulate a COM port).

EDIT:

Added the output from using https://live.boost./doc/libs/1_86_0/doc/html/boost_asio/overview/core/handler_tracking.html

It clearly shows a premature entering of the handler after reading 0 bytes and with no error code (handler 432).

@asio|1732123199.067050|<428|
@asio|1732123199.067050|>429|ec=system:0,bytes_transferred=536
@asio|1732123199.071049|429^430|in 'async_read' (.\build\default\vcpkg_installed\x64-windows\include\bo:373)
@asio|1732123199.071049|429*430|[email protected]_read_some
@asio|1732123199.098571|<429|
@asio|1732123199.098571|>430|ec=system:0,bytes_transferred=536
@asio|1732123199.101569|430^431|in 'async_read' (.\build\default\vcpkg_installed\x64-windows\include\bo:373)
@asio|1732123199.101569|430*431|[email protected]_read_some
@asio|1732123199.130571|<430|
@asio|1732123199.130571|>431|ec=system:0,bytes_transferred=536
@asio|1732123199.131573|431|[email protected]
@asio|1732123199.139613|431^432|in 'async_read' (.\build\default\vcpkg_installed\x64-windows\include\bo:373)
@asio|1732123199.139613|431*432|[email protected]_read_some
@asio|1732123199.140618|<431|
@asio|1732123199.161532|>432|ec=system:0,bytes_transferred=0

Edit 2

It seems only to happen if data has been written to the serial port first. If I do not write first, it will wait for data.

Edit 3

If I set a timeout on the serial port via the native_handle() on Windows, I do not get the issue (but then I'm relying on my data coming within a specified period and my code is not platform independent anymore).

Share Improve this question edited Nov 29, 2024 at 8:22 SupAl asked Nov 20, 2024 at 11:22 SupAlSupAl 1,0818 silver badges20 bronze badges 4
  • You are using to simple API. There is a way to define what condition should be met to report a result. Since you are using serial port I suspect you would like to read data line by line, so just use: boost::asio::async_read_until(serial_port_, buffer, '\n', .... – Marek R Commented Nov 20, 2024 at 13:11
  • If you have fixed size data to read use async_read_at. – Marek R Commented Nov 20, 2024 at 13:17
  • @MarekR, Serial is not an AsyncRandomAccessReadDevice. Also, this simply skips bytes, but otherwise it has the same functionality as async_read – SupAl Commented Nov 20, 2024 at 13:32
  • @MarekR, regarding your first comment - no, this is not a too simple API, but exactly what I need. I know how many bytes I need to read. The bytes are not ASCII / text, but binary data, so I cannot use termination characters. – SupAl Commented Nov 20, 2024 at 13:33
Add a comment  | 

1 Answer 1

Reset to default 2

I know how many bytes I need to read

So tell Asio:

bool async_read_bytes(std::span<uint8_t> buffer, completion_handler&& handler) {
    static constexpr unsigned bytes_to_read = 536; // buffer.size()?

    asio::async_read(                              //
        serial_port_,                              //
        asio::buffer(buffer),                      //
        asio::transfer_exactly(bytes_to_read),     //
        [/*this,*/ handler = std::move(handler)](error_code const& ec, size_t bytes_read) {
            if (ec) {
                // THIS IS NOT TRIGGERED (i.e., ec is success),
                // BUT bytes_read is 0!
                log_error("Error reading data package: " + ec.message());
            }

            handler(rx_status{ec, bytes_read});
        });

There is asio::transfer_at_least as well. As you know (from the comment) there's also asio::read_until which has many more options, not constrained to delimiter sequences. See e.g. MatchCondition overloads, examples:

  • Boost ASIO System timer spurious timeout using it to track composed reads on a serial port
  • a midway solution using regex as match condition, then checks a checksum asio_read_some countinue reading to get all data
  • A fully dynamic framing protocol unget or similar solution for boost::asio::ip::tcp::iostream with a completely context-dependent match condition
  • a slightly simpler condition that uses a JSON stream reader to detect the end of a full JSON object boost::asio::async_read_until with custom match_char to accept only JSON format

There are other problems with the code, some of which are probably copy/paste due to creating the question (missing/excess rx_status).

Bonus: Idiomatic Initiation Functions

I see you adding non-asio call-backs. This may invite errors when operations are composed, because the associated executors (and other Associated Characteristics) are not preserved. See e.g. boost::asio::bind_executor does not execute in strand

Creating idiomatic async initiations is Not That Hard(TM) anymore these days:

template <typename CompletionToken>
auto async_read_bytes(std::span<uint8_t> buffer, CompletionToken&& token) {
    return asio::async_initiate<CompletionToken, ReadSig>(
        [](auto h, auto& s, auto b) mutable {
            async_read( //
                s, b, asio::transfer_exactly(asio::buffer_size(b)),
                [h = std::move(h)](error_code const& ec, size_t bytes_read) mutable {
                    // add behavior?
                    std::move(h)(ec, bytes_read);
                });
        },
        token, serial_port_, asio::buffer(buffer));
}

Or if you don't even need to add behaviour:

template <asio::completion_token_for<ReadSig> CompletionToken = asio::deferred_t>
auto async_read_bytes(std::span<uint8_t> buffer, CompletionToken&& token = {}) {
    return async_read(serial_port_, asio::buffer(buffer), std::forward<decltype(token)>(token));
}

This makes it so you can use any type of completion handler, including (bound) handlers, futures, c++ coros, etc:

Live On Coliru

#include <iostream>
#include <boost/asio.hpp>
#include <boost/asio/serial_port.hpp>
namespace asio = boost::asio;
using boost::system::error_code;

void log_error(std::string const& msg) { std::cerr << msg << std::endl; }

struct stream_channel_serial_device {
    stream_channel_serial_device(asio::any_io_executor ex, std::string const& port_name) 
        : serial_port_(ex, port_name) {}

    using ReadSig = void(error_code, size_t);

    // idiomatic ASIO initiation function:
    template <typename CompletionToken>
    auto async_read_bytes(std::span<uint8_t> buffer, CompletionToken&& token) {
        return asio::async_initiate<CompletionToken, ReadSig>(
            [](auto h, auto& s, auto b) mutable {
                async_read( //
                    s, b, asio::transfer_exactly(asio::buffer_size(b)),
                    [h = std::move(h)](error_code const& ec, size_t bytes_read) mutable {
                        // add behavior?
                        std::move(h)(ec, bytes_read);
                    });
            },
            token, serial_port_, asio::buffer(buffer));
    }

    // but even simpler, if no behaviour needs to be added:
    template <asio::completion_token_for<ReadSig> CompletionToken = asio::deferred_t>
    auto async_read_bytes_simple(std::span<uint8_t> buffer, CompletionToken&& token = {}) {
        return async_read(serial_port_, asio::buffer(buffer), std::forward<decltype(token)>(token));
    }

    asio::serial_port serial_port_;
};

int main() {
    asio::thread_pool io_context(1);
    stream_channel_serial_device device(io_context.get_executor(), "/dev/ttyUSB0");

    std::vector<uint8_t> buffer(536);
    device.async_read_bytes(buffer, [](error_code ec, size_t n) {
        std::cout << "Handler read " << n << " bytes (" << ec.message() << ")" << std::endl;
    });

    // or the simple version
    device.async_read_bytes_simple(buffer, [](error_code ec, size_t n) {
        std::cout << "Handler read " << n << " bytes (" << ec.message() << ")" << std::endl;
    });

    // or e.g. using futures (blocks main thread)
    auto [ec, n] = device.async_read_bytes_simple(buffer, asio::as_tuple(asio::use_future)).get();
    std::cout << "Future read " << n << " bytes (" << ec.message() << ")" << std::endl;

    // or e.g. using C++20 coroutines
    co_spawn(
        io_context,
        [&] -> asio::awaitable<void> {
            error_code ec;
            size_t     n = co_await device.async_read_bytes_simple(buffer);
            if (ec) {
                log_error("Error reading data package: " + ec.message());
            } else {
                std::cout << "Read " << n << " bytes\n";
            }
        },
        asio::detached);

    io_context.join();
}

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1742361855a4429545.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信