pythonc++rs485

RS485 communication between C++ and Python, message splitted and receive in multiple reception


Context

I am working on a personal project on which I have to connect a Python script (3.7) to a QML application (Qt 5.2, C++17), all running on a Linux RHEL8 distribution.

Hardware connections

I have two computers with 2 USB ports, and 2 homemade cables with USB/RS485 converters in each sides.

What works

I can communicate from PC1 to PC2 with first cable, and in another direction with second cable.

What is wrong

The Python side receive data from C++, sometimes in one reception, sometimes in two. So I deal with it receiving data one by one character and waiting for SOF and EOF character to tell to manage the data received. But for me there is something wrong receiving sometimes data in multiple reception.

The same thing appears in the C++ side, when it is reading the data sent by the Python, sometimes the data is complete at first reception, sometimes two reception are needed.

Example: a message of length 15 bytes can be received in one time, or first I receive 4 characters then the 11 followings.

Wished RS485 configuration

Question

Being not an expert in RS485 communication, I read a lot of tutorial and documentation to understand how to configure both sides (C++ and Python). Can you tell me if the multiple reception is due to a wrong configuration of RS485 ? I don't know if the problem is coming from the C++ or Python side.

Minimalist code

In each side I have a thread for the reception, and another one to send data.

Python:

import threading, struct
from .GenericMessage import crc_16
import serial

class SerialManager:

    # Constructor
    def __init__(self):
        # Update parameter for the RS485 connection
        self.serialPortName_read = "/dev/ttyUSB1"
        self.serialPortName_send = "/dev/ttyUSB0"
        self.serialPort_read = serial.Serial()
        self.serialPort_send = serial.Serial()

        # Create sockets and start listening thread
        self.createRS485connection()
        self.receiverThread = threading.Thread(target=self.listeningThread)
        self.receiverThread.daemon = True
        self.receiverThread.start()


    def createRS485connection(self):
        # Configure sending device
        self.serialPort_send.port       = self.serialPortName_send  # Set device name
        self.serialPort_send.baudrate   = 115200                    # Set baud rate at 115 200 baud
        self.serialPort_send.bytesize   = serial.EIGHTBITS          # Set Number of data bits for a byte = 8
        self.serialPort_send.parity     = serial.PARITY_EVEN        # Set parity bit to even
        self.serialPort_send.stopbits   = serial.STOPBITS_ONE       # Set stop bit to 1

        # Open sending device
        self.serialPort_send.open()

        # Configure receiving device
        self.serialPort_read.port       = self.serialPortName_read  # Set device name
        self.serialPort_read.baudrate   = 115200                    # Set baud rate at 115 200 baud
        self.serialPort_read.bytesize   = serial.EIGHTBITS          # Set Number of data bits for a byte = 8
        self.serialPort_read.parity     = serial.PARITY_EVEN        # Set parity bit to even
        self.serialPort_read.stopbits   = serial.STOPBITS_ONE       # Set stop bit to 1
        self.serialPort_read.timeout    = 0.0001                    # Set timeout to read to 0.0001 second

        # Open reading device
        self.serialPort_read.open()

        # Reset all previous data received
        self.serialPort_read.reset_input_buffer()
        return


    def sendMessage(self, message):
        bytesSent = self.serialPort_send.write(message)
        if bytesSent < 0:
            print("Message not sending")
        elif bytesSent != len(message):
            print("Message not sent completely")
        else:
            return

    def listeningThread(self):
        # Initialize variables
        dataReceived = b''

        while self.isListening:

            # Wait for data to read
            if self.serialPort_read.in_waiting > 0:
                # Read byte one by one
                for i in range(self.serialPort_read.in_waiting):
                    dataReceived += self.serialPort_read.read(1)

                    # Function to manage character one by one and reconstruct the original message to deal with

C++

#include "../inc/SerialManager.h"

SerialManager::SerialManager()
{
    m_RS485_serialPortName_sending = "/dev/ttyUSB0";
    m_RS485_serialPortName_reading = "/dev/ttyUSB1";

    // Configure RS485 devices
    configureRS485sendingSide();
    configureRS485readingSide();

    // Create a new thread for listening and one for sending
    m_threadListeningServer = std::thread(&SerialManager::listenWhileLoop, this);
    m_threadListeningServer.detach();
    m_threadSendingServer = std::thread(&SerialManager::sendWhileLoop, this);
    m_threadSendingServer.detach();
}

SerialManager::~SerialManager()
{
    // Close the RS485 devices when finished
    if (close(m_RS485_serialPort_reading) < 0)
        qDebug() << "Reading device not closed properly: " << m_RS485_serialPortName_reading;
    if (close(m_RS485_serialPort_sending) < 0)
        qDebug() << "Reading device not closed properly: " << m_RS485_serialPortName_sending;
}

bool SerialManager::configureRS485readingSide()
{
    // Open the serial port in read only mode
    m_RS485_serialPort_reading = open(m_RS485_serialPortName_reading.toStdString().c_str(), O_RDONLY);

    // Create new termios struct, we call it 'tty' for convention
    struct termios tty;

    // Read in existing settings, and handle any error
    if(tcgetattr(m_RS485_serialPort_reading, &tty) != 0)
    {
        std::cout << "Error in reading side from tcgetattr: " + std::string(std::strerror(errno));
        throw std::runtime_error("Error in reading side from tcgetattr: " + std::string(std::strerror(errno)));
    }

    // Set control modes
    tty.c_cflag |= PARENB;     // 1. Set parity bit, enabling parity
    tty.c_cflag &= ~CSTOPB;    // 2. Clear stop field, only one stop bit used in communication
    tty.c_cflag &= ~CSIZE;     // 3.a. Clear all bits that set the data size
    tty.c_cflag |= CS8;        // 3.b. 8 bits per byte
    tty.c_cflag &= ~CRTSCTS;   // 4. Disable RTS/CTS hardware flow control (most common)
    tty.c_cflag |= CREAD | CLOCAL;     // 5. Turn on READ & ignore ctrl lines (CLOCAL = 1)

    // Set local modes
    tty.c_lflag &= ~ICANON;    // 1. Disable canonical mode
    tty.c_lflag &= ~ECHO;      // 2. Disable echo
    tty.c_lflag &= ~ECHOE;     // 3. Disable erasure
    tty.c_lflag &= ~ECHONL;    // 4. Disable new-line echo
    tty.c_lflag &= ~ISIG;      // 5. Disable interpretation of INTR, QUIT and SUSP characters

    // Set input modes
    tty.c_iflag &= ~(IXON | IXOFF | IXANY);                          // 1. Turn off software flow control
    tty.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL); // 2. Disable any special handling of received bytes

    // Set output modes
    tty.c_oflag &= ~OPOST;     // 1. Prevent special interpretation of output bytes (e.g. newline chars)
    tty.c_oflag &= ~ONLCR;     // 2. Prevent conversion of newline to carriage return/line feed

    // Set read configuration: blocking read of any number of chars with a maximum timeout (given by VTIME)
    tty.c_cc[VTIME] = 10;      // Wait for up to 1s (10 deciseconds), returning as soon as any data is received
    tty.c_cc[VMIN] = 0;

    // Set in baud rate to be 115200 baud
    cfsetispeed(&tty, B115200);

    // Save tty settings, also checking for error
    if (tcsetattr(m_RS485_serialPort_reading, TCSANOW, &tty) != 0)
    {
        std::cout << "Error in reading side from tcgetattr: " + std::string(std::strerror(errno));
        throw std::runtime_error("Error in reading side from tcsetattr: " + std::string(std::strerror(errno)));
    }

    qDebug() << "Reading side correctly configured";

    return true;
}

bool SerialManager::configureRS485sendingSide()
{
    // Open the serial port in write only mode
    m_RS485_serialPort_sending = open(m_RS485_serialPortName_sending.toStdString().c_str(), O_WRONLY);

    // Create new termios struct, we call it 'tty' for convention
    struct termios tty;

    // Read in existing settings, and handle any error
    if(tcgetattr(m_RS485_serialPort_sending, &tty) != 0)
    {
        std::cout << "Error in sending side from tcgetattr: " + std::string(std::strerror(errno));
        throw std::runtime_error("Error in sending side from tcgetattr: " + std::string(std::strerror(errno)));
    }

    // Set control modes
    tty.c_cflag |= PARENB;     // 1. Set parity bit, enabling parity
    tty.c_cflag &= ~CSTOPB;    // 2. Clear stop field, only one stop bit used in communication
    tty.c_cflag &= ~CSIZE;     // 3.a. Clear all bits that set the data size
    tty.c_cflag |= CS8;        // 3.b. 8 bits per byte
//    tty.c_cflag &= ~CRTSCTS;   // 4. Disable RTS/CTS hardware flow control (most common)
    tty.c_cflag |= CLOCAL;     // 5. Do not turn on READ & ignore ctrl lines (CLOCAL = 1)

    // Set local modes
    tty.c_lflag &= ~ICANON;    // 1. Disable canonical mode
    tty.c_lflag &= ~ECHO;      // 2. Disable echo
    tty.c_lflag &= ~ECHOE;     // 3. Disable erasure
    tty.c_lflag &= ~ECHONL;    // 4. Disable new-line echo
    tty.c_lflag &= ~ISIG;      // 5. Disable interpretation of INTR, QUIT and SUSP characters

    // Set input modes
    tty.c_iflag &= ~(IXON | IXOFF | IXANY);                          // 1. Turn off software flow control
    tty.c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL); // 2. Disable any special handling of received bytes

    // Set output modes
    tty.c_oflag &= ~OPOST;     // 1. Prevent special interpretation of output bytes (e.g. newline chars)
    tty.c_oflag &= ~ONLCR;     // 2. Prevent conversion of newline to carriage return/line feed

    // Set read configuration: blocking read of any number of chars with a maximum timeout (given by VTIME)
    tty.c_cc[VTIME] = 10;      // Wait for up to 1s (10 deciseconds), returning as soon as any data is received
    tty.c_cc[VMIN] = 0;

    // Set out baud rate to be 115200 baud
    cfsetospeed(&tty, B115200);

    // Save tty settings, also checking for error
    if (tcsetattr(m_RS485_serialPort_sending, TCSANOW, &tty) != 0)
    {
        std::cout << "Error in sending side from tcgetattr: " + std::string(std::strerror(errno));
        throw std::runtime_error("Error in sending side from tcsetattr: " + std::string(std::strerror(errno)));
    }

    qDebug() << "Writing side correctly configured";

    return true;
}


bool SerialManager::sendResponse(const char* msg, int msgSize)
{
    int bytesSent = write(m_RS485_serialPort_sending, msg, msgSize);

    if (bytesSent < 0)
    {
        std::cout << "Error sending message: " + std::string(std::strerror(errno));
        throw std::runtime_error("Error sending message: " + std::string(std::strerror(errno)));
    }
    else if (static_cast<size_t>(bytesSent) != msgSize)
    {
        std::cout << "Incomplete message sent: " + std::to_string(bytesSent) + " bytes sent instead of " + std::to_string(msgSize) + " bytes";
        throw std::runtime_error("Incomplete message sent: " + std::to_string(bytesSent) + " bytes sent instead of " + std::to_string(msgSize) + " bytes");
    }

    return true;
}


void SerialManager::listenWhileLoop()
{
    pthread_setname_np(pthread_self(), "listenWhileLoop");

    // First wait the end of the initialisation
    while (!m_swIsInitialized)
    {
        // Wait for 100s not to override processor
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }

    char buf[1024];
    while (m_softwareIsRunning)
    {
        // Wait the software to be initialized
        if (m_swIsInitialized)
        {
            // Wait until receive a message from a socket
            int sizeDataReceived = read(m_RS485_serialPort_reading, &buf, sizeof(buf));

            if (sizeDataReceived > 0)
            {
                switchTypeMessageReceived(buf, sizeDataReceived);
            }
            //clean buffer
            memset(&buf[0], 0, sizeof(buf));
        }
    }
}

void SerialManager::switchTypeMessageReceived(char *buffer, int sizeDataReceived)
{
    // Manage data received
}

Solution

  • Example: a message of length 15 bytes can be received in one time, or first I receive 4 characters then the 11 followings.

    That is perfectly normal behaviour.

    Serial lines transmit stream of bytes, not datagrams. There are no message boundaries, you have to add them yourself. One write() does not necessarily equal one read(). Since serial lines are quite slow compared CPU speed, you have a good change to call read() in the middle of the transmission and depending on buffering, you can get the message in arbitrary chunks.

    You can use idle time as a boundary, VMIN,VTIME can help with that - see termios(3) . Personally, I would not rely on that as you will have issues creating the gaps on the sending side due to presence of multiple SW buffers between your code and the hardware in normal OS.