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
As an Amazon Associate, I earn from qualifying purchases. If you buy through this link, you help keep this project running.



