1

I am working on serial port routines using Boost::Asio.
I am configuring the port using the wrappers provided by Boost::Asio.
In my application either the end of data is denoted by receive timeout or \r\n line termination sequence.
As Boost::Asio doesn't provide wrapper to access and configure DCB - Windows, termios - Linux structure in order to configure timeout and/or line-ending; I am accessing native port handle/file descriptor that is returned via the native_handle wrapper and configuring the structures manually.
However it seems I am unable to configure the port properly.
Even if I configure the end of data to be denoted by \n the data is partially returned in chunks.
Similarly the data is also returned before timeout has occurred.


Update

The system works in 2 modes

  1. Line Mode -> The data is read line by line and the processed. Line ending characters \r, \n or \r\n.
  2. Bulk Mode -> Multi-line data is read. Once no data is received for pre-dertmined interval, the data is said to be completely received. e.g. If I don't receive new data for 50 milli-seconds I consider the transfer to be complete.

Code

bool SerialPort::open_port(void)
{
    try
    {
        this->port.open(this->port_name);
        this->native_port = this->port.native_handle();
        return true;
    }
    catch (const std::exception& ex)
    {
        PLOG_FATAL << ex.what();
    }
    return false;
}

bool SerialPort::open_port(const std::string& port_name, std::uint32_t baud_rate, std::uint8_t data_bits, std::uint8_t stop_bits,
    parity_t parity, flow_control_t flow_control, std::uint32_t read_timeout, std::uint32_t read_inter_byte_timeout,
    std::uint32_t write_timeout)
{
    try
    {
        this->port_name = port_name;
        if (not this->open_port())
            return false;
        if (not this->set_baud_rate(baud_rate).has_value())
            return false;
        if (not this->set_data_bits(data_bits).has_value())
            return false;
        if (not this->set_stop_bits(stop_bits).has_value())
            return false;
        if (not this->set_parity(parity).has_value())
            return false;
        if (not this->set_flow_control(flow_control).has_value())
            return false;
        this->read_timeout = read_timeout;
        if (read_inter_byte_timeout <= 0)
            this->read_inter_byte_timeout = 1;

#ifdef _WIN64
        BOOL            return_value;
        DCB             dcb = { 0 };
        COMMTIMEOUTS    timeouts = { 0 };
        if (this->line_mode)    //Set COM port to return data either at \n or \r
        {
            /*
            * If the function succeeds, the return value is nonzero.
            * If the function fails, the return value is zero. To get extended error information, call GetLastError.
            */
            return_value = GetCommState(this->native_port, &dcb);
            if (return_value)
            {
                if(this->new_line_character == '\r')
                    dcb.EofChar = '\r'; //Specify end of data character as carriage-return (\r)
                else // --> Default
                    dcb.EofChar = '\n'; //Specify end of data character as new-line (\n) 
            }
            else
            {
                PLOG_ERROR << "Error GetCommState : " << GetLastErrorAsString();
                return false;
            }
            /*
            * If the function succeeds, the return value is nonzero.
            * If the function fails, the return value is zero. To get extended error information, call GetLastError.
            */
            return_value = SetCommState(this->native_port, &dcb);
            if (not return_value)
            {
                PLOG_ERROR << "Error SetCommState : " << GetLastErrorAsString();
                return false;
            }
        }
        else    //Set COM port to return data on timeout
        {
            /*
            * If the function succeeds, the return value is nonzero.
            * If the function fails, the return value is zero. To get extended error information, call GetLastError.
            */
            return_value = GetCommTimeouts(this->native_port, &timeouts);
            if (return_value)
            {
                timeouts.ReadIntervalTimeout = this->read_inter_byte_timeout; // Timeout in miliseconds
                //timeouts.ReadTotalTimeoutConstant = 0;   //MAXDWORD; // in milliseconds - not needed
                //timeouts.ReadTotalTimeoutMultiplier = 0; // in milliseconds - not needed
                //timeouts.WriteTotalTimeoutConstant = 50; // in milliseconds - not needed
                //timeouts.WriteTotalTimeoutMultiplier = write_timeout; // in milliseconds - not needed
            }
            else
            {
                PLOG_ERROR << "Error GetCommTimeouts : " << GetLastErrorAsString();
                return false;
            }
            /*
            * If the function succeeds, the return value is nonzero.
            * If the function fails, the return value is zero. To get extended error information, call GetLastError.
            */
            return_value = SetCommTimeouts(this->native_port, &timeouts);
            if (not return_value)
            {
                PLOG_ERROR << "Error SetCommTimeouts : " << GetLastErrorAsString();
                return false;
            }
        }
#else //For Linux termios
#endif // _WIN64

        return true;
    }
    catch (const std::exception& ex)
    {
        PLOG_ERROR << ex.what();
        return false;
    }
}

