Why This Build Beats Basic Raspberry Pi Weekend Projects
Most raspberry pi weekend projects stall out at blinking LEDs, basic web servers, or reading a single analog sensor via an ADC. If you want a project that actually exercises the Pi’s hardware interfaces and teaches you robust embedded Python, you need to work with I2C multiplexing, environmental sensing, and inductive load switching.
This guide walks through building a smart greenhouse monitor using a Raspberry Pi 5 (8GB variant) running Raspberry Pi OS (Bookworm, 64-bit). We will interface a BME280 environmental sensor and a capacitive soil moisture probe over the I2C bus, and trigger a 5V relay to control a water pump. Unlike toy projects, this build includes production-style error handling for I2C bus lockups and hardware timeouts.
Hardware Spec Sheet & Sensor Selection
Choosing the right sensors is where most weekend builds fail. Resistive soil sensors corrode within weeks, and DHT22 sensors rely on bit-banged 1-Wire protocols that drop packets under heavy CPU load. We are using I2C-based capacitive sensing and a Bosch BME280 for rock-solid data.
| Sensor Module | Protocol | Default I2C Addr | Key Metric Accuracy | VCC Range | 2026 Street Price |
|---|---|---|---|---|---|
| BME280 (Adafruit 2652) | I2C / SPI | 0x77 | ±1.0°C, ±3% RH | 3.3V - 5V | $14.95 |
| STEMMA Soil Sensor (Adafruit 4026) | I2C (Seesaw) | 0x36 | Capacitive (No corrosion) | 3.3V - 5V | $7.50 |
| DHT22 / AM2302 (Legacy) | 1-Wire-ish | N/A (GPIO) | ±0.5°C, ±2% RH | 3.3V - 5V | $4.00 |
| Generic Resistive Soil | Analog | N/A (ADC) | Degrades in 2 weeks | 3.3V - 5V | $1.50 |
Note: Always buy the BME280, not the BMP280. The BMP280 lacks the humidity sensor. For a deep dive on the BME280 breakout, refer to the Adafruit BME280 Guide.
Wiring & Pin Mapping
The Raspberry Pi 5 uses a 3.3V logic level for its I2C bus. While the Pi 5 has internal 1.8kΩ pull-up resistors on SDA and SCL, keeping your I2C wire runs under 30cm (12 inches) is critical to avoid bus capacitance issues. Use 24 AWG stranded silicone wire for breadboarding.
| Pi 5 Pin (Physical / BCM) | Function | Target Module Pin | Wire Color |
|---|---|---|---|
| Pin 1 (3V3 Power) | VCC | BME280 VIN & Soil VIN | Red |
| Pin 3 (GPIO 2 / SDA) | I2C Data | BME280 SDI & Soil SDA | Blue |
| Pin 5 (GPIO 3 / SCL) | I2C Clock | BME280 SCK & Soil SCL | Yellow |
| Pin 6 (Ground) | GND | All Modules GND | Black |
| Pin 12 (GPIO 18) | PWM / Control | Relay Module IN | Green |
| Pin 4 (5V Power) | VCC (High Current) | Relay Module VCC | Orange |
- Power down the Pi 5 completely and disconnect the USB-C PSU.
- Wire the I2C shared bus first (SDA/SCL in parallel to both sensors).
- Connect the 5V Relay module. Warning: The relay module requires 5V for the coil, but the optocoupler input (IN pin) is 3.3V tolerant on most modern boards. Verify your specific relay board's optocoupler specs.
- Double-check VCC and GND. Swapping 5V and GND on the Pi 5 header will instantly destroy the PMIC and the board.
Python Control Code with I2C Error Handling
To interact with these sensors, we use the Adafruit Blinka ecosystem, which ports CircuitPython libraries to the Raspberry Pi's Linux environment. We also use gpiozero for the relay.
First, install the dependencies via your terminal:
sudo apt update
sudo apt install python3-gpiozero i2c-tools
pip3 install adafruit-circuitpython-bme280 adafruit-circuitpython-seesaw
Below is the complete, compilable Python script. It reads the soil moisture and ambient temperature, triggers the pump if the soil is dry, and includes robust try/except blocks to handle I2C bus lockups—a common failure mode in weekend builds.
import time
import board
import busio
import adafruit_bme280
from adafruit_seesaw.seesaw import Seesaw
from gpiozero import OutputDevice
import sys
# --- PIN & CONFIGURATION DEFINITIONS ---
RELAY_PIN = 18 # BCM GPIO 18 (Physical Pin 12)
PUMP_DURATION_SEC = 5 # How long to run the pump
SOIL_DRY_THRESHOLD = 2000 # Capacitive value (Lower = wetter, Higher = drier)
READ_INTERVAL_SEC = 30 # Time between sensor reads
# Initialize GPIO Relay (Active Low for most optocoupler relay boards)
pump_relay = OutputDevice(RELAY_PIN, active_high=False, initial_value=False)
# Initialize I2C Bus
i2c = busio.I2C(board.SCL, board.SDA)
def init_sensors():
"""Initialize I2C sensors with error handling for address conflicts."""
try:
bme = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=0x77)
# Seesaw chip handles the capacitive touch and temp on the STEMMA soil sensor
soil = Seesaw(i2c, addr=0x36)
print("[INFO] Sensors initialized successfully.")
return bme, soil
except ValueError as e:
print(f"[FATAL] I2C Address Error: {e}")
print("Check wiring and run 'i2cdetect -y 1' to verify addresses.")
sys.exit(1)
def read_and_act(bme, soil):
"""Read sensor data and trigger pump if necessary."""
try:
# Read BME280
temp_c = bme.temperature
humidity = bme.relative_humidity
# Read Soil Moisture (Capacitive touch returns 0-4095)
soil_moisture = soil.moisture_read()
soil_temp = soil.get_temp(0x30) # Seesaw internal temp
print(f"[{time.strftime('%H:%M:%S')}] Air: {temp_c:.1f}C | Hum: {humidity:.1f}% | "
f"Soil Moisture: {soil_moisture} | Soil Temp: {soil_temp:.1f}C")
# Automation Logic
if soil_moisture > SOIL_DRY_THRESHOLD:
print("[ACTION] Soil is dry. Triggering water pump...")
pump_relay.on()
time.sleep(PUMP_DURATION_SEC)
pump_relay.off()
print("[ACTION] Pump cycle complete.")
except OSError as e:
# Catches I2C Remote I/O errors (Errno 121) or bus lockups
print(f"[ERROR] I2C Bus Read Failure: {e}")
print("[WARN] Resetting I2C bus on next cycle...")
# In a production daemon, you would re-initialize the i2c bus object here
except Exception as e:
print(f"[ERROR] Unexpected failure: {e}")
if __name__ == "__main__":
bme_sensor, soil_sensor = init_sensors()
try:
while True:
read_and_act(bme_sensor, soil_sensor)
time.sleep(READ_INTERVAL_SEC)
except KeyboardInterrupt:
print("\n[INFO] Script terminated by user. Ensuring pump is OFF.")
pump_relay.off()
sys.exit(0)
Debugging I2C Failures: Exact Errors and Fixes
When working with I2C on the Pi 5, you will inevitably hit a bus error. Do not guess; use the exact error strings to diagnose the fault.
The "First Three Things to Check" Rule
Before rewriting code or swapping sensors, execute these three diagnostic steps:
- Verify Bus Enumeration: Run
sudo i2cdetect -y 1in the terminal. You must see36and77in the grid. If the grid is empty, your I2C interface is disabled inraspi-configor your SDA/SCL wires are swapped. - Check Voltage Levels: Use a multimeter to measure the voltage between Pin 1 (3V3) and Pin 6 (GND). It must read exactly 3.28V to 3.32V. If it reads 5V, you are probing the wrong rail and risk frying the sensors.
- Inspect Pull-Up Resistors: The Pi 5 has internal pull-ups, but if you are using wires longer than 30cm, signal edges degrade. Add external 4.7kΩ pull-up resistors between SDA and 3.3V, and SCL and 3.3V.
Common Exact Error Strings
ValueError: No I2C device at address: 0x77Ranked Causes:
1. The BME280 SDO pin is tied to GND, changing the address to 0x76. (Fix: Change
address=0x76 in code).2. The sensor is wired to the 5V rail but lacks a logic-level shifter, causing the I2C ACK to fail.
3. The sensor module is dead (check for physical damage or cold solder joints on the header).
OSError: [Errno 121] Remote I/O errorRanked Causes:
1. Bus capacitance is too high (wires too long), causing the Pi to miss the ACK bit. (Fix: Shorten wires or drop I2C speed to 50kHz via
/boot/firmware/config.txt).2. Another process (like a background logging daemon) is currently holding the I2C bus lock.
3. The sensor browned out during a pump relay switching event. (Fix: Add a 100µF decoupling capacitor across the sensor's VCC and GND).
FileNotFoundError: [Errno 2] No such file or directory: '/dev/i2c-1'Ranked Causes:
1. I2C is disabled in the OS. Run
sudo raspi-config, navigate to Interface Options -> I2C, and enable it. Reboot.2. You are running a custom kernel that lacks the
i2c-bcm2835 module loaded.
Scaling the Build: Simplify or Extend
Once your raspberry pi weekend projects baseline is stable, you need to decide whether to strip it down for reliability or scale it up for home automation.
How to Simplify (The "Set and Forget" Route)
If you just want data logging without the risk of water leaks from a stuck relay:
- Drop the Relay: Remove the pump circuit entirely. Use the Pi strictly as a datalogger.
- Log to CSV: Add Python's native
csvmodule to append readings to a local file every 5 minutes. - Use Cron: Instead of a continuous
while Trueloop, strip the loop out and use Linuxcronto run the script once every 10 minutes. This frees up CPU cycles and prevents I2C bus lockups from long-running Python memory leaks.
How to Extend (The Smart Home Route)
To turn this into a permanent fixture in your greenhouse:
- Add MQTT: Install
paho-mqttand publish the soil moisture and BME280 data to a local Mosquitto broker. This allows Home Assistant to ingest the data natively via MQTT Discovery. - Add Light Sensing: Daisy-chain a TSL2591 High Dynamic Range Digital Light Sensor (I2C address 0x29) to the same bus. Plants need PAR (Photosynthetically Active Radiation) data, and the TSL2591 provides excellent lux and IR readings.
- Hardware Watchdog: The Pi 5 includes a hardware watchdog timer. Enable it via
systemdto automatically reboot the Pi if the Python script hangs due to an unrecoverable I2C kernel panic.
For more details on configuring the Pi 5's underlying hardware interfaces, always refer to the official Raspberry Pi Configuration Documentation and cross-reference your physical GPIO layout with Pinout.xyz before applying power.






