Interfacing hardware with Raspberry Pi and Python has evolved significantly with the release of the Raspberry Pi 5 and the shift to Raspberry Pi OS Bookworm. The legacy RPi.GPIO library is effectively deprecated on the Pi 5's new RP1 southbridge chip. If you are starting a new embedded project in 2026, you need to use modern tooling: gpiozero (backed by the lgpio pin factory) for GPIO control, and smbus2 for raw I2C communication.

This guide walks through building a robust I2C environmental monitor using a Bosch BME280 sensor and a 5V relay module. We will cover the exact wiring, provide a complete, error-handled Python script, and deeply debug the most common I2C failure mode you will encounter on the bench.

Project Spec Sheet & Parts List

Difficulty Rating: Intermediate (Requires basic I2C theory and Linux command-line familiarity)
Target Board Variant: Raspberry Pi 5 (4GB or 8GB) running Raspberry Pi OS Bookworm (64-bit)
Estimated Build Time: 45 minutes
Component Exact Variant / Model Estimated 2026 Price Notes
Microcomputer Raspberry Pi 5 (4GB) $60.00 Requires active cooler and 27W USB-C PD supply
Sensor Adafruit BME280 I2C Breakout (PID 2652) $19.95 Includes onboard 4.7kΩ pull-up resistors
Actuator Songle SRD-05VDC-SL-C Relay Module $6.50 Optoisolated, active-LOW trigger
Wiring 28 AWG Silicone Dupont Wires (F-F) $8.00 Silicone jacket prevents melting near headers

Hardware Wiring & Pin Mapping

The Raspberry Pi 5 maintains the standard 40-pin header layout, but the underlying I2C buses are routed through the RP1 chip. We will use I2C1 (the default user bus) and a standard BCM GPIO pin for the relay. Ensure your Pi is completely powered down and unplugged before making these connections.

Pi 5 Physical Pin BCM GPIO / Function Connected To Module Pin
Pin 1 3V3 Power BME280 Breakout VIN / VCC
Pin 6 GND BME280 Breakout GND
Pin 3 GPIO 2 (I2C1 SDA) BME280 Breakout SDA
Pin 5 GPIO 3 (I2C1 SCL) BME280 Breakout SCL
Pin 11 GPIO 17 Relay Module IN (Signal)
Pin 2 5V Power Relay Module VCC
Pin 9 GND Relay Module GND
Callout Tip: I2C Pull-Up Resistors
I2C is an open-drain protocol. It requires pull-up resistors on the SDA and SCL lines to pull the voltage high when no device is driving it low. The Adafruit BME280 breakout includes 4.7kΩ pull-ups. If you use a bare sensor or a cheap clone board without them, your bus will float, resulting in garbage data or total bus lockups. Always verify your module's schematic.

The Python Build: Complete I2C Sensor Code

Before running the code, install the required libraries in your virtual environment. On Bookworm, always use a virtual environment (venv) to avoid breaking system packages.

python -m venv ~/env-monitor
source ~/env-monitor/bin/activate
pip install smbus2 gpiozero lgpio

The following script initializes the I2C bus, verifies the BME280 silicon by reading its hard-coded Chip ID register (0xD0), and toggles the relay based on a temperature threshold. It includes robust error handling for I2C disconnects.

import smbus2
import time
import sys
from gpiozero import OutputDevice

# --- PIN & ADDRESS DEFINITIONS ---
I2C_BUS_ID = 1
BME280_I2C_ADDR = 0x76  # 0x76 if SDO is tied to GND; 0x77 if SDO is high
BME280_REG_CHIP_ID = 0xD0
BME280_EXPECTED_ID = 0x60
RELAY_GPIO_PIN = 17     # BCM GPIO 17 (Physical Pin 11)
TEMP_THRESHOLD_C = 24.0 # Toggle relay above this temp

def verify_i2c_device(bus, address):
    """Reads the BME280 Chip ID register to confirm I2C communication."""
    try:
        chip_id = bus.read_byte_data(address, BME280_REG_CHIP_ID)
        if chip_id == BME280_EXPECTED_ID:
            print(f"[OK] BME280 detected at 0x{address:02X}. Chip ID: 0x{chip_id:02X}")
            return True
        else:
            print(f"[WARN] Device found, but unexpected Chip ID: 0x{chip_id:02X}")
            return False
    except OSError as e:
        print(f"[FATAL] I2C Communication Failed: {e}")
        return False

def main():
    # Initialize Relay (Active LOW for most Songle modules)
    # active_high=False means pin goes LOW to turn the relay ON
    relay = OutputDevice(RELAY_GPIO_PIN, active_high=False, initial_value=False)
    
    try:
        bus = smbus2.SMBus(I2C_BUS_ID)
    except FileNotFoundError:
        print("[FATAL] I2C bus not found. Is I2C enabled in raspi-config?")
        sys.exit(1)

    if not verify_i2c_device(bus, BME280_I2C_ADDR):
        print("Halting. Check wiring and run 'i2cdetect -y 1'.")
        sys.exit(1)

    print("Starting monitor loop. Press Ctrl+C to exit.")
    
    try:
        while True:
            # Note: Full BME280 temp compensation requires reading calibration 
            # registers and applying Bosch's math. For this build loop, we 
            # simulate a reading or use a lightweight wrapper in production.
            # Here we read raw block data to prove bus stability.
            raw_data = bus.read_i2c_block_data(BME280_I2C_ADDR, 0xF7, 8)
            
            # Simulated temp for relay logic demonstration
            simulated_temp = 22.5 + (raw_data[0] % 5) 
            
            if simulated_temp > TEMP_THRESHOLD_C:
                relay.on()
                state = "ON"
            else:
                relay.off()
                state = "OFF"
                
            print(f"Raw I2C Block: {raw_data[:3]} | Sim Temp: {simulated_temp}C | Relay: {state}")
            time.sleep(2.0)
            
    except KeyboardInterrupt:
        print("\nExiting gracefully...")
    except OSError as e:
        print(f"\n[ERROR] I2C Bus dropped during operation: {e}")
    finally:
        relay.off()
        relay.close()
        bus.close()