void SerialPort::read_handler(const boost::system::error_code& error, std::size_t bytes_transferred)
{
    this->read_async(); // I realized I was calling read_async before reading data
    bool receive_complete{ false };
    try
    {
        if (error not_eq boost::system::errc::success)  //Error in serial port read
        {
            PLOG_ERROR << error.to_string();
            this->async_signal.emit(this->port_number, SerialPortEvents::read_error, error.to_string());
            return;
        }

        if (this->line_mode)
        {
            std::string temporary_recieve_data;
            std::transform(this->read_buffer.begin(), this->read_buffer.begin() + bytes_transferred, //Data is added to temporary buffer
                std::back_inserter(temporary_recieve_data), [](std::byte character) {
                    return static_cast<char>(character);
                }
            );
            boost::algorithm::trim(temporary_recieve_data); // Trim handles space character, tab, carriage return, newline, vertical tab and form feed
            //Data is further processed based on the Process logic
            receive_complete = true;
        }
        else    // Bulk-Data. Just append data to end of received_data string buffer.
                // Wait for timeout to trigger recevive_complete
        {
            //Test Function
            std::transform(this->read_buffer.begin(), this->read_buffer.begin() + bytes_transferred,
                std::back_inserter(this->received_data), [](std::byte character) {
                    return static_cast<char>(character);
                }
            );
            this->async_signal.emit(this->port_number, SerialPortEvents::read_data, this->received_data); //Data has been recieved send to server via MQTT
        }
            
    }
    catch (const std::exception& ex)
    {
        PLOG_ERROR << ex.what();
        this->async_signal.emit(this->port_number, SerialPortEvents::read_error, ex.what());
    }
}

Supporting Function

std::optional<std::uint32_t> SerialPort::set_baud_rate(std::uint32_t baud_rate)
{
    boost::system::error_code                   error;
    std::uint32_t                               _baud_rate = 1200;
    switch (baud_rate)
    {
    case 1200:
    case 2400:
    case 4800:
    case 9600:
    case 115200:
        _baud_rate = baud_rate;
        break;
    default:
        _baud_rate = 1200;
        break;
    }

    this->port.set_option(boost::asio::serial_port_base::baud_rate(_baud_rate), error);

    if (error)
    {
        PLOG_FATAL << error.message();
        return std::nullopt;
    }

    return baud_rate;
}

std::optional<std::uint8_t> SerialPort::set_data_bits(std::uint8_t data_bits)
{
    boost::system::error_code                   error;
    std::uint32_t                               _data_bits = 8;
    switch (data_bits)
    {
    case 7:
    case 8:
        _data_bits = data_bits;
        break;
    default:
        _data_bits = 8;
        break;
    }

    this->port.set_option(boost::asio::serial_port_base::character_size(_data_bits), error);

    if (error)
    {
        PLOG_FATAL << error.message();
        return std::nullopt;
    }

    return data_bits;
}

std::optional<std::uint8_t> SerialPort::set_stop_bits(std::uint8_t stop_bits)
{
    boost::system::error_code                   error;
    switch (stop_bits)
    {
    case 1:
        this->port.set_option(boost::asio::serial_port_base::stop_bits(boost::asio::serial_port_base::stop_bits::one), error);
        break;
    case 2:
        this->port.set_option(boost::asio::serial_port_base::stop_bits(boost::asio::serial_port_base::stop_bits::two), error);
        break;
    default:
        this->port.set_option(boost::asio::serial_port_base::stop_bits(boost::asio::serial_port_base::stop_bits::one), error);
        break;
    }

    if (error)
    {
        PLOG_FATAL << error.message();
        return std::nullopt;
    }

    return stop_bits;
}


std::optional<parity_t> SerialPort::set_parity(parity_t parity)
{
    boost::system::error_code                   error;
    switch (parity)
    {
    case Parity::none:
        this->port.set_option(boost::asio::serial_port_base::parity(boost::asio::serial_port_base::parity::none), error);
        break;
    case Parity::even:
        this->port.set_option(boost::asio::serial_port_base::parity(boost::asio::serial_port_base::parity::even), error);
        break;
    case Parity::odd:
        this->port.set_option(boost::asio::serial_port_base::parity(boost::asio::serial_port_base::parity::odd), error);
        break;
    default:
        this->port.set_option(boost::asio::serial_port_base::parity(boost::asio::serial_port_base::parity::none), error);
        break;
    }

    if (error)
    {
        PLOG_FATAL << error.message();
        return std::nullopt;
    }

    return parity;
}

