Project Overview & Hardware Specifications
When searching for practical tutorials for Raspberry Pi, most guides stop at blinking an LED or printing 'Hello World' to the console. Real-world embedded projects require reliable sensor polling, robust bus communication, and network telemetry. This build bridges that gap by wiring a Bosch BME280 environmental sensor to a Raspberry Pi via the I2C bus, then publishing compensated temperature, humidity, and barometric pressure data to an MQTT broker.
This guide targets the Raspberry Pi 5 (4GB variant) running Raspberry Pi OS (64-bit, Bookworm). The Pi 5's updated I2C controller and faster clock speeds require strict attention to pull-up resistor values and bus capacitance, which we will address in the wiring section.
Estimated Time: 45 minutes
Estimated Cost: $95 - $110 USD
Hardware Bill of Materials (BOM)
Do not substitute the BME280 breakout with a generic, unregulated clone. The Pi's GPIO operates strictly at 3.3V. Feeding 5V into the Pi's SDA/SCL pins will destroy the SoC. The Adafruit breakout specified below includes onboard level-shifting and a 3.3V LDO regulator, making it safe and reliable.
| Component | Exact Variant / Part Number | Key Specifications | Est. Price (2026) |
|---|---|---|---|
| Microcontroller | Raspberry Pi 5 (4GB) | BCM2712 SoC, 4GB LPDDR4X, 2x I2C buses | $60.00 |
| Sensor Breakout | Adafruit BME280 (PID: 2652) | I2C/SPI, 3.3V-5V logic, onboard 10k pull-ups | $19.95 |
| Power Supply | Official Raspberry Pi 27W USB-C PD | 5.1V / 5A, required for Pi 5 peripheral headroom | $12.00 |
| Wiring | 28 AWG Silicone Jumper Wires (F-F) | Low capacitance, 20cm length | $5.00 |
Wiring the I2C Bus: Pin Mapping and Pull-Up Considerations
The Raspberry Pi exposes its primary I2C bus (I2C1) on the 40-pin GPIO header. The BME280 breakout board defaults to I2C address 0x77 (or 0x76 if the address pad is bridged). We will use the default 0x77.
Pin Mapping Table
| Raspberry Pi 5 Pin (Physical) | GPIO / Function | BME280 Breakout Pin | Wire Color (Suggested) |
|---|---|---|---|
| Pin 1 | 3.3V Power | VIN (or 3Vo) | Red |
| Pin 6 | GND | GND | Black |
| Pin 3 | GPIO 2 (SDA1) | SDA | Blue |
| Pin 5 | GPIO 3 (SCL1) | SCL | Yellow |
The Raspberry Pi's internal I2C pull-up resistors are relatively weak (typically 1.8kΩ to 50kΩ depending on the SoC revision and configuration). The Adafruit 2652 breakout includes 10kΩ pull-up resistors on both SDA and SCL. This is sufficient for a short bus (< 30cm) with low capacitance. If you extend the wires beyond 50cm, the bus capacitance will exceed the I2C spec (400pF), causing signal degradation. For long runs, use an active I2C bus extender like the PCA9615.
Software Setup: Enabling I2C and Installing Dependencies
Before writing code, we must enable the I2C interface in the kernel and install the Python libraries. The Raspberry Pi 5 uses the boot/firmware/config.txt file for hardware configuration, but raspi-config remains the safest way to toggle interfaces.
- Open the terminal and run:
sudo raspi-config - Navigate to Interface Options > I2C and select Yes to enable it.
- Reboot the Pi:
sudo reboot - After rebooting, verify the I2C bus is active by running:
ls -l /dev/i2c-1. You should see a character device file. - Install the I2C tools to verify the sensor address:
sudo apt update && sudo apt install i2c-tools -y - Scan the bus:
i2cdetect -y 1. You should see77in the grid output.
Next, set up a Python virtual environment (best practice on Bookworm to avoid PEP 668 'externally managed environment' errors) and install the required libraries:
mkdir ~/env_monitor && cd ~/env_monitor
python3 -m venv venv
source venv/bin/activate
pip install adafruit-blinka adafruit-circuitpython-bme280 paho-mqtt
Note on Paho MQTT: As of 2024, Eclipse Paho MQTT Python released v2.0, which introduced breaking changes to callback signatures. The code below uses the modern CallbackAPIVersion.VERSION2 syntax to ensure forward compatibility.
The Python Script: Polling BME280 and Publishing to MQTT
This script initializes the I2C bus, reads the BME280 calibration registers via the Adafruit CircuitPython library, formats the data into a JSON payload, and publishes it to an MQTT broker every 10 seconds. It includes explicit error handling for I2C bus dropouts and network disconnections.
Target Board: Raspberry Pi 5 (4GB) | OS: Raspberry Pi OS 64-bit Bookworm
import time
import json
import sys
import board
import busio
import adafruit_bme280.advanced as adafruit_bme280
import paho.mqtt.client as mqtt
from paho.mqtt.enums import CallbackAPIVersion
# --- PIN & BUS DEFINITIONS ---
# Raspberry Pi I2C1 Bus (Physical Pins 3 and 5)
I2C_SDA_PIN = board.SDA
I2C_SCL_PIN = board.SCL
I2C_BUS_ID = 1
# --- MQTT CONFIGURATION ---
MQTT_BROKER = '192.168.1.50' # Replace with your local broker IP (e.g., Mosquitto)
MQTT_PORT = 1883
MQTT_TOPIC = 'home/environment/livingroom'
MQTT_QOS = 1
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code == 0:
print(f'[MQTT] Connected successfully to {MQTT_BROKER}')
else:
print(f'[MQTT] Connection failed with reason code: {reason_code}')
def init_sensor():
"""Initialize I2C bus and BME280 sensor with error handling."""
try:
i2c = busio.I2C(I2C_SCL_PIN, I2C_SDA_PIN)
# Adafruit library handles the 0x77 / 0x76 address detection automatically
sensor = adafruit_bme280.Adafruit_BME280_I2C(i2c)
sensor.sea_level_pressure = 1013.25 # Adjust for your local altitude
print('[I2C] BME280 initialized successfully.')
return sensor
except ValueError as e:
print(f'[I2C FATAL] BME280 not found on I2C bus. Check wiring and i2cdetect. Error: {e}')
sys.exit(1)
except Exception as e:
print(f'[I2C FATAL] Unexpected initialization error: {e}')
sys.exit(1)
def init_mqtt():
"""Initialize MQTT client using Paho v2.0 API."""
client = mqtt.Client(CallbackAPIVersion.VERSION2)
client.on_connect = on_connect
try:
client.connect(MQTT_BROKER, MQTT_PORT, 60)
client.loop_start() # Runs network loop in background thread
return client
except ConnectionRefusedError:
print(f'[MQTT FATAL] Connection refused by {MQTT_BROKER}:{MQTT_PORT}. Is the broker running?')
sys.exit(1)
except Exception as e:
print(f'[MQTT FATAL] Unexpected connection error: {e}')
sys.exit(1)
def main():
sensor = init_sensor()
mqtt_client = init_mqtt()
print('[MAIN] Starting telemetry loop. Press Ctrl+C to exit.')
try:
while True:
try:
# Read sensor data
temp_c = sensor.temperature
humidity = sensor.relative_humidity
pressure = sensor.pressure
# Build JSON payload
payload = {
'temperature_c': round(temp_c, 2),
'humidity_pct': round(humidity, 2),
'pressure_hpa': round(pressure, 2),
'timestamp': time.time()
}
# Publish to MQTT
result = mqtt_client.publish(MQTT_TOPIC, json.dumps(payload), qos=MQTT_QOS)
if result.rc == mqtt.MQTT_ERR_SUCCESS:
print(f'[TX] {payload}')
else:
print(f'[TX WARN] Publish failed with code: {result.rc}')
except OSError as e:
# Catches I2C bus dropouts (e.g., loose wire, EMI spike)
print(f'[I2C ERROR] Bus read failed: {e}. Retrying in 5s...')
time.sleep(5)
continue
time.sleep(10)
except KeyboardInterrupt:
print('\n[MAIN] Shutting down gracefully...')
mqtt_client.loop_stop()
mqtt_client.disconnect()
sys.exit(0)
if __name__ == '__main__':
main()
Debugging Common I2C and MQTT Failures
Embedded systems fail in predictable ways. When your telemetry stream stops or the script crashes on boot, do not guess. Follow this diagnostic hierarchy. These are the first three things to check when the build fails:
- Verify Physical Continuity: Use a multimeter in continuity mode to check the GND and 3.3V lines from the Pi header to the breakout board. A missing ground reference is the #1 cause of I2C ghosting.
- Run
i2cdetect -y 1: If the sensor doesn't show up as77or76, the kernel cannot see the hardware. No amount of Python debugging will fix a physical layer fault. - Check Broker Firewall Rules: If the Pi can ping the MQTT server but the connection is refused, check
ufw statusoriptableson the broker machine to ensure port 1883 is open.
Exact Error Strings and Ranked Causes
OSError: [Errno 121] Remote I/O errorContext: This occurs during
sensor.temperature reads.Ranked Causes:
- Bus Capacitance / Wire Length: Wires exceed 30cm, degrading the I2C clock edges. Fix: Shorten wires or add an I2C bus extender.
- Missing Pull-ups: Using a raw BME280 chip instead of a breakout board. Fix: Add 4.7kΩ resistors from SDA/SCL to 3.3V.
- Power Brownout: The Pi 5 is under-volted, causing the I2C controller to reset. Fix: Use the official 27W PD power supply.
FileNotFoundError: [Errno 2] No such file or directory: '/dev/i2c-1'Context: Occurs at
busio.I2C() initialization.Ranked Causes:
- I2C Interface Disabled: You forgot to enable it in
raspi-config. Fix: Runsudo raspi-configand enable I2C. - Wrong Bus ID: You are using a compute module or a Pi variant where I2C1 is mapped differently. Fix: Check
ls /dev/i2c*and update the bus ID.
paho.mqtt.client.ConnectionRefusedError: [Errno 111] Connection refusedContext: Occurs at
client.connect().Ranked Causes:
- Broker Not Running: Mosquitto service is stopped on the target machine. Fix: Run
sudo systemctl start mosquittoon the broker. - Wrong IP Address: Typo in the
MQTT_BROKERvariable. Fix: Ping the IP from the Pi terminal to verify routing. - Authentication Required: The broker requires a username/password, but the script doesn't provide one. Fix: Add
client.username_pw_set('user', 'pass')before connecting.
Extending and Simplifying the Build
Once the baseline telemetry is flowing, you can adapt this architecture to fit your specific constraints or expand its capabilities.
How to Simplify the Build
If you do not have an MQTT broker and simply want to log data for offline analysis, strip out the paho-mqtt dependency entirely. Replace the MQTT publish block with Python's built-in csv module:
import csv
# Inside the while loop:
with open('/home/pi/env_data.csv', mode='a', newline='') as f:
writer = csv.writer(f)
writer.writerow([time.time(), temp_c, humidity, pressure])
This reduces the script's memory footprint and eliminates network dependencies, making it ideal for battery-powered or isolated deployments.
How to Extend the Build
To integrate this sensor into a modern smart home ecosystem, extend the MQTT payload to support Home Assistant MQTT Discovery. By publishing a specific configuration JSON payload to the homeassistant/sensor/bme280/config topic on boot, Home Assistant will automatically detect the sensor and create the entities without manual YAML configuration.
For hardware extensions, add a 5V relay module to GPIO 17. You can program the Pi to subscribe to an MQTT command topic (e.g., home/environment/livingroom/humidifier/set). When the BME280 reports humidity dropping below 35%, the Pi toggles GPIO 17 HIGH, triggering the relay to switch on a humidifier. Ensure you use a flyback diode across the relay coil and a logic-level MOSFET (like the IRLZ44N) to drive the relay, as the Pi's GPIO pins can only source ~16mA safely.
For authoritative reference on I2C bus configuration and MQTT protocol standards, consult the Raspberry Pi I2C Documentation and the Eclipse Paho MQTT v2.0 Migration Guide. For sensor specifics, review the Adafruit BME280 Breakout Guide.






