Skip to Content

Introduction to Modbus and Raspberry PLC. Part 3

Write and read a coil over Modbus RTU in Python with pymodbus 3.16
February 21, 2024 by
Introduction to Modbus and Raspberry PLC. Part 3
Boot & Work Corp. S.L, Bernat Brunet Pedra

Complete guide: Modbus RTU and TCP with Python and Node-RED

To write and read a coil over Modbus RTU from a Raspberry PLC in Python, install pymodbus in a virtual environment, open the RS-485 port with ModbusSerialClient(port, framer=FramerType.RTU, ...), and call write_coil(address, value, device_id=...) (function code 05) and read_coils(address, count=1, device_id=...) (function code 01). The examples of this post were tested with pymodbus 3.16.1.

This is part 3 of the Modbus and Raspberry PLC series. Parts 1 and 2 use Node-RED, and this part and part 4 use Python. All the details, such as Modbus TCP, timeouts, addressing and Node-RED settings, are in the Modbus guide for Raspberry Pi PLCs.

PartProtocolTool
Part 1Modbus RTUNode-RED
Part 2Modbus TCPNode-RED
Part 3 (this post)Modbus RTUPython
Part 4Modbus TCPPython

What you need

  • A Raspberry PLC. It is the Modbus RTU master.
  • A Modbus RTU slave: any device with RS-485, or another Industrial Shields PLC working as a slave, for example an Arduino or ESP32 based PLC as in Modbus RTU with Arduino and ESP32 based PLCs.
  • RS-485 wiring: A+ to A+ and B- to B-, with termination at the ends of the bus.
  • The serial settings of the slave: address (device ID), baud rate, parity and stop bits. This post uses device ID 31 at 38400 bit/s, 8N1. Change them to match your slave.

Install pymodbus

Install pymodbus with pip in a virtual environment, and fix the version. Raspberry Pi OS Bookworm and later block pip install outside a virtual environment, and the version that apt installs depends on the OS release. pymodbus changes its API between versions, so the version decides if an example runs.

python3 -m venv ~/modbus-venv
source ~/modbus-venv/bin/activate
pip install "pymodbus[serial]==3.16.1"
ls /dev/ttySC*

The last command lists the serial ports. On the Raspberry PLC v3, and on the v4 without expansion, RS-485 is /dev/ttySC0 and /dev/ttySC1. On the v6, and on the v4 with expansion, it is /dev/ttySC2 and /dev/ttySC3. This post uses /dev/ttySC0.

Write a coil with Python (FC05)

Save this code as rtu_write_coil.py:

from pymodbus.client import ModbusSerialClient
from pymodbus.exceptions import ModbusException
from pymodbus.framer import FramerType

PORT = "/dev/ttySC0"  # RS-485 port, check the name for your PLC version
DEVICE_ID = 31        # Modbus address of the slave (1 to 247, never 0)
COIL = 0              # coil address (coil 00001 in the device manual is address 0)

client = ModbusSerialClient(
    port=PORT,
    framer=FramerType.RTU,
    baudrate=38400,
    bytesize=8,
    parity="N",
    stopbits=1,
    timeout=1,
)
if not client.connect():
    raise SystemExit(f"Cannot open {PORT}")

try:
    rr = client.write_coil(COIL, True, device_id=DEVICE_ID)  # FC05
    if rr.isError():
        print("Exception response:", rr)
    else:
        print(f"Coil {COIL} set to ON")
except ModbusException as exc:
    print("No valid answer:", exc)  # timeout, CRC error or wrong device ID
finally:
    client.close()
  1. Imports. ModbusSerialClient is the RTU master, FramerType selects the RTU frame, and ModbusException catches the cases where the slave does not answer.
  2. Port and slave. DEVICE_ID is the Modbus address of the slave. Address 0 is broadcast: slaves do not answer it.
  3. Client. Baud rate, data bits, parity and stop bits must be the same as in the slave. timeout=1 waits one second for each answer.
  4. Write. write_coil(COIL, True, device_id=DEVICE_ID) sends function code 05. Use False to turn the coil off.
  5. Two kinds of error. rr.isError() is an exception response: the slave answered but refused the request, for example because the address does not exist. ModbusException means that no valid answer came back.

Run it with the virtual environment active:

source ~/modbus-venv/bin/activate
python rtu_write_coil.py
Coil 0 set to ON
Raspberry PLC terminal: python rtu.py runs twice and writes the value 0 and then 1 to coil address 0
Output of the first version of the script on a Raspberry PLC: it writes 0 and then 1 to coil 0.

Write and read a coil in a loop (FC05 and FC01)

This version changes the coil every second and reads it back to check the value:

import time

from pymodbus.client import ModbusSerialClient
from pymodbus.exceptions import ModbusException
from pymodbus.framer import FramerType

PORT = "/dev/ttySC0"  # RS-485 port, check the name for your PLC version
DEVICE_ID = 31
COIL = 0

client = ModbusSerialClient(
    port=PORT,
    framer=FramerType.RTU,
    baudrate=38400,
    bytesize=8,
    parity="N",
    stopbits=1,
    timeout=1,
)
if not client.connect():
    raise SystemExit(f"Cannot open {PORT}")

state = True
try:
    while True:
        try:
            client.write_coil(COIL, state, device_id=DEVICE_ID)        # FC05
            rr = client.read_coils(COIL, count=1, device_id=DEVICE_ID)  # FC01
            if rr.isError():
                print("Exception response:", rr)
            else:
                print(f"Wrote {state}, read back {rr.bits[0]}")
        except ModbusException as exc:
            print("No valid answer:", exc)
        state = not state
        time.sleep(1)
except KeyboardInterrupt:
    pass
finally:
    client.close()
  • read_coils(COIL, count=1, device_id=DEVICE_ID) sends function code 01. The answer packs the coils in bytes, so rr.bits has 8 values even if you read one coil. The first one, rr.bits[0], is the coil you asked for.
  • The client opens the port once and keeps it open. Each cycle catches its own errors, so a missing answer does not stop the loop.
  • Press Ctrl+C to stop. The port is closed in finally.

Output of our test:

Wrote True, read back True
Wrote False, read back False
Wrote True, read back True
Wrote False, read back False
Raspberry PLC terminal: the write and read loop prints a failed first write, then reads True, False and True from coil 0
Capture of the first version on a Raspberry PLC: the first write fails, and then the coil alternates between True and False.

Errors with older examples

The first version of this post used ModbusSerialClient(method='rtu', ...) and the device ID as a positional argument. We ran it against a simulated slave with several pymodbus versions: it works with 3.0.2 and 3.6.9, and it fails with 3.7 and later.

ErrorCauseFix
unexpected keyword argument 'method'pymodbus 3.7 and later select the frame with framerframer=FramerType.RTU
unexpected keyword argument 'slave'pymodbus 3.10 and later call it device_iddevice_id=31
cannot import name 'ModbusSerialClient' from 'pymodbus.client'pymodbus 2.xInstall pymodbus 3 in a virtual environment
No response receivedThe slave does not answerDevice ID, baud rate, parity, A and B not swapped, termination
Exception response with code 02The coil address does not exist in the slaveThe coil table in the manual of the slave

What we tested. Python 3.13 and pymodbus 3.16.1 on Linux, with a simulated Modbus RTU slave (device ID 31, 38400 8N1) on a virtual serial link. Both scripts ran and gave the output shown above. The original code was run the same way with pymodbus 2.5.3, 3.0.2, 3.6.9, 3.7.4, 3.8.6 and 3.16.1. We did not run this update on Raspberry PLC hardware: the two screenshots come from the first version of the post.

Modbus RTU on a Raspberry PLC in a real plant. In the biogas plant case study, a Raspberry PLC 58 runs four process sections, and the meters with Modbus RTU go on one of its two RS-485 ports.

Read the biogas plant case studyOpen the complete Modbus guide

FAQ about Modbus RTU with Python

The manual says coil 00001. Which address do I use?

Address 0. pymodbus uses the address of the frame, which starts at 0, and many manuals number the coils from 1.

Why does read_coils return 8 values?

Because the answer carries the coils in whole bytes. pymodbus returns all the bits of those bytes. Use only the first count values.

Can I install pymodbus with apt?

You can, but you get the version of your OS release, which may have a different API from the examples. A virtual environment with a fixed version gives the same result on every PLC.

Can I use device ID 0?

Only for broadcast writes. All slaves execute them and none answers, so you cannot read with device ID 0.

How do I do the same over Ethernet?

With ModbusTcpClient and the IP address of the slave. See part 4: Modbus TCP and Python.

​Search in our Blog

Introduction to Modbus and Raspberry PLC. Part 3
Boot & Work Corp. S.L, Bernat Brunet Pedra February 21, 2024
Share this post

Looking for your ideal Programmable Logic Controller?

Take a look at this product comparison with other industrial controllers Arduino-based. 

We are comparing inputs, outputs, communications and other features with the ones of the relevant brands.

PLC Comparison