pythonserial-portpyserialmodbusrs485

Unable to read the full response coming over rs-485 from a Modbus device


I have been trying to communicate with a I-7000 IO module device that supports Modbus. In general I am able to successfully send commands such as writing to a coil or reading a coil but I face one issue - I am unable to read the response coming from the IO module device, at least not every time. I am using pyserial and I expect 6 bytes returned with a specific order but I fail to read the message correctly. Each message has a device id, function code, coil address, value and CRC message at the end. Most of the time I am able to only read the CRC message at the end and sometimes I am able to read some of the other bytes before that. Rarely I am able to read the whole message.

Here I did setup this simple code to debug my problem, it is not my main code but the issue I face is the same.

import serial
import time
import crcmod.predefined

# Function to calculate Modbus RTU CRC
def calculate_rs_485_crc(message):
    crc16 = crcmod.predefined.mkCrcFun("modbus")
    return crc16(message)

port = "/dev/ttyS0"
baudrate = 115200
timeout = 1

ser = serial.Serial(
    port,
    baudrate,
    timeout=timeout,
    parity=serial.PARITY_NONE,
)

# Construct Modbus RTU command
message = "020100060001"
command = bytes.fromhex(message)
crc = calculate_rs_485_crc(command)
command_with_crc = command + crc.to_bytes(2, "little")
    
print(f"Sent: {command_with_crc.hex()}")
ser.write(command_with_crc)

byteData = ser.read_until(b"\xcc")

print(f"byteData: {byteData}")

ser.close()

I have checked baudrate and the port, they are okay. Whatever timeout I place, the result is the same. I tried putting a time.sleep(0.5) between the write and read commands but this makes it actually worse because I am unable to receive any data then. Having no sleep time at least lets me receive some parts of the message.

This is my terminal where I see what is printed. Sometimes receive the end of the message (CRC bytes):
Sent: 0201000600011df8
byteData: b'Q\xcc'

And sometimes I can see the actually expected message. Or sometimes also something in between.
Sent: 0201000600011df8
byteData: b'\x02\x01\x01\x00Q\xcc'

Also, currently you can see me using read_until in the code but I tried using readline and just read by specifying the number of bytes I expect to read but none of it helped. Same result. I am not sure why this inconsistency happens. Most of my reading online shows timing issues or hardware issues but it is definitely not hardware issues because there is some other tools I used, provided by the manufacturer to talk to this device and it worked fine. If it is a timing issue, I don't understand how it is such because if I put some time to sleep in order to make sure every piece of the message has arrived, then i get nothing.

I hope you can help me out.

EDIT: Added mbpoll result:

user@localhost:~$ sudo mbpoll -m rtu -b 115200 -d 8 -s 1 -p none /dev/ttyS0 -a 2 -r 1 -1 -t 0
mbpoll 1.0-0 - FieldTalk(tm) Modbus(R) Master Simulator
Copyright © 2015-2019 Pascal JEAN, https://github.com/epsilonrt/mbpoll
This program comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
under certain conditions; type 'mbpoll -w' for details.

Protocol configuration: Modbus RTU
Slave configuration...: address = [2]
                        start reference = 1, count = 1
Communication.........: /dev/ttyS0,     115200-8E1
                        t/o 1.00 s, poll rate 1000 ms
Data type.............: discrete output (coil)

-- Polling slave 2...
Read discrete output (coil) failed: Connection timed out

EDIT #2: Imported a function

I have this other code which pretty much does the same thing, only it is in a function. I called this function in my code you can see above and then it works all fine. Not really sure why. But that is still not a good solution because I create two serial port instances and both write and read:

import serial
import time
import logging
import os
import crcmod.predefined
    
logger = logging.getLogger(__name__)
    
def send_over_rs485(message: str, max_attempts=3):
        attempts = 0
        while attempts < max_attempts:
            try:
                ser = serial.Serial("/dev/ttyS0", 115200, timeout=0.3)

                command = bytes.fromhex(message)
                crc = calculate_rs_485_crc(command)
                command_with_crc = command + crc.to_bytes(2, "little")

                ser.write(command_with_crc)
                 
                time.sleep(0.2) 

                if ser.in_waiting:
                    response = ser.readline()
                    return response.hex()
                else:
                    # print("No response available in rs-485 serial buffer.")
                    logging.debug("No response available rs-485 in serial buffer.")

            except serial.SerialException as e:
                logging.error(f"Serial communication error: {e}")
            except OSError as e:
                logging.error(f"OS-level communication error: {e}")
            except Exception as e:
                logging.error(f"Unexpected error: {e}")
            finally:
                if "ser" in locals() and ser.is_open:
                    ser.close()
                attempts += 1

        logging.warning(f"Reached maximum attempts without success: {attempts}")
        return None