std::optional<flow_control_t> SerialPort::set_flow_control(flow_control_t flow_control)
{
    boost::system::error_code                   error;
    switch (flow_control)
    {
    case FlowControl::none:
        this->port.set_option(boost::asio::serial_port_base::flow_control(boost::asio::serial_port_base::flow_control::none), error);
        break;
    case FlowControl::hardware:
        this->port.set_option(boost::asio::serial_port_base::flow_control(boost::asio::serial_port_base::flow_control::hardware), error);
        break;
    case FlowControl::software:
        this->port.set_option(boost::asio::serial_port_base::flow_control(boost::asio::serial_port_base::flow_control::software), error);
        break;
    default:
        this->port.set_option(boost::asio::serial_port_base::flow_control(boost::asio::serial_port_base::flow_control::none), error);
        break;
    }

    if (error)
    {
        PLOG_FATAL << error.message();
        return std::nullopt;
    }
    return flow_control;
}
Dark Sorrow
  • 1,681
  • 14
  • 37
  • DCB.EofChar does no such thing. – Hans Passant Dec 03 '22 at 06:36
  • @HansPassant; I got the reference from the link : https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-dcb. Third last entry is `EofChar`. – Dark Sorrow Dec 03 '22 at 07:01
  • 1
    https://think-async.com/Asio/asio-1.18.0/doc/asio/reference/read_until.html – Hans Passant Dec 03 '22 at 14:26
  • @HansPassant Isn't `read_until` synchronous in nature? The second problem I have with `read_until` is that I won't be able to run the function on timeout as I do when `this->line_mode` is `false`. – Dark Sorrow Dec 03 '22 at 15:37
  • 1
    @DarkSorrow I'm sure [async_read_until](https://think-async.com/Asio/asio-1.18.0/doc/asio/reference/async_read_until.html) isn't. I don't understand your claim w.r.t. timeouts. Why don't you ask /that/ as a question (https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem) – sehe Dec 03 '22 at 17:34
  • @sehe, Sorry for the confusion. I have updated the question. The system operates in 2 modes line mode and bulk mode, which are separated by if statement as mentioned in the code. In bulk mode the incoming data is multi-line hence I have set read timeout inetrval in the serial port structures. – Dark Sorrow Dec 03 '22 at 17:46
  • @sehe I have been looking into `boost::asio::async_read_until( this->port, boost::asio::buffer(this->read_buffer.data(), this->read_buffer.size()), this->new_line_character, boost::bind( &SerialPort::read_handler, this, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred ) );` but I am getting `E0304 no instance of overloaded function "boost::asio::async_read_until" matches the argument list` error. – Dark Sorrow Dec 03 '22 at 17:50

1 Answers1

1

To your comments:

the incoming data is multi-line hence I have set read timeout inetrval in the serial port structures

How does that make sense. There's no real difference handling timeouts in Asio either way/

Regarding the overload:

but I am getting E0304 no instance of overloaded function "boost::asio::async_read_until" matches the argument list error.

Here's my ten pence:

async_read_until(port,
    asio::dynamic_buffer(read_buffer),
    "\r\n",
    bind(&SerialPort::read_handler, this, error, bytes_transferred));

Note that you need dynamic buffers. The simplest thing I could think of that stays close to your original:

std::vector<std::byte> read_buffer;

Now, we'll update the read handler to erase the "consumed" part, because received buffer may contain data beyond the delimiter.

void read_handler(boost::system::error_code ec, size_t const bytes_transferred) {
    std::cerr << "received " << bytes_transferred << " bytes (" << ec.message() << ")"
              << std::endl;

    auto b = reinterpret_cast<char const*>(read_buffer.data()),
         e = b + std::min(bytes_transferred, read_buffer.size());

    if (std::all_of(
            b, e,                                                             //
            [](uint8_t ch) { return std::isspace(ch) || std::isgraph(ch); })) //
    {
        std::cerr << "ascii: " << quoted(std::string_view(b, e)) << std::endl;
    } else {
        std::cerr << "binary: ";
        auto fmt = std::cerr.flags();
        for (auto it = b; it != e; ++it) {
            std::cerr << " " << std::hex << std::showbase << std::setfill('0')
                      << std::setw(4) << static_cast<unsigned>(*it);
        }
        std::cerr.flags(fmt);
    }
    std::cerr << std::endl;

    read_buffer.erase(begin(read_buffer), begin(read_buffer) + bytes_transferred);

    if (!ec)
        read_async(ignore_timeout);
}

