Carson Ray
Carson Ray

Reputation: 11

Proper method to send and receive binary data over serial port with boost asio?

I am trying to write a serial communication protocol between a C++ application and ESP32 microcontroller. The basic structure uses 1 byte headers specify the category of data followed by various lengths of byte array data (I want to encode integers and floats mainly). I have tested the ESP32 side of the protocol using a binary terminal application and it has the expected behavior, so the issue is with the C++ application side, which uses the Boost ASIO library (v 1.82.0). Any time I attempt to send binary data using the synchronous write method, I get the error "The handle is invalid", so I think the main issue is I am unable to get byte array data into a format acceptable to the library.

I have checked and double checked that I have the right port configuration (baud rate, etc.). I have tried various methods of passing the data to the write function including just passing a pointer to the byte array, wrapping in vector<char>, and converting to C++ string but all of these get the same error. Also, I assume reading the data will just involve reversing this conversion, but I am not sure. Below are my basic methods for configuring the port, sending and receiving data so far.

bool SerialInterface::begin(const char* port, long baudRate) {
    try {
        serialPort.open(port);
        serialPort.set_option(asio::serial_port_base::baud_rate(baudRate));
        serialPort.set_option(asio::serial_port_base::character_size(8));
        serialPort.set_option(asio::serial_port_base::parity(asio::serial_port_base::parity::none));
        serialPort.set_option(asio::serial_port_base::stop_bits(asio::serial_port_base::stop_bits::one));
        serialPort.set_option(asio::serial_port_base::flow_control(asio::serial_port_base::flow_control::none));
    }
    catch (boost::system::system_error& e) {
        cerr << "Error opening serial port: " << e.what() << endl;
        return false; // Error opening the port
    }
    return true; // Success
}

void SerialInterface::sendByte(uint8_t data) {
    try {
        if (serialPort.is_open()) {
            //Should convert byte array into acceptable string format for write buffer
            uint8_t bytes[1] = { data };
            string buffer(reinterpret_cast<char*>(bytes), 1);
            asio::write(serialPort, asio::buffer(buffer, 1));
        }
    }
    catch (system::system_error& e) {
        //This exception gets thrown
        std::cerr << "Exception: " << e.what() << std::endl;
    }
}

uint8_t SerialInterface::readByte() {
    if (!serialPort.is_open()) {
        return 0;
    }

    uint8_t buffer[1];
    system::error_code error;

    try {
        //May need to reverse the conversion done when sending data?
        size_t bytes_read = asio::read(serialPort, boost::asio::buffer(buffer, 1), error);
        if (error || bytes_read == 0) {
            return 0;
        }
    }
    catch (system::system_error& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
        return 0;
    }

    return buffer[0];
}

Upvotes: 1

Views: 32

Answers (1)

sehe
sehe

Reputation: 393664

"I get the error "The handle is invalid", so I think the main issue is I am unable to get byte array data into a format acceptable to the library"

I get the error "the color is red" so I think the main issue is that the color is green?

The handle refers to the file-descriptor/native handle used to identify the stream opened to the serial device. If it is bad, it implies that it isn't valid. Perhaps it wasn't opened, or it has been closed.

The only thing we can imagine is that you don't keep the SerialInterface instance around long enough. When it is destructed, the serial_port will be destructed along with it. But that's part of the code not shown.

Here's my fixed listing, which does work as expected:

Live On Coliru

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

struct SerialInterface {
    SerialInterface(asio::any_io_executor ex) : port_(ex) {}

    bool begin(const char* port, long baudRate);
    void sendByte(uint8_t data);
    uint8_t readByte();

  private:
    using system_error = boost::system::system_error;
    using error_code   = boost::system::error_code;
    using SP = asio::serial_port;
    SP port_;
};

bool SerialInterface::begin(char const* port, long baudRate) try {
    port_.open(port);
    port_.set_option(SP::baud_rate(baudRate));
    port_.set_option(SP::character_size(8));
    port_.set_option(SP::parity(SP::parity::none));
    port_.set_option(SP::stop_bits(SP::stop_bits::one));
    port_.set_option(SP::flow_control(SP::flow_control::none));
    return true; // Success
} catch (system_error& e) {
    std::cerr << "Error opening serial port: " << e.what() << std::endl;
    return false; // Error opening the port
}

void SerialInterface::sendByte(uint8_t data) try {
    if (port_.is_open()) {
        // Should convert byte array into acceptable string format for write buffer
        std::vector const bytes{data}; // just as another example
        asio::write(port_, asio::buffer(bytes));
    }
} catch (system_error const& e) {
    std::cerr << "Exception: " << e.code().message() << std::endl;
}

uint8_t SerialInterface::readByte() {
    std::array<uint8_t, 1> buffer{0};

    try {
        error_code ec;
        size_t     bytes_read = asio::read(port_, asio::buffer(buffer), ec);
        if (ec || bytes_read == 0)
            buffer = {0};
    } catch (boost::system::system_error const& e) {
        std::cerr << "Exception: " << e.code().message() << std::endl;
        buffer = {0};
    }

    return buffer.front();
}

int main(int argc, char* argv[]) {
    asio::io_context io;
    SerialInterface  serial(io.get_executor());

    if (serial.begin(argc > 1 ? argv[1] : "/dev/pts/5", 9600)) {
        serial.sendByte(0x42);
        std::cout << "Received: " << std::showbase << std::hex << +serial.readByte() << std::endl;
    }
}

Test commands:

  • bottom left: socat -d -d pty,raw,echo=0 pyt,raw,echo=0 to emulate a serial device. It shows the endpoints as /dev/pts/5 and /dev/pts/6
  • top half: ./build/sotest /dev/pts/5
  • bottom right:
    • printf '\x43\x44\x45' | tee -a /dev/pts/6 provides some data sent back to the C++ code
    • stdbuf -i 0 -o 0 -- cat /dev/pts/6 | xxd -c 100 to print a hex-dump byte-for-byte print of the data received from the C++ side

Upvotes: 1

Related Questions