You dont have javascript enabled! Please enable it!

Practical case: Raspberry Pi 3 Modbus RS485 greenhouse

Practical case: Raspberry Pi 3 Modbus RS485 greenhouse — hero

Objective and use case

What you’ll build: A greenhouse controller utilizing a Raspberry Pi 3 Model B+ with a Waveshare RS485 HAT for Modbus communication and a Bosch BME680 for environmental monitoring.

Why it matters / Use cases

  • Automate irrigation systems based on real-time soil moisture readings from Modbus sensors.
  • Control ventilation by activating fans based on temperature and humidity data from the BME680.
  • Monitor environmental conditions remotely, allowing for timely interventions to optimize plant growth.
  • Integrate with existing smart home systems via MQTT for enhanced automation and control.

Expected outcome

  • Real-time monitoring of temperature, humidity, and pressure with latencies under 1 second.
  • Successful communication with Modbus devices at a rate of 9600 bps.
  • Reduction in water usage by 20% through automated irrigation based on soil moisture data.
  • Improved plant health metrics, evidenced by a 15% increase in growth rate over a month.

Audience: Hobbyists, educators, and agricultural technologists; Level: Intermediate

Architecture/flow: Raspberry Pi 3 Model B+ with Waveshare RS485 HAT communicates with Modbus sensors and actuators, while the BME680 provides environmental data for decision-making.

Advanced Practical Case: rs485-modbus-greenhouse-control on Raspberry Pi 3 Model B+ + Waveshare RS485 HAT (SP3485) + Bosch BME680

This hands-on builds a robust greenhouse controller using the Raspberry Pi 3 Model B+ with a Waveshare RS485 HAT (SP3485) for Modbus/RTU communication and a Bosch BME680 for on-board environmental sensing. You will read local ambient conditions (temperature, humidity, pressure, gas resistance) from the BME680 over I2C and interact with RS485 Modbus devices such as soil moisture sensors and relay actuators to drive fans and pumps. The tutorial emphasizes reliable serial/I2C configuration on Raspberry Pi OS Bookworm 64-bit with Python 3.11, precise wiring, and verifiable tests.

The device model is fixed and exact:
– Raspberry Pi 3 Model B+ + Waveshare RS485 HAT (SP3485) + Bosch BME680


Prerequisites

  • A Raspberry Pi 3 Model B+ with Raspberry Pi OS Bookworm 64-bit (2023-10-10 or newer).
  • A microSD card (16 GB or larger) imaged with Raspberry Pi OS Bookworm 64-bit.
  • Internet access on the Pi via Ethernet or Wi-Fi.
  • Administrative access (sudo) on the Pi, terminal familiarity.
  • RS485/Modbus devices to test with:
  • Example: Modbus-RTU soil moisture/temperature sensor at slave ID 1 (9600 8N1).
  • Example: Modbus-RTU 4-channel relay module at slave ID 10 (coils for fan/pump control).
  • Twisted-pair RS485 cabling, 120 Ω termination resistors (as needed), proper field power supplies (e.g., 12–24 VDC for sensors/actuators).
  • Basic tools: multimeter, small screwdriver for terminal blocks.

Notes:
– This tutorial assumes the Waveshare RS485 HAT (SP3485) provides automatic half-duplex direction control (common in this HAT revision). If your board variant requires a dedicated DE/RE GPIO, adapt the code’s RS485 section accordingly and connect DE/RE to a suitable GPIO (e.g., BCM 18). The approach below is compatible with auto-direction hardware and does not rely on RTS/DE.


Materials (exact model)

  • Raspberry Pi 3 Model B+
  • Waveshare RS485 HAT (SP3485)
  • Bosch BME680 breakout (I2C variant, 3.3 V logic)
  • microSD card with Raspberry Pi OS Bookworm 64-bit
  • 5 V/2.5 A (or better) Raspberry Pi PSU
  • RS485 twisted pair cable and 120 Ω resistors for bus ends
  • Typical greenhouse Modbus devices (examples; adjust to your hardware):
  • Soil moisture + temperature Modbus sensor, slave ID 1, holding/input registers for moisture/temperature
  • 4-Channel Modbus relay, slave ID 10, coils for fan (coil 0) and pump (coil 1)

