When you search for raspberry pi popular projects, you will find thousands of tutorials for smart mirrors, retro consoles, and media centers. But if you want to actually learn embedded hardware interfacing, the quintessential starter build is an environmental monitor paired with a relay controller. This project forces you to master the two most critical communication protocols on the Pi: I2C (for the BME280 sensor) and GPIO (for switching physical loads via relays).
This guide walks through building a climate-triggered relay controller using a Raspberry Pi 5 (4GB). We will wire the hardware, write production-ready Python with proper error handling, and deeply debug the specific I2C and GPIO errors that cause 90% of forum posts on this exact build.
Project Spec Sheet & Bill of Materials
Before wiring anything, verify your components. The most common point of failure in this build is using a 5V relay module with a Pi 5 without level shifting, which can backfeed 5V into the 3.3V GPIO pins and fry the SoC. We specify a 3.3V relay module below to eliminate this risk.
| Component | Exact Variant / Model | Est. Cost (2026) | Critical Notes |
|---|---|---|---|
| Microcontroller | Raspberry Pi 5 (4GB RAM) | $60.00 | Target board for this code. Pi 4 Model B is fully compatible. |
| Sensor | BME280 Breakout (I2C) | $12.00 | Must be I2C, not SPI. Default address is usually 0x76 or 0x77. |
| Relay Module | 4-Channel 3.3V Relay (Optocoupler) | $9.00 | MUST be 3.3V coil/opto. Standard 5V modules require logic level converters. |
| Wiring | 22 AWG Solid Core Hookup Wire | $8.00 | Pre-tinned ends prevent fraying in screw terminals. |
| Power Supply | 27W USB-C PD Power Supply (Official) | $25.00 | Pi 5 requires PD negotiation to enable full USB current limits. |
Pin Mapping & Wiring Procedure
We are using BCM (Broadcom) pin numbering, which is the standard for modern Python libraries like gpiozero. Physical pin numbers on the header are included for reference.
| Signal / Function | Pi BCM GPIO | Pi Physical Pin | Module Connection |
|---|---|---|---|
| I2C SDA | GPIO 2 | 3 | BME280 SDA |
| I2C SCL | GPIO 3 | 5 | BME280 SCL |
| 3.3V Power | N/A | 1 | BME280 VCC & Relay VCC |
| Ground | N/A | 6 | BME280 GND & Relay GND |
| Relay 1 (Fan) | GPIO 17 | 11 | Relay IN1 |
| Relay 2 (Heater) | GPIO 27 | 13 | Relay IN2 |
- De-energize: Unplug the USB-C power cable from the Raspberry Pi.
- Wire I2C: Connect Pi Pin 1 (3.3V) to BME280 VCC, Pin 3 (SDA) to SDA, Pin 5 (SCL) to SCL, and Pin 6 (GND) to GND.
- Wire Relays: Connect Pi GPIO 17 to Relay IN1, GPIO 27 to Relay IN2. Share the 3.3V and GND rails with the relay module's VCC and GND.
- Verify I2C Address: Power up the Pi, open terminal, and run
sudo i2cdetect -y 1. You should see76or77in the grid. Note this address for the code.
Compilable Python Code with Error Handling
This script targets the Raspberry Pi 5 (and Pi 4) using gpiozero for relay control and smbus2 for raw I2C communication. We read the BME280's Chip ID register (0xD0) to verify the connection, then read the uncompensated temperature register to trigger the relay.
Prerequisites: Run sudo apt update && sudo apt install python3-gpiozero python3-smbus2
import time
import sys
from gpiozero import OutputDevice
from smbus2 import SMBus
# --- PIN & I2C DEFINITIONS ---
RELAY_FAN_PIN = 17 # BCM GPIO 17
RELAY_HEAT_PIN = 27 # BCM GPIO 27
I2C_BUS_ID = 1 # /dev/i2c-1
BME280_ADDR = 0x76 # Change to 0x77 if i2cdetect shows 77
# BME280 Registers
REG_CHIP_ID = 0xD0
REG_TEMP_MSB = 0xFA
EXPECTED_CHIP_ID = 0x60 # BME280 returns 0x60; BMP280 returns 0x58
# Initialize Relays (Active LOW for most optocoupler modules)
# If your relay clicks ON when initialized, change to active_high=False
fan_relay = OutputDevice(RELAY_FAN_PIN, active_high=False, initial_value=False)
heat_relay = OutputDevice(RELAY_HEAT_PIN, active_high=False, initial_value=False)
def read_bme280_temp(bus, address):
"""Reads raw temperature data from BME280 (simplified for demonstration)."""
# Read 3 bytes of temperature data (MSB, LSB, XLSB)
data = bus.read_i2c_block_data(address, REG_TEMP_MSB, 3)
raw_temp = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
# Note: Real-world apps must apply factory calibration from registers 0x88-0x9F.
# This raw value is proportional to temp; we use a simplified threshold here.
return raw_temp
def main():
print("Initializing BME280 Climate Controller...")
try:
with SMBus(I2C_BUS_ID) as bus:
# 1. Verify Sensor Connection
chip_id = bus.read_byte_data(BME280_ADDR, REG_CHIP_ID)
if chip_id != EXPECTED_CHIP_ID:
print(f"Warning: Chip ID is 0x{chip_id:02X}. Expected 0x60 for BME280.")
else:
print(f"BME280 confirmed at I2C address 0x{BME280_ADDR:02X}")
# 2. Main Control Loop
while True:
raw_temp = read_bme280_temp(bus, BME280_ADDR)
# Simplified logic: if raw ADC value exceeds threshold, trigger fan
# (In production, apply calibration formulas from Bosch datasheet)
if raw_temp > 500000:
print(f"Temp high ({raw_temp}). Engaging fan.")
fan_relay.on()
heat_relay.off()
elif raw_temp < 450000:
print(f"Temp low ({raw_temp}). Engaging heater.")
fan_relay.off()
heat_relay.on()
else:
print(f"Temp nominal ({raw_temp}). Relays off.")
fan_relay.off()
heat_relay.off()
time.sleep(5)
except OSError as e:
if e.errno == 121:
print(f"CRITICAL I2C ERROR: {e}")
print("Fix: Check wiring, ensure I2C is enabled in raspi-config, and verify address.")
else:
print(f"Unexpected I2C OSError: {e}")
sys.exit(1)
except RuntimeError as e:
print(f"CRITICAL GPIO ERROR: {e}")
print("Fix: You are likely using legacy RPi.GPIO on a Pi 5. Switch to gpiozero.")
sys.exit(1)
except KeyboardInterrupt:
print("\nShutdown requested. Turning off relays...")
fan_relay.off()
heat_relay.off()
sys.exit(0)
if __name__ == "__main__":
main()
Debugging: The First Three Things to Check When It Fails
When building hardware projects, the code rarely runs perfectly on the first try. If your script crashes, look at the exact traceback. Here are the three most common failures and how to fix them.
1. The I2C Bus Failure
Exact Error String: OSError: [Errno 121] Remote I/O error
This is the most notorious error in Raspberry Pi popular projects involving sensors. It means the Linux kernel attempted to talk to the I2C bus, but the hardware did not acknowledge the address.
- Cause A (Most Likely): Wrong I2C address. The BME280 defaults to 0x76, but some manufacturers tie the SDO pin high, making it 0x77. Run
i2cdetect -y 1and updateBME280_ADDRin the code. - Cause B: SDA and SCL are swapped. I2C is not bidirectional on the wire level; SDA must go to SDA.
- Cause C: I2C interface is disabled. Run
sudo raspi-config, navigate to Interface Options > I2C, and enable it. Reboot.
2. The Pi 5 GPIO Compatibility Crash
Exact Error String: RuntimeError: Cannot determine SOC peripheral base address
If you copy-pasted an older tutorial from 2021, it likely uses the RPi.GPIO library. The Raspberry Pi 5 uses a completely new Southbridge chip (RP1), and the legacy RPi.GPIO library cannot map the memory addresses for the GPIO pins on the Pi 5.
- The Fix: Never use
RPi.GPIOfor new projects. As shown in our code block, usegpiozero. It automatically detects the Pi 5 and routes commands through thelgpiobackend seamlessly. Read the gpiozero documentation for migration paths.
3. The Relay Logic Inversion
Symptom: No Python error, but the relay clicks ON when the Pi boots, and turns OFF when you call relay.on().
- Cause: Most optocoupler relay modules are "Active LOW". They trigger when the GPIO pin pulls to ground (0V), not when it outputs 3.3V.
- The Fix: In the
gpiozeroinitialization, we explicitly setactive_high=False. If your specific module is Active HIGH, change that parameter toTrue.
Extending or Simplifying the Build
Once you have the baseline climate controller running, you can scale the project up or down depending on your end goal.
How to Extend (Smart Home Integration)
To make this a true IoT node, integrate MQTT. Install the paho-mqtt library (pip install paho-mqtt) and publish the calibrated temperature and humidity values to a Mosquitto broker. From there, Home Assistant can ingest the MQTT topics and handle the relay logic via its own automations, allowing you to strip the relay control out of the Python script entirely and use the Pi purely as a sensor gateway.
How to Simplify (Data Logging Only)
If you don't need physical switching and just want to log greenhouse data, drop the relay module entirely. Replace the relay logic in the while loop with Python's built-in csv module. Write a new row with a timestamp, temperature, pressure, and humidity every 60 seconds. This reduces the hardware BOM to just the Pi and the BME280, and eliminates all GPIO-related failure modes.
adafruit-circuitpython-bme280 library, which handles the math under the hood via Adafruit Blinka.