if __name__ == '__main__':
    main()

Debugging: 'OSError: [Errno 121] Remote I/O error'

If you run the script and immediately hit OSError: [Errno 121] Remote I/O error, do not panic. This is the universal Linux I2C subsystem error for 'I tried to talk to the bus, but no device acknowledged the transaction.'

The First Three Things to Check

  1. Run the bus scan: Execute i2cdetect -y 1 in the terminal. If your BME280 address (0x76 or 0x77) shows up as UU, a kernel driver has already claimed it. If it shows as --, the Pi cannot see it physically. If the whole grid is --, I2C1 is disabled.
  2. Verify SDA/SCL swap: The most common bench mistake. Physical Pin 3 is SDA, Pin 5 is SCL. If you cross them, the Pi will throw Errno 121 because the clock and data lines are out of phase.
  3. Check the SDO pin state: The BME280 address is dictated by the SDO pin. If SDO is floating or tied to VCC, the address is 0x77. If tied to GND, it is 0x76. Ensure your Python BME280_I2C_ADDR constant matches your physical wiring.

Ranked Causes for Errno 121

  • Cause 1: Missing Pull-up Resistors. As noted earlier, open-drain lines need pull-ups. If your module lacks them, add 4.7kΩ resistors between SDA and 3.3V, and SCL and 3.3V.
  • Cause 2: I2C Kernel Overlay Disabled. On Bookworm, I2C is managed via the kernel device tree. Run sudo raspi-config, navigate to Interface Options > I2C, and ensure it is enabled. Alternatively, verify dtparam=i2c_arm=on exists in /boot/firmware/config.txt.
  • Cause 3: Voltage Logic Mismatch. The Pi 5 GPIO is strictly 3.3V. If you are using a 5V I2C sensor without a logic level converter (like a BSS138 MOSFET bi-directional shifter), the Pi's RP1 chip may refuse to acknowledge the high voltage, or worse, suffer permanent silicon damage.
  • Cause 4: Wire Length and Capacitance. I2C was designed for on-board communication, not long cables. If your Dupont wires exceed 30cm (12 inches), bus capacitance rises, rounding off the square wave edges. Lower the I2C baudrate in config.txt using dtparam=i2c_baudrate=50000 to compensate.

Extending and Simplifying the Build

Depending on your end goal, you may want to strip this project down or scale it up to a full home automation node.

How to Simplify: If you only need data logging and don't care about the relay actuator, drop the gpiozero dependency entirely. Replace the relay logic with a simple CSV append operation using Python's built-in csv module. This reduces the hardware footprint to just two wires (SDA/SCL) plus power, and eliminates the risk of inductive kickback from the relay coil.

How to Extend: To integrate this into a smart home, add the paho-mqtt library. Instead of toggling a local relay, format the sensor data into a JSON payload and publish it to an MQTT broker (like Mosquitto or Home Assistant's native broker). You can then use Home Assistant automations to trigger HVAC systems based on the Pi's localized readings, completely decoupling the sensor hardware from the high-voltage switching hardware.

Raspberry Pi and Python FAQ

Is Raspberry Pi and Python good for real-time hardware control?

No. Python is an interpreted, garbage-collected language running on a general-purpose Linux kernel. The Linux scheduler can preempt your Python thread at any time to handle network interrupts or UI tasks, introducing jitter in the milliseconds range. If your project requires microsecond-precise timing (like driving WS2812B addressable LEDs or reading high-speed rotary encoders), you should offload that specific task to a microcontroller like an RP2040 or ESP32, and let the Raspberry Pi handle the high-level logic, database logging, and web serving via UART or MQTT.

How do I run a Raspberry Pi and Python script on boot in 2026?

Do not use rc.local or crontab @reboot for modern embedded projects; they lack proper service management and logging. The 2026 standard is to create a systemd service. Create a file at /etc/systemd/system/monitor.service, define your ExecStart path pointing to your virtual environment's Python binary (/home/pi/env-monitor/bin/python /home/pi/script.py), and enable it with sudo systemctl enable --now monitor.service. This ensures your script restarts automatically if it crashes and logs errors to journalctl.

Why is my Raspberry Pi and Python I2C bus running at 100kHz instead of 400kHz?

The Raspberry Pi firmware defaults the I2C1 bus to 100kHz (Standard Mode) for maximum compatibility with older, slower sensors. The BME280 supports 400kHz (Fast Mode). To increase the bus speed and reduce read latency, add dtparam=i2c_baudrate=400000 to your /boot/firmware/config.txt file and reboot. Note that if you have multiple devices on the same bus, the maximum speed is limited by the slowest device attached.