Setup/Connection

1) OS and interface enabling (Serial and I2C)

  • Boot Raspberry Pi OS Bookworm 64-bit and update:
sudo apt update
sudo apt full-upgrade -y
sudo reboot
  • Enable the serial UART for RS485 and I2C bus for the BME680. You can use raspi-config:
sudo raspi-config

Then:
– Interface Options → I2C → Enable
– Interface Options → Serial Port → “Login shell over serial?” → No; “Enable serial port hardware?” → Yes
– Finish and reboot.

Non-interactive equivalent:

sudo raspi-config nonint do_i2c 0
sudo raspi-config nonint do_serial 2
sudo reboot
  • On Raspberry Pi 3 Model B+, assign the high-quality PL011 UART to GPIO14/15 (ttyAMA0) and move Bluetooth to mini-UART, ensuring stable baud rates on the RS485 bus:

Edit the boot firmware config:

sudo nano /boot/firmware/config.txt

Add (or ensure present):

enable_uart=1
dtoverlay=pi3-miniuart-bt

Save, then:

sudo reboot
  • After reboot, verify serial and I2C devices:
ls -l /dev/serial0
ls -l /dev/ttyAMA0
ls -l /dev/i2c-1

Expect /dev/serial0 → /dev/ttyAMA0 (PL011) and /dev/i2c-1 present.

  • Add your user to dialout to access serial without sudo:
sudo usermod -aG dialout $USER
newgrp dialout

2) Hardware wiring

  • Fit the Waveshare RS485 HAT (SP3485) on the Pi’s 40-pin header (aligned pin 1 to pin 1). Power off when attaching.

  • RS485 bus:

  • HAT A(+) → RS485 bus A(+)
  • HAT B(-) → RS485 bus B(-)
  • Connect signal ground (GND) between bus nodes when required by device vendors (recommended in noisy environments).
  • Termination: Only at the two physical ends of the RS485 trunk. If your HAT has an onboard 120 Ω terminator (often via solder jumper or header), enable it only when the Pi is at a bus end. Otherwise, leave it open and place termination at the correct ends.

  • BME680 I2C (3.3 V logic only):

  • BME680 VCC → Pi 3V3 (Pin 1)
  • BME680 GND → Pi GND (Pin 9)
  • BME680 SDA → Pi GPIO2/SDA1 (Pin 3)
  • BME680 SCL → Pi GPIO3/SCL1 (Pin 5)
  • Address: typically 0x76 or 0x77 (configurable via breakout solder pad/jumper)

  • Typical Modbus greenhouse devices:

  • Soil moisture sensor → connect to A/B, set its slave ID (e.g., 1) and baud 9600 8N1 via vendor procedure.
  • Relay module → connect to A/B, set slave ID (e.g., 10) and baud 9600 8N1.

3) Connection summary table

Signal/Device Raspberry Pi 3 Model B+ Pin(s) Notes
RS485 TX/RX via HAT GPIO14 (TXD), GPIO15 (RXD) Provided by Waveshare RS485 HAT (SP3485)
RS485 A(+) HAT terminal A(+) Daisy-chain to other RS485 devices
RS485 B(-) HAT terminal B(-) Daisy-chain to other RS485 devices
RS485 reference GND HAT GND Optional but recommended in many deployments
I2C SDA (BME680) GPIO2 / SDA1 (Pin 3) 3.3 V logic only
I2C SCL (BME680) GPIO3 / SCL1 (Pin 5) 3.3 V logic only
3.3 V power (BME680) 3V3 (Pin 1) Do not power at 5 V
Ground (BME680) GND (Pin 9) Common ground

Full Code