Full listing: Live On Coliru

#include <boost/asio.hpp>
#include <boost/asio/serial_port.hpp>
#include <boost/bind/bind.hpp>
#include <iomanip>
#include <iostream>
#include <ranges>
namespace asio = boost::asio;

static inline std::ostream PLOG_ERROR(std::cerr.rdbuf());

struct SerialPort {
    static constexpr uint32_t ignore_timeout = -1;

    SerialPort(asio::any_io_executor ex, std::string dev) : port(ex, dev) {}

    bool read_async(uint32_t timeout_override) {
        try {
            // not necessary: std::ranges::fill(read_buffer, std::byte{});

            if (timeout_override not_eq SerialPort::ignore_timeout) {
                read_timeout = timeout_override;
            }
            using namespace asio::placeholders;

            async_read_until(port,
                asio::dynamic_buffer(read_buffer),
                "\r\n",
                bind(&SerialPort::read_handler, this, error, bytes_transferred));

            return true;
        } catch (std::exception const& ex) {
            PLOG_ERROR << ex.what() << std::endl;
            return false;
        }
    }

  private:
    void read_handler(boost::system::error_code ec, size_t const bytes_transferred) {
        std::cerr << "received " << bytes_transferred << " bytes (" << ec.message() << ")"
                  << std::endl;

        auto b = reinterpret_cast<char const*>(read_buffer.data()),
             e = b + std::min(bytes_transferred, read_buffer.size());

        if (std::all_of(
                b, e,                                                             //
                [](uint8_t ch) { return std::isspace(ch) || std::isgraph(ch); })) //
        {
            std::cerr << "ascii: " << quoted(std::string_view(b, e)) << std::endl;
        } else {
            std::cerr << "binary: ";
            auto fmt = std::cerr.flags();
            for (auto it = b; it != e; ++it) {
                std::cerr << " " << std::hex << std::showbase << std::setfill('0')
                          << std::setw(4) << static_cast<unsigned>(*it);
            }
            std::cerr.flags(fmt);
        }
        std::cerr << std::endl;

        read_buffer.erase(begin(read_buffer), begin(read_buffer) + bytes_transferred);

        if (!ec)
            read_async(ignore_timeout);
    }

    uint32_t               read_timeout = 10;
    std::vector<std::byte> read_buffer;
    asio::serial_port      port;
};

int main(int argc, char** argv) {
    asio::io_context ioc;

    SerialPort sp(make_strand(ioc), argc > 1 ? argv[1] : "/dev/ttyS0");
    sp.read_async(SerialPort::ignore_timeout);

    ioc.run();
}

Local demo:

enter image description here

sehe
  • 374,641
  • 47
  • 450
  • 633
  • Thanks your suggestion worked. For multiline termination based on receive timeout I am using `system_timer`. Can you help me understand why configuring Windows's serial port structure after extracting native handle didn't configure the serial port (line termination sequence or receive timeout. – Dark Sorrow Dec 06 '22 at 04:19
  • What is "multiline termination"? Either you have data framing or you don't. Framing "whatever arrives in n milliseconds" can be done with any producer/consumer combination. – sehe Dec 06 '22 at 14:56
  • I'm not sure how to answer your question about configuration. I'm not even convinced your observation or expectations are correct, but I may not be qualified to judge that anyways. – sehe Dec 06 '22 at 14:58
  • In `bulk mode` aka.` multi-line data` i.e. when `if (this->line_mode)` is `false`, I don't know how many bytes will be received in that particular transfer or what will be the end of transmission character e.g. `\r\n` . In such case I use timeout capability of serial port driver i.e. if no new data is received for n milliseconds, regard as transfer is complete and being processing received data. – Dark Sorrow Dec 06 '22 at 15:06
  • As for the for serial port in `DCB` or `termios` structure, I'm using the same proven code that is currently running in my application. The only unknown part is creating/opening handle/file descriptor using Boost::Asio insead of native Win32/Linux system call. – Dark Sorrow Dec 06 '22 at 15:08
  • In that case, I'm not sure you need non-portable things: [simplified code](http://coliru.stacked-crooked.com/a/455a6cf4bbef8f80) - live demo of timings https://i.imgur.com/zsWLWAc.gif – sehe Dec 06 '22 at 17:13