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).
1 Answer
Reset to default 2I 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
boost::asio::async_read_until
(serial_port_, buffer, '\n', ....
– Marek R Commented Nov 20, 2024 at 13:11async_read
– SupAl Commented Nov 20, 2024 at 13:32