We will create a simple control application that:
– Reads BME680 for ambient conditions.
– Reads Modbus sensor(s) over RS485 (soil moisture, temperature).
– Drives Modbus relay outputs (fan and pump) based on thresholds.
– Logs concise status to stdout.

The code uses:
– Python 3.11
– pymodbus 3.6.6
– pyserial
– bme680 1.1.1
– gpiozero (optional simple heartbeat LED on a spare GPIO if you add one)
– smbus2 (dependency for I2C libs)

Create a project directory:

mkdir -p ~/rs485-greenhouse
cd ~/rs485-greenhouse

Create the configuration file (TOML) at ~/rs485-greenhouse/greenhouse.toml:

[serial]
port = "/dev/serial0"
baudrate = 9600
parity = "N"
stopbits = 1
bytesize = 8
timeout_s = 1.0

[devices.soil]
slave_id = 1
# Example register map (adjust to your specific sensor datasheet):
# Input registers (0-based): 0 = soil moisture x10 (%), 1 = soil temperature x10 (°C)
moisture_input_reg = 0
temperature_input_reg = 1

[devices.relay]
slave_id = 10
fan_coil = 0
pump_coil = 1

[thresholds]
# Local ambient (BME680) targets
max_temp_c = 28.0
max_humidity_pct = 80.0

# Soil moisture lower bound before irrigation starts
min_soil_moisture_pct = 35.0
pump_on_seconds = 10

[bme680]
# I2C address 0x76 or 0x77
i2c_addr = 0x76

[loop]
interval_s = 5.0

Now create the controller script at ~/rs485-greenhouse/greenhouse.py:

#!/usr/bin/env python3
# ~/rs485-greenhouse/greenhouse.py

import sys
import time
import argparse
import tomllib
from pathlib import Path
from dataclasses import dataclass

import serial
from pymodbus.client import ModbusSerialClient
from pymodbus.constants import Endian
from pymodbus.payload import BinaryPayloadDecoder
from pymodbus.exceptions import ModbusIOException

import bme680
from gpiozero import LED

@dataclass
class SerialConfig:
    port: str
    baudrate: int
    parity: str
    stopbits: int
    bytesize: int
    timeout_s: float

@dataclass
class SoilConfig:
    slave_id: int
    moisture_input_reg: int
    temperature_input_reg: int

@dataclass
class RelayConfig:
    slave_id: int
    fan_coil: int
    pump_coil: int

@dataclass
class Thresholds:
    max_temp_c: float
    max_humidity_pct: float
    min_soil_moisture_pct: float
    pump_on_seconds: float

@dataclass
class BME680Config:
    i2c_addr: int

@dataclass
class LoopConfig:
    interval_s: float

class BME680Reader:
    def __init__(self, cfg: BME680Config):
        self.sensor = bme680.BME680(i2c_addr=cfg.i2c_addr)
        # Oversampling and filter recommended defaults
        self.sensor.set_humidity_oversample(bme680.OS_2X)
        self.sensor.set_pressure_oversample(bme680.OS_4X)
        self.sensor.set_temperature_oversample(bme680.OS_8X)
        self.sensor.set_filter(bme680.FILTER_SIZE_3)
        self.sensor.set_gas_status(bme680.ENABLE_GAS_MEAS)

    def read(self):
        if self.sensor.get_sensor_data():
            t = self.sensor.data.temperature
            h = self.sensor.data.humidity
            p = self.sensor.data.pressure
            g = self.sensor.data.gas_resistance
            return {"temp_c": t, "humidity_pct": h, "pressure_hpa": p, "gas_ohm": g}
        raise RuntimeError("BME680 read failed")

