Hardware Bill of Materials and Sensor Comparison
Before wiring anything, you need to select the right sensor. Many hobbyists default to the BME280, but for true indoor air quality (IAQ) monitoring, the BME680's volatile organic compound (VOC) gas sensor is mandatory. Below is the exact parts list for this build, followed by a data-dense comparison to justify the component selection.Exact Parts List
- Compute: Raspberry Pi 5 (4GB) with active cooler and 27W USB-C PD power supply (~$85 total)
- Sensor: Pimoroni BME680 Breakout or Adafruit BME680 STEMMA QT (~$25)
- Wiring: 4x female-to-female DuPont jumpers (24 AWG silicone, max 15cm length to minimize I2C capacitance)
- Broker: Mosquitto MQTT running on a local NAS, Home Assistant server, or a secondary Pi Zero 2 W
Environmental Sensor Comparison Matrix
| Feature | Bosch BME680 | Bosch BME280 | Sensirion SHT40 | Sensirion SCD41 |
|---|---|---|---|---|
| Temp Accuracy | ±1.0 °C | ±1.0 °C | ±0.2 °C | ±0.4 °C |
| RH Accuracy | ±3 % | ±3 % | ±1.8 % | ±2 % |
| Gas / VOC | Yes (MOX) | No | No | No (CO2 only) |
| I2C Address | 0x76 or 0x77 | 0x76 or 0x77 | 0x44 | 0x62 |
| Avg Price (2026) | $22 - $28 | $10 - $14 | $8 - $12 | $28 - $35 |
| Best Use Case | IAQ & VOC tracking | Basic weather | Precision humidity | CO2 ventilation |
Pin Mapping and Physical Wiring
The Raspberry Pi 5's 40-pin header maintains the same I2C pinout as previous generations, but the 3.3V logic level is strictly enforced. Never connect a 5V I2C device to the Pi 5 without a bidirectional logic level converter; you will fry the BCM2712 SoC's GPIO bank.GPIO to BME680 Pin Mapping
| Pi 5 GPIO Pin (Physical) | BCM Name | BME680 Breakout Pin | Wire Color (Standard) |
|---|---|---|---|
| Pin 1 | 3V3 Power | VIN / 3V3 | Red |
| Pin 6 | GND | GND | Black |
| Pin 3 | GPIO 2 (SDA1) | SDA | Blue |
| Pin 5 | GPIO 3 (SCL1) | SCL | Yellow |
Wiring Steps
- De-energize: Unplug the Pi 5's USB-C power supply before touching the GPIO header.
- Connect Power: Route 3.3V from Physical Pin 1 to the sensor's VIN. The BME680 has an onboard 3.3V LDO, so feeding it 5V is technically possible on some breakouts, but feeding it 3.3V directly bypasses the LDO, reducing self-heating errors by up to 0.5 °C.
- Connect Ground: Route GND from Physical Pin 6 to the sensor's GND.
- Connect I2C Data: Connect SDA (Pin 3) and SCL (Pin 5). Keep these wires under 15cm. I2C was designed for on-board communication, not long cable runs. If you must run wires further, use a STEMMA QT / Qwiic cable which provides better shielding, or drop the I2C bus speed.
- Verify I2C Address: Power on the Pi, open a terminal, and run
i2cdetect -y 1. You should see77(or76depending on the breakout's jumper pad). If the grid is empty, check your physical connections.
Python MQTT Implementation
This script uses the Adafruit CircuitPython BME680 library (via Blinka) for sensor reads and the Eclipse Paho MQTT v2.0 client for publishing. It includes a mandatory burn-in bypass for the gas sensor and explicit exception handling for I2C and network timeouts.Prerequisites: Run sudo apt install python3-pip python3-venv, create a virtual environment, and install dependencies via pip install adafruit-circuitpython-bme680 paho-mqtt.
import time
import json
import board
import busio
import adafruit_bme680
import paho.mqtt.client as mqtt
from paho.mqtt.enums import CallbackAPIVersion
# --- PIN & CONFIGURATION DEFINITIONS ---
# Using default I2C1 pins on Raspberry Pi 5
I2C_SDA = board.SDA # Physical Pin 3 (GPIO 2)
I2C_SCL = board.SCL # Physical Pin 5 (GPIO 3)
MQTT_BROKER_IP = "192.168.1.50"
MQTT_PORT = 1883
MQTT_TOPIC = "home/lab/environment/bme680"
MQTT_QOS = 1 # QoS 1 ensures at-least-once delivery for sensor data
POLL_INTERVAL_SEC = 60
GAS_BURN_IN_HOURS = 4 # MOX gas sensor requires a burn-in period for baseline
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code == 0:
print(f"Connected to MQTT broker at {MQTT_BROKER_IP}")
else:
print(f"MQTT Connection failed with reason code: {reason_code}")
# Initialize I2C bus with a lower clock speed to prevent Pi 5 clock-stretching bugs
# Standard is 100kHz, dropping to 50kHz improves reliability on long wires
i2c = busio.I2C(I2C_SCL, I2C_SDA, frequency=50000)
try:
sensor = adafruit_bme680.Adafruit_BME680_I2C(i2c, address=0x77)
sensor.sea_level_pressure = 1013.25
print("BME680 initialized successfully.")
except ValueError as e:
print(f"FATAL: Could not find BME680 at address 0x77. Check wiring. Error: {e}")
exit(1)
# Initialize MQTT Client (Paho v2.0 API)
mqtt_client = mqtt.Client(CallbackAPIVersion.VERSION2, client_id="pi5_env_monitor")
mqtt_client.on_connect = on_connect
try:
mqtt_client.connect(MQTT_BROKER_IP, MQTT_PORT, keepalive=120)
mqtt_client.loop_start() # Non-blocking network loop
except ConnectionRefusedError:
print(f"FATAL: MQTT Broker at {MQTT_BROKER_IP} refused connection. Is Mosquitto running?")
exit(1)
burn_in_start = time.time()
try:
while True:
try:
temp_c = sensor.temperature
humidity = sensor.relative_humidity
pressure_hpa = sensor.pressure
gas_ohms = sensor.gas
# The MOX gas sensor reads low resistance when VOCs are high.
# Ignore gas readings during the initial burn-in period.
uptime_hours = (time.time() - burn_in_start) / 3600
gas_valid = uptime_hours >= GAS_BURN_IN_HOURS
payload = {
"temperature_c": round(temp_c, 2),
"humidity_pct": round(humidity, 2),
"pressure_hpa": round(pressure_hpa, 2),
"gas_ohms": round(gas_ohms, 0) if gas_valid else "burning_in",
"timestamp": int(time.time())
}
# Publish with QoS 1
result = mqtt_client.publish(MQTT_TOPIC, json.dumps(payload), qos=MQTT_QOS)
result.wait_for_publish()
print(f"Published: {payload}")
except OSError as e:
print(f"I2C Read Error: {e}. Retrying in 10s...")
time.sleep(10)
continue
except RuntimeError as e:
print(f"Sensor calculation error: {e}")
time.sleep(5)
continue
time.sleep(POLL_INTERVAL_SEC)
except KeyboardInterrupt:
print("\nShutting down gracefully...")
mqtt_client.loop_stop()
mqtt_client.disconnect()
print("MQTT disconnected. Exiting.")
Debugging: First Three Checks and Exact Error Strings
When your Raspberry Pi internet of things projects fail in production, it is almost always an I2C bus collision or a dropped network socket. If the script crashes or hangs, do not rewrite the code immediately. Perform these first three diagnostic checks.The First Three Things to Check
- Check I2C Bus Visibility: Run
i2cdetect -y 1. If the output shows all dashes (--), your Pi cannot see the sensor. If it showsUU, another process (like a rogue systemd service or Home Assistant container) has already claimed the I2C device handle. - Check Broker Status: SSH into your MQTT server and run
systemctl status mosquitto. Verify it is listening on port 1883 and hasn't crashed due to a full disk log. - Check for Power Brownouts: The Pi 5 is highly sensitive to voltage drops. If the USB-C PD supply sags below 4.8V under load, the SoC will throttle and drop I2C transactions. Run
dmesg | grep -i voltage. If you see "Under-voltage detected", replace your power supply or cable.
Exact Error Strings and Ranked Causes
OSError: [Errno 121] Remote I/O errorContext: Thrown during
sensor.temperature read.
- Cause 1 (Most Likely): I2C Clock Stretching Bug. The BCM2712 chip in the Pi 5 has a known silicon errata regarding I2C clock stretching. If the BME680 holds the SCL line low to process data, the Pi's I2C controller times out prematurely. Fix: Lower the I2C bus frequency to 50kHz or 25kHz in the
busio.I2C()initialization, as done in the code above. - Cause 2: Missing Pull-up Resistors. The Pi's internal pull-ups are ~50kΩ, which is too weak for the BME680's capacitance. Fix: Use a breakout board (like Pimoroni or Adafruit) that includes onboard 4.7kΩ pull-up resistors to 3.3V.
- Cause 3: Wire Length/Capacitance. Wires longer than 20cm act as capacitors, rounding off the square I2C clock waves. Fix: Shorten wires or add an I2C bus extender (like the PCA9615).
paho.mqtt.client.MQTTException: Invalid protocol version or ConnectionRefusedError: [Errno 111] Connection refusedContext: Thrown during
mqtt_client.connect().
- Cause 1: Paho v2.0 API Changes. If you are copying code from a 2023 tutorial, it likely uses Paho v1.6 syntax. Paho v2.0 requires the
CallbackAPIVersion.VERSION2enum in the client constructor. Fix: Update the constructor as shown in the provided script. - Cause 2: Mosquitto ACL / Listener Config. Modern Mosquitto (v2.0+) defaults to localhost-only and requires explicit listener definitions. Fix: Add
listener 1883 0.0.0.0andallow_anonymous true(or configure passwords) in yourmosquitto.conf.
Scaling Your Raspberry Pi Internet of Things Projects
Once your BME680 monitor is stable, you will inevitably want to scale the architecture. Here is how to extend the build for a full smart-home deployment, or simplify it if you realize a full Linux OS is overkill.How to Extend the Build
- Home Assistant MQTT Discovery: Instead of manually configuring entities in Home Assistant, modify the Python script to publish a configuration payload to the
homeassistant/sensor/bme680/configtopic. This allows Home Assistant to auto-discover the temperature, humidity, and VOC entities on boot. - Add an e-Ink Dashboard: Wire a 2.13" Waveshare e-Paper HAT via SPI. Because the BME680 uses I2C, there is no bus conflict. You can render local graphs using the
Pillowlibrary and update the display every 5 minutes without causing screen burn-in. - Prometheus Exporter: If you are monitoring a server rack or greenhouse, replace the MQTT publish block with a lightweight HTTP server using Flask or FastAPI to expose a
/metricsendpoint for Prometheus scraping.
How to Simplify the Build
If your goal is simply to get environmental data into Home Assistant and you do not need local data logging, camera integration, or heavy edge computing, drop the Raspberry Pi entirely. Switch to an ESP32-C3 SuperMini ($4) running ESPHome. The ESP32 handles WiFi reconnections, deep sleep, and MQTT publishing natively via YAML configuration, eliminating the need to manage Linux updates, Python virtual environments, and I2C clock-stretching bugs. Reserve the Raspberry Pi 5 for projects that require a camera, local LLM inference, or complex USB peripheral management.
For more details on configuring the Pi 5's hardware interfaces, refer to the official Raspberry Pi I2C documentation. For advanced MQTT network topology, consult the Eclipse Paho Python client docs.