# Function to calculate Modbus RTU CRC
def calculate_rs_485_crc(message):
    crc16 = crcmod.predefined.mkCrcFun("modbus")
    return crc16(message)

So this funciton I import and call in my code above like this:

import serial
import time
import crcmod.predefined
# NOTE: IMPORTED FUNCTION HERE
from serial_protocol import send_over_rs485 # NOTE: it worked only because of this


# Function to calculate Modbus RTU CRC
def calculate_rs_485_crc(message):
 crc16 = crcmod.predefined.mkCrcFun("modbus")
 return crc16(message)

port = "/dev/ttyS0"
baudrate = 115200
timeout = 1

ser = serial.Serial(
 port,
 baudrate,
 timeout=timeout,
 parity=serial.PARITY_NONE,
)

# Construct Modbus RTU command
message = "020100060001"
# NOTE: CALLED FUNCTION HERE 
# NOTE: This somehow makes it work:
send_over_rs485(message)

command = bytes.fromhex(message)
crc = calculate_rs_485_crc(command)
command_with_crc = command + crc.to_bytes(2, "little")
 
print(f"Sent: {command_with_crc.hex()}")
ser.write(command_with_crc)

byteData = ser.read_until(b"\xcc")

print(f"byteData: {byteData}")

ser.close()

With this change which I do not understand I get the full response. Hopefully you can help me out understand why.

Response:

Sent: 0201000600011df8
in waiting false 0
byteData: b'\x02\x01\x01\x00Q\xcc'

EDIT #3: Actually it was not the send_over_rs485 that made it possible to work but I was able to make it work if I create the socket write then read,then close the socket, then create the socket again and write and read again and then it works. The second time. I do not understand why. Here is the updated code:

import serial
import time
import crcmod.predefined

# Function to calculate Modbus RTU CRC
def calculate_rs_485_crc(message):
    crc16 = crcmod.predefined.mkCrcFun("modbus")
    return crc16(message)


def setup_serial_port() -> serial.Serial:
    
    port = "/dev/ttyS0"
    baudrate = 115200
    timeout = 0.3
    
    ser = serial.Serial(
        port=port,
        baudrate=baudrate ,
        bytesize=serial.EIGHTBITS,
        parity=serial.PARITY_NONE,
        stopbits=serial.STOPBITS_ONE,
        timeout=timeout
    )

    return ser

# Construct Modbus RTU command
message = "020100060001"

command = bytes.fromhex(message)
crc = calculate_rs_485_crc(command)
command_with_crc = command + crc.to_bytes(2, "little")

ser = setup_serial_port()

print(f"Sent: {command_with_crc}")
ser.write(command_with_crc)
time.sleep(0.2) 
byteData = ser.read(6)
print(f"byteData: {byteData}")

ser.close()

ser = setup_serial_port()

print(f"Sent: {command_with_crc}")
ser.write(command_with_crc)
time.sleep(0.5)  # Short delay to ensure the response is available

if ser.in_waiting:
    print(f"in waiting {ser.in_waiting}")
else:
    print(f"in waiting false {ser.in_waiting}")
byteData = ser.read(6)

print(f"byteData: {byteData}")

ser.close()

Solution

  • After more debugging and tinkering with the code, eventually it turned out its not a problem with the software but with the serial port on the computer where my code worked. With the help of someone else I was able to use a logic analyzer and saw that on the /dev/ttyS0 port the request goes successfully to the Modbus RTU device but the response did not arrive properly. It seems like the port is being used for something else too even though I used before that linux commands in the terminal to check if the port is being used and I saw nothing then.

    After finding this out, I moved the RS-485 cable to the other serial port present on the computer /dev/ttyS1 and then everything worked fine.

    Thank you all for the input. Hopefully this is useful for other people in need too.