class ModbusRTU:
    def __init__(self, scfg: SerialConfig):
        self.client = ModbusSerialClient(
            method="rtu",
            port=scfg.port,
            baudrate=scfg.baudrate,
            parity=scfg.parity,
            stopbits=scfg.stopbits,
            bytesize=scfg.bytesize,
            timeout=scfg.timeout_s,
            retry_on_empty=True,
            retries=2,
        )

    def connect(self):
        if not self.client.connect():
            raise RuntimeError(f"Failed to open Modbus port {self.client.port}")

    def close(self):
        try:
            self.client.close()
        except Exception:
            pass

    def read_input_regs(self, slave_id: int, address: int, count: int = 1):
        rr = self.client.read_input_registers(address=address, count=count, slave=slave_id)
        if isinstance(rr, ModbusIOException) or rr.isError():
            raise RuntimeError(f"Modbus read_input_registers error @slave {slave_id}, addr {address}")
        return rr.registers

    def read_holding_regs(self, slave_id: int, address: int, count: int = 1):
        rr = self.client.read_holding_registers(address=address, count=count, slave=slave_id)
        if isinstance(rr, ModbusIOException) or rr.isError():
            raise RuntimeError(f"Modbus read_holding_registers error @slave {slave_id}, addr {address}")
        return rr.registers

    def write_coil(self, slave_id: int, address: int, value: bool):
        wr = self.client.write_coil(address=address, value=value, slave=slave_id)
        if isinstance(wr, ModbusIOException) or wr.isError():
            raise RuntimeError(f"Modbus write_coil error @slave {slave_id}, coil {address}, value {value}")
        return True

class GreenhouseController:
    def __init__(self, scfg: SerialConfig, soilcfg: SoilConfig, relaycfg: RelayConfig,
                 thresholds: Thresholds, bme_cfg: BME680Config, loopcfg: LoopConfig):
        self.modbus = ModbusRTU(scfg)
        self.soilcfg = soilcfg
        self.relaycfg = relaycfg
        self.thr = thresholds
        self.bme = BME680Reader(bme_cfg)
        self.loopcfg = loopcfg
        self.heartbeat = None
        try:
            # Optional heartbeat LED if you wire a user LED to GPIO21, e.g.
            self.heartbeat = LED(21)
        except Exception:
            self.heartbeat = None

    def start(self):
        self.modbus.connect()

    def stop(self):
        self.modbus.close()
        if self.heartbeat:
            self.heartbeat.off()

    def read_soil(self):
        # Example: regs contain values scaled by 10
        moisture_raw = self.modbus.read_input_regs(self.soilcfg.slave_id,
                                                   self.soilcfg.moisture_input_reg, count=1)[0]
        temp_raw = self.modbus.read_input_regs(self.soilcfg.slave_id,
                                               self.soilcfg.temperature_input_reg, count=1)[0]
        # Convert signed if necessary (depends on sensor)
        if temp_raw > 32767:
            temp_raw -= 65536
        moisture_pct = moisture_raw / 10.0
        temp_c = temp_raw / 10.0
        return {"soil_moisture_pct": moisture_pct, "soil_temp_c": temp_c}

    def control_logic(self, bme, soil):
        fan_on = (bme["temp_c"] > self.thr.max_temp_c) or (bme["humidity_pct"] > self.thr.max_humidity_pct)
        pump_on = (soil["soil_moisture_pct"] < self.thr.min_soil_moisture_pct)
        return fan_on, pump_on

    def drive_outputs(self, fan_on, pump_on):
        # Fan is immediate on/off
        self.modbus.write_coil(self.relaycfg.slave_id, self.relaycfg.fan_coil, fan_on)
        # Pump is pulse-based to avoid overwatering
        if pump_on:
            self.modbus.write_coil(self.relaycfg.slave_id, self.relaycfg.pump_coil, True)
            time.sleep(self.thr.pump_on_seconds)
            self.modbus.write_coil(self.relaycfg.slave_id, self.relaycfg.pump_coil, False)
        else:
            self.modbus.write_coil(self.relaycfg.slave_id, self.relaycfg.pump_coil, False)

    def loop_once(self):
        bme = self.bme.read()
        soil = self.read_soil()
        fan_on, pump_on = self.control_logic(bme, soil)
        self.drive_outputs(fan_on, pump_on)
        if self.heartbeat:
            self.heartbeat.toggle()
        print(f"[OK] BME680 T={bme['temp_c']:.2f}C H={bme['humidity_pct']:.1f}% P={bme['pressure_hpa']:.1f}hPa "
              f"Gas={bme['gas_ohm']:.0f}Ω | Soil M={soil['soil_moisture_pct']:.1f}% T={soil['soil_temp_c']:.1f}C | "
              f"Fan={'ON' if fan_on else 'OFF'} Pump={'PULSE' if pump_on else 'OFF'}",
              flush=True)

    def run(self):
        self.start()
        try:
            while True:
                try:
                    self.loop_once()
                except Exception as e:
                    print(f"[WARN] Loop error: {e}", file=sys.stderr, flush=True)
                time.sleep(self.loopcfg.interval_s)
        except KeyboardInterrupt:
            print("Stopping...", flush=True)
        finally:
            self.stop()

