<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rebecca Anderson</title>
    <description>The latest articles on DEV Community by Rebecca Anderson (@rebecca_anderson_e63d00b1).</description>
    <link>https://dev.to/rebecca_anderson_e63d00b1</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3879522%2Ff85dbd75-4fbb-4927-aa81-d5143ee06921.webp</url>
      <title>DEV Community: Rebecca Anderson</title>
      <link>https://dev.to/rebecca_anderson_e63d00b1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rebecca_anderson_e63d00b1"/>
    <language>en</language>
    <item>
      <title>Reading Modbus RTU Data with Python</title>
      <dc:creator>Rebecca Anderson</dc:creator>
      <pubDate>Wed, 15 Apr 2026 02:12:40 +0000</pubDate>
      <link>https://dev.to/rebecca_anderson_e63d00b1/reading-modbus-rtu-data-with-python-4i37</link>
      <guid>https://dev.to/rebecca_anderson_e63d00b1/reading-modbus-rtu-data-with-python-4i37</guid>
      <description>&lt;p&gt;Most tutorials on reading Modbus RTU data with Python focus entirely on the software side. The standard advice is to run pip install pymodbus, plug a USB-to-RS485 adapter into your machine, and write a script using ModbusSerialClient.&lt;br&gt;
It usually works perfectly on your desk. But when you deploy it to a production environment, the connection randomly drops, you get timeout errors, or the data reads as garbage.&lt;/p&gt;

&lt;p&gt;Let's look at why this happens, and how to structure your Modbus integration so it actually stays up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Underlying Problem: Timing and Noise&lt;/strong&gt;&lt;br&gt;
Reading serial data directly via Python in a production setting introduces two major failure points:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;OS Timing Issues: Modbus RTU relies on strict timing gaps between data frames (typically 3.5 character times). Standard operating systems like Linux or Windows are not real-time OSs. If the CPU is busy and delays reading the serial buffer, the frame breaks, and your Python script throws a CRC error.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Electrical Isolation: The floors of factories are electrically loud. A normal USB-to-RS485 dongle does not provide galvanic isolation. The ground potential changes when a big machine nearby starts up. That spike goes all the way up the RS485 cable, crashes the USB driver on your server, and you have to unplug and plug it back in to fix it.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The Fix: Decoupling the Architecture&lt;/strong&gt;&lt;br&gt;
The most reliable way to read Modbus RTU in Python is to avoid dealing with the serial port entirely.&lt;/p&gt;

&lt;p&gt;Instead of letting your server handle the physical layer, decouple it using an industrial protocol gateway. You place a Modbus RTU-to-TCP converter (in my current setup, I use a &lt;a href="https://valtoris.com/product-center/serial-device-server/serial-to-ethernet/" rel="noopener noreferrer"&gt;Valtoris isolated Modbus gateway&lt;/a&gt;) right next to the physical sensors.&lt;/p&gt;

&lt;p&gt;The hardware gateway handles the strict RS485 timing, the electrical isolation, and the continuous polling. It then exposes that data over standard Ethernet as Modbus TCP.&lt;/p&gt;

&lt;p&gt;Your Python script no longer fights with ttyUSB0 or baud rates. It simply makes a standard TCP network request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Code: Switching to Modbus TCP&lt;/strong&gt;&lt;br&gt;
By shifting to TCP, the Python implementation becomes much cleaner and naturally handles network resilience. Here is the baseline structure using pymodbus:&lt;/p&gt;

&lt;p&gt;from pymodbus.client import ModbusTcpClient&lt;br&gt;
import time&lt;/p&gt;

&lt;h1&gt;
  
  
  Point this to the IP of your hardware gateway
&lt;/h1&gt;

&lt;p&gt;GATEWAY_IP = '192.168.1.100'&lt;br&gt;
GATEWAY_PORT = 502&lt;/p&gt;

&lt;p&gt;def poll_sensor():&lt;br&gt;
    # Use TCP Client instead of Serial&lt;br&gt;
    client = ModbusTcpClient(GATEWAY_IP, port=GATEWAY_PORT, timeout=3)&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;try:
    if not client.connect():
        print("Connection failed. Check network or gateway.")
        return

    # Read 2 registers from Slave ID 1, starting at address 0
    response = client.read_holding_registers(address=0, count=2, slave=1)

    if response.isError():
        print(f"Modbus Error: {response}")
    else:
        raw_data = response.registers
        print(f"Raw Registers: {raw_data}")

        # Basic parsing example
        temp = raw_data[0] / 10.0
        print(f"Temperature: {temp} °C")

except Exception as e:
    print(f"Exception caught: {e}")
finally:
    client.close()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;if &lt;strong&gt;name&lt;/strong&gt; == '&lt;strong&gt;main&lt;/strong&gt;':&lt;br&gt;
    while True:&lt;br&gt;
        poll_sensor()&lt;br&gt;
        time.sleep(5)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One Last Gotcha: Endianness&lt;/strong&gt;&lt;br&gt;
If your script connects but the data doesn't make sense (for example, your temperature says 18432 instead of 24.5), you haven't done anything wrong. There is a problem with the byte order.&lt;/p&gt;

&lt;p&gt;The Modbus protocol specification does not strictly define how 32-bit floats or long integers should be ordered across 16-bit registers. Different sensor manufacturers use different byte orders (Big-Endian vs. Little-Endian).&lt;/p&gt;

&lt;p&gt;If you come across this, don't try to figure out the bits by hand. To change the order of the bytes until the output matches what you expect, use the BinaryPayloadDecoder module that comes with pymodbus.&lt;/p&gt;

&lt;p&gt;from pymodbus.payload import BinaryPayloadDecoder&lt;br&gt;
from pymodbus.constants import Endian&lt;/p&gt;

&lt;h1&gt;
  
  
  Example: Decoding a 32-bit float
&lt;/h1&gt;

&lt;p&gt;decoder = BinaryPayloadDecoder.fromRegisters(raw_data, byteorder=Endian.Big, wordorder=Endian.Little)&lt;br&gt;
real_value = decoder.decode_32bit_float()&lt;/p&gt;

</description>
      <category>python</category>
      <category>modbus</category>
      <category>iot</category>
      <category>software</category>
    </item>
  </channel>
</rss>
