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
}
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.