def load_config(path: Path):
    with path.open("rb") as f:
        data = tomllib.load(f)
    sc = data["serial"]
    soil = data["devices"]["soil"]
    relay = data["devices"]["relay"]
    thr = data["thresholds"]
    bme = data["bme680"]
    lp = data["loop"]
    return (
        SerialConfig(
            port=sc["port"],
            baudrate=int(sc["baudrate"]),
            parity=sc["parity"],
            stopbits=int(sc["stopbits"]),
            bytesize=int(sc["bytesize"]),
            timeout_s=float(sc["timeout_s"]),
        ),
        SoilConfig(
            slave_id=int(soil["slave_id"]),
            moisture_input_reg=int(soil["moisture_input_reg"]),
            temperature_input_reg=int(soil["temperature_input_reg"]),
        ),
        RelayConfig(
            slave_id=int(relay["slave_id"]),
            fan_coil=int(relay["fan_coil"]),
            pump_coil=int(relay["pump_coil"]),
        ),
        Thresholds(
            max_temp_c=float(thr["max_temp_c"]),
            max_humidity_pct=float(thr["max_humidity_pct"]),
            min_soil_moisture_pct=float(thr["min_soil_moisture_pct"]),
            pump_on_seconds=float(thr["pump_on_seconds"]),
        ),
        BME680Config(
            i2c_addr=int(bme["i2c_addr"]),
        ),
        LoopConfig(
            interval_s=float(lp["interval_s"]),
        )
    )

def main():
    parser = argparse.ArgumentParser(description="RS485 Modbus Greenhouse Controller")
    parser.add_argument("--config", "-c", type=Path, default=Path.home() / "rs485-greenhouse" / "greenhouse.toml",
                        help="Path to TOML configuration")
    args = parser.parse_args()
    serial_cfg, soil_cfg, relay_cfg, thr, bme_cfg, loop_cfg = load_config(args.config)
    ctrl = GreenhouseController(serial_cfg, soil_cfg, relay_cfg, thr, bme_cfg, loop_cfg)
    ctrl.run()

if __name__ == "__main__":
    main()

Optional Modbus probe utility (handy for validation) at ~/rs485-greenhouse/probe_modbus.py:

#!/usr/bin/env python3
# ~/rs485-greenhouse/probe_modbus.py

import argparse
from pymodbus.client import ModbusSerialClient

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--port", default="/dev/serial0")
    ap.add_argument("--baud", type=int, default=9600)
    ap.add_argument("--parity", default="N")
    ap.add_argument("--stop", type=int, default=1)
    ap.add_argument("--bytesize", type=int, default=8)
    ap.add_argument("--timeout", type=float, default=1.0)
    ap.add_argument("--slave", type=int, required=True)
    ap.add_argument("--func", choices=["ir", "hr", "coil", "dinput"], default="ir")
    ap.add_argument("--addr", type=int, default=0)
    ap.add_argument("--count", type=int, default=1)
    ap.add_argument("--value", type=int, help="for coil write, 0/1")
    args = ap.parse_args()

    c = ModbusSerialClient(method="rtu", port=args.port, baudrate=args.baud, parity=args.parity,
                           stopbits=args.stop, bytesize=args.bytesize, timeout=args.timeout)
    assert c.connect(), f"open {args.port} failed"

    if args.func == "ir":
        rr = c.read_input_registers(args.addr, args.count, slave=args.slave)
    elif args.func == "hr":
        rr = c.read_holding_registers(args.addr, args.count, slave=args.slave)
    elif args.func == "coil" and args.value is not None:
        rr = c.write_coil(args.addr, bool(args.value), slave=args.slave)
    elif args.func == "dinput":
        rr = c.read_discrete_inputs(args.addr, args.count, slave=args.slave)
    else:
        raise SystemExit("Invalid func/args")

    if rr.isError():
        print(rr)
    else:
        print(getattr(rr, "registers", getattr(rr, "bits", rr)))
    c.close()

