Setting up Raspberry Pi as a mini PLC

In this tutorial, you’ll configure a Raspberry Pi running Raspberry Pi OS Lite with a static IP, install and prepare the necessary Python libraries (python3-pymodbus and python3-rpi.gpio), wire an HC-SR04 ultrasonic sensor (TRIG to GPIO 23, ECHO through a 5 V→3.3 V voltage divider to GPIO 24, plus 5 V and GND), then drop a ready-to-run Python script into /home/pi/pi_modbus_hcsr04.py that triggers the sensor, measures its echo pulse, and exposes the resulting distance (in centimeters) at holding register 0 over Modbus TCP on port 5020. You’ll wrap that script in a systemd service so it launches automatically at boot—after reboot, any Modbus master on your network can simply poll your Pi’s static IP on port 5020 to read real-time distance data.

Download Raspberry Pi Imager

Below is the link for Raspberry Pi Imager https://www.raspberrypi.com/software/

Below is the configuration:

Once imaged and loaded it is setup for DHCP by default.

Set Static IP Address

SSH into the Rpi with the dhcp address shown on the HDMI screen with the credentials from above.

To set the static IP address run this command:

nmtui

You may need to power cycle the unit after saving. Confirm the HDMI screen shows your static address.

Install and Update Packages

Make sure you are connecting via SSH on your static IP address.

  1. Update the Rpi
sudo apt update
sudo apt upgrade -y

You may be prompted for initrd.d, just press Enter.

  1. Cleanup packages to keep OS small.
sudo apt autoremove -y
  1. Install Python Packages
sudo apt install python3-pymodbus python3-rpi.gpio -y

Create script

  1. Run the following command to create the script:
sudo tee /home/sysadmin/pi_modbus_hcsr04.py > /dev/null << 'EOF'
#!/usr/bin/env python3
"""
pi_modbus_hcsr04.py

Reads an HC-SR04 ultrasonic sensor on GPIO23/24,
and serves the distance (cm) via Modbus TCP on port 502.
"""

from pymodbus.server import StartTcpServer
from pymodbus.datastore import ModbusSparseDataBlock, ModbusSlaveContext, ModbusServerContext
from pymodbus.device import ModbusDeviceIdentification
import RPi.GPIO as GPIO
import threading
import time

# ─── GPIO SETUP ────────────────────────────────────────────────────────────────
TRIG_PIN = 23
ECHO_PIN = 24

GPIO.setmode(GPIO.BCM)
GPIO.setup(TRIG_PIN, GPIO.OUT)
GPIO.setup(ECHO_PIN, GPIO.IN)
GPIO.output(TRIG_PIN, False)
time.sleep(2)  # Allow sensor to settle

def read_distance_cm():
    """Trigger the HC-SR04 and measure the echo pulse to compute distance in cm."""
    # Send 10 μs pulse on TRIG
    GPIO.output(TRIG_PIN, True)
    time.sleep(0.00001)
    GPIO.output(TRIG_PIN, False)

    # Wait for echo start
    while GPIO.input(ECHO_PIN) == 0:
        pulse_start = time.time()
    # Wait for echo end
    while GPIO.input(ECHO_PIN) == 1:
        pulse_end = time.time()

    pulse_duration = pulse_end - pulse_start
    # Speed of sound 34300 cm/s, divide by 2 for round trip
    dist_cm = (pulse_duration * 34300) / 2
    # Clamp between 0–400 cm and round
    return max(0, min(400, round(dist_cm)))

# ─── MODBUS DATA BLOCK ────────────────────────────────────────────────────────
class HCSR04Block(ModbusSparseDataBlock):
    def __init__(self, address):
        super().__init__({address: 0})
        self.address = address

    def getValues(self, address, count=1):
        """
        Called by pymodbus when a Modbus master reads holding registers.
        We sample the HC-SR04 on each read.
        """
        if address == self.address:
            self.values[self.address] = read_distance_cm()
        return [self.values.get(addr, 0) for addr in range(address, address + count)]

# Holding register 0 → distance in cm
BLOCK_ADDR = 0
block      = HCSR04Block(BLOCK_ADDR)
store      = ModbusSlaveContext(hr=block, zero_mode=True)
context    = ModbusServerContext(slaves=store, single=True)

# ─── DEVICE IDENTIFICATION ────────────────────────────────────────────────────
identity = ModbusDeviceIdentification()
identity.VendorName          = 'PiPLC'
identity.ProductName         = 'Pi Modbus TCP HC-SR04'
identity.ModelName           = 'HC-SR04-PLC'
identity.MajorMinorRevision  = '1.1'

# ─── SERVER THREAD ────────────────────────────────────────────────────────────
def run_modbus_tcp():
    StartTcpServer(context=context, identity=identity, address=("0.0.0.0", 502))

if __name__ == "__main__":
    thread = threading.Thread(target=run_modbus_tcp, daemon=True)
    thread.start()
    print("Modbus TCP slave running on port 502, serving HC-SR04 distance. Ctrl-C to exit.")
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        print("Cleaning up GPIO…")
        GPIO.cleanup()
EOF
  1. Make it executable:
sudo chmod+x pi_modbus_hcsr04.py
  1. Create systemd service file:
sudo tee /etc/systemd/system/pi-hcsr04.service > /dev/null << 'EOF'
[Unit]
Description=HC-SR04 ModbusTCP Service
After=network.target

[Service]
ExecStart=/usr/bin/python3 /home/pi/pi_modbus_hcsr04.py
Restart=always
User=pi
WorkingDirectory=/home/pi

[Install]
WantedBy=multi-user.target
EOF
  1. Reload systemd
sudo systemctl daemon-reload
  1. Enable and Start at Boot
# Enable the service so it starts on every boot
sudo systemctl enable pi-hcsr04.service

# Start it right now
sudo systemctl start pi-hcsr04.service
  1. Verify Operation
# Status
sudo systemctl status pi-hcsr04.service

# Logs
sudo journalctl -u pi-hcsr04.service -f

# Test Modbus
mbtget <rpi-add> 5020 1 0
  1. Tips
# If you ever update the Python script, restart the service:
sudo systemctl restart pi-hcsr04.service

# To disable auto-start:
sudo systemctl disable pi-hcsr04.service

Wiring HC-SR04 Sensor

   HC-SR04         Raspberry Pi
   -------         -------------
   VCC   ───────── 5 V
   GND   ───────── GND
   TRIG  ───────── GPIO23 (BCM)
   ECHO  ──[div]── GPIO24 (BCM)

← Back to home