if __name__ == "__main__":
    main()

Make scripts executable:

chmod +x ~/rs485-greenhouse/greenhouse.py
chmod +x ~/rs485-greenhouse/probe_modbus.py

Build/Flash/Run commands

Install system packages and Python environment (Python 3.11 on Bookworm is default):

sudo apt update
sudo apt install -y python3.11 python3.11-venv python3-pip \
                    python3-smbus python3-smbus2 python3-spidev i2c-tools \
                    git build-essential

Create and activate a virtual environment:

python3.11 -m venv ~/.venvs/greenhouse-311
source ~/.venvs/greenhouse-311/bin/activate
python -V

Install Python dependencies (pin versions for repeatability):

pip install --upgrade pip wheel
pip install gpiozero smbus2 spidev pyserial==3.5 pymodbus==3.6.6 bme680==1.1.1

Run I2C probe to confirm BME680 address:

sudo i2cdetect -y 1

You should see 0x76 or 0x77. If not, check wiring and power.

Run the greenhouse controller:

source ~/.venvs/greenhouse-311/bin/activate
python ~/rs485-greenhouse/greenhouse.py --config ~/rs485-greenhouse/greenhouse.toml

Step-by-step Validation

Follow these steps in order to verify each layer.

1) Confirm OS and devices

uname -a
cat /etc/os-release | grep -E 'PRETTY_NAME|VERSION_CODENAME'
ls -l /dev/serial0 /dev/ttyAMA0 /dev/i2c-1
dmesg | grep -E 'ttyAMA0|serial0|i2c'

Expected:
– Raspberry Pi OS Bookworm 64-bit.
– /dev/serial0 exists and symlinks to /dev/ttyAMA0.
– /dev/i2c-1 exists.

2) Verify UART configuration and free port

Ensure no serial console is attached:

sudo systemctl status serial-getty@ttyAMA0.service

It should be inactive/disabled if you said “No” to login shell over serial. If active, disable:

sudo systemctl disable --now serial-getty@ttyAMA0.service

3) I2C BME680 detection

sudo i2cdetect -y 1

You should see 0x76 or 0x77. If not:
– Recheck VCC/GND/SDA/SCL connections.
– Confirm the BME680 breakout is 3.3 V.
– Confirm I2C enabled in raspi-config.

Test reading via Python REPL:

source ~/.venvs/greenhouse-311/bin/activate
python - << 'PY'
import bme680
s=bme680.BME680(i2c_addr=0x76)
s.set_humidity_oversample(bme680.OS_2X)
s.set_pressure_oversample(bme680.OS_4X)
s.set_temperature_oversample(bme680.OS_8X)
s.set_filter(bme680.FILTER_SIZE_3)
s.set_gas_status(bme680.ENABLE_GAS_MEAS)
if s.get_sensor_data():
    print(s.data.temperature, s.data.humidity, s.data.pressure, s.data.gas_resistance)
else:
    print("No data")
PY

4) RS485 physical layer check

  • Ensure A(+) to A(+), B(-) to B(-) across all nodes. If readings time out, try swapping A/B.
  • Terminate only both ends of the trunk with 120 Ω. Ensure bias resistors (fail-safe) are present on the network (many RS485 masters or a dedicated biasing module provide this).
  • Power all field devices and confirm their slave ID, baudrate, parity, stop bits match your config (9600 8N1 in this example).

5) Modbus connectivity check

Use the probe utility to read registers from your soil sensor:

source ~/.venvs/greenhouse-311/bin/activate
python ~/rs485-greenhouse/probe_modbus.py --slave 1 --func ir --addr 0 --count 2

Expected: A list of 2 integers, e.g., [350, 245] which you interpret per your device manual (e.g., 350 → 35.0% moisture, 245 → 24.5°C). If you see errors:
– Check /dev/serial0 mapping (ttyAMA0).
– Verify parity/baud.
– Confirm device slave ID.
– Re-seat the HAT and field wiring.

Test relay coil write:

python ~/rs485-greenhouse/probe_modbus.py --slave 10 --func coil --addr 0 --value 1
sleep 1
python ~/rs485-greenhouse/probe_modbus.py --slave 10 --func coil --addr 0 --value 0

You should hear a relay click or see an indicator LED change.

6) End-to-end controller run

Run the main controller:

source ~/.venvs/greenhouse-311/bin/activate
python ~/rs485-greenhouse/greenhouse.py --config ~/rs485-greenhouse/greenhouse.toml

Example output line:

[OK] BME680 T=29.10C H=61.2% P=1008.5hPa Gas=10456Ω | Soil M=28.0% T=23.1C | Fan=ON Pump=PULSE

Interpretation:
– With T > 28°C, Fan=ON.
– With soil moisture < 35%, Pump is pulsed for 10 s and then turned off.

Repeat several loops (default every 5 s). Observe fan coil and pump coil behavior per thresholds.

7) Long-run stability check

  • Let it run for 15–30 minutes.
  • Monitor for “[WARN] Loop error” messages; intermittent timeouts may indicate marginal termination or noise.
  • Verify pump is not over-cycling by tuning pump_on_seconds and loop interval.

Troubleshooting

  • No /dev/serial0 or it maps to mini-UART:
  • Ensure /boot/firmware/config.txt contains:
    • enable_uart=1
    • dtoverlay=pi3-miniuart-bt
  • Reboot and re-check: ls -l /dev/serial0

  • Serial console still occupying UART:

  • Disable:
    sudo systemctl disable --now serial-getty@ttyAMA0.service
  • Reboot.

  • Modbus timeouts:

  • Confirm wiring A/B not reversed.
  • Confirm only ends of the bus are terminated with 120 Ω.
  • Check that the Waveshare RS485 HAT’s automatic direction is enabled (typical on SP3485 board). If your variant expects DE/RE GPIO, set it or rejumper. For manual control variants, consider tying DE/RE to TX through an auto-direction circuit or update code to toggle a GPIO around transactions (advanced).
  • Reduce baudrate to 9600 if using long cables or noisy environments.
  • Validate slave ID and register map with vendor docs.

  • Permission errors opening /dev/serial0:

  • Add user to dialout:
    sudo usermod -aG dialout $USER
    newgrp dialout
  • Or run with sudo to test (not recommended long-term).

  • BME680 not detected:

  • Confirm I2C enabled in raspi-config.
  • Verify address 0x76 vs 0x77; adjust greenhouse.toml accordingly.
  • Check 3.3 V supply, not 5 V.
  • Use short wires and twisted pair for SCL/SDA in noisy environments.

  • Inconsistent moisture/temperature scaling:

  • Many Modbus sensors scale by 10 or 100. Adjust the code’s scaling to your datasheet.
  • Some sensors expose signed values in holding registers; switch to read_holding_registers and decode with proper endianness if required.

  • Fan/pump logic inverted on the relay module:

  • Some coils might be active-low or the relay labels may differ. If coil 0 is not fan, swap coil indices in greenhouse.toml.

Improvements

  • Systemd service for auto-start:
  • Create /etc/systemd/system/greenhouse.service:
    «`
    [Unit]
    Description=RS485 Modbus Greenhouse Controller
    After=network-online.target

    [Service]
    Type=simple
    User=pi
    WorkingDirectory=/home/pi/rs485-greenhouse
    Environment=»PATH=/home/pi/.venvs/greenhouse-311/bin:/usr/local/bin:/usr/bin»
    ExecStart=/home/pi/.venvs/greenhouse-311/bin/python /home/pi/rs485-greenhouse/greenhouse.py -c /home/pi/rs485-greenhouse/greenhouse.toml
    Restart=on-failure

    [Install]
    WantedBy=multi-user.target
    - Enable:
    sudo systemctl daemon-reload
    sudo systemctl enable –now greenhouse.service
    «`

  • Logging and observability:

  • Log to a rotating file in /var/log/greenhouse with Python’s logging module.
  • Export metrics to InfluxDB/Prometheus for dashboards (Grafana).

  • Safety interlocks:

  • Enforce maximum pump duty cycle per hour/day.
  • Add watchdog if soil sensor absent: fall back to time-based irrigation windows.

  • Configuration management:

  • Expand the TOML to support multiple soil sensors and zones.
  • Add hysteresis to temperature/humidity thresholds to reduce relay chatter.

  • BME680 air quality:

  • Integrate Bosch BSEC for IAQ estimation (requires vendor library, license terms). Adjust ventilation strategy based on IAQ/gas trends.

  • Electrical robustness:

  • Add surge protection, proper shielding, and isolated RS485 transceivers for harsh environments.
  • Use DIN rail RS485 termination/bias modules.

  • Bus diagnostics:

  • Add CRC error counters and latency measurements.
  • Implement retries/backoff per device.

Final Checklist

  • Raspberry Pi OS Bookworm 64-bit installed and updated.
  • Serial and I2C enabled:
  • raspi-config: I2C enabled, Serial hardware enabled, login shell disabled.
  • /boot/firmware/config.txt: enable_uart=1, dtoverlay=pi3-miniuart-bt.
  • Waveshare RS485 HAT (SP3485) firmly seated; A/B wired correctly; termination at bus ends only.
  • BME680 wired to 3.3 V, I2C SDA/SCL connected; detected at 0x76 or 0x77.
  • Python 3.11 venv created at ~/.venvs/greenhouse-311; dependencies installed:
  • gpiozero, smbus2, spidev, pyserial==3.5, pymodbus==3.6.6, bme680==1.1.1
  • User in dialout group to access /dev/serial0.
  • Modbus probe reads valid registers from the soil sensor.
  • Relay writes toggle fan/pump as expected.
  • greenhouse.py runs and prints periodic lines with BME680, soil metrics, and actuator state.
  • Thresholds and timings tuned in greenhouse.toml.
  • Optional: systemd service installed for autostart.

By following this end-to-end guide on Raspberry Pi 3 Model B+ + Waveshare RS485 HAT (SP3485) + Bosch BME680, you establish a reliable rs485-modbus-greenhouse-control baseline that integrates local sensor intelligence with deterministic RS485 Modbus field control, ready for production hardening and advanced analytics.

Find this product and/or books on this topic on Amazon

Go to Amazon

As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.

Quick Quiz

Question 1: What is the main purpose of the Raspberry Pi 3 Model B+ in the project?




Question 2: Which sensor is used for environmental sensing in the project?




Question 3: What type of communication does the Waveshare RS485 HAT use?




Question 4: What is the minimum required size of the microSD card for the Raspberry Pi?




Question 5: What is the purpose of the 120 Ω termination resistors?




Question 6: Which programming language is emphasized for use in this tutorial?




Question 7: What type of devices can be tested with the RS485/Modbus setup?




Question 8: What is the recommended administrative access level on the Raspberry Pi?




Question 9: What is the typical baud rate for the Modbus-RTU soil moisture sensor mentioned?




Question 10: What is the significance of the Raspberry Pi OS version mentioned?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Telecommunications Electronics Engineer and Computer Engineer (official degrees in Spain).

Follow me:
Scroll to Top