Project Overview: I2C Climate & MQTT Node
If you are staring at a bare board wondering what to do with Raspberry Pi hardware, building a headless environmental telemetry node is the perfect intersection of hardware wiring, Linux system administration, and Python scripting. This project reads temperature, humidity, and barometric pressure via I2C and publishes the payload to an MQTT broker for integration with platforms like Home Assistant or Node-RED.
Build Specifications
- Target Board Variant: Raspberry Pi 4 Model B (4GB RAM, Rev 1.4) or Raspberry Pi 5 (4GB). The code and pinouts are identical for both.
- Operating System: Raspberry Pi OS (64-bit, Bookworm or newer).
- Difficulty Rating: Intermediate (requires basic Linux CLI and I2C bus knowledge).
- Estimated Time: 45 minutes.
Exact Parts List
| Component | Exact Variant / Part Number | Approx. Cost |
|---|---|---|
| Microcomputer | Raspberry Pi 4 Model B (4GB) or Pi 5 | $55 - $60 |
| Sensor | Adafruit BME280 I2C/SPI Breakout (Product ID: 2652) | $10 |
| Wiring | 28 AWG Silicone Jumper Wires (Male-to-Female) | $6 |
| Power Supply | Official Raspberry Pi 27W USB-C PD Power Supply (for Pi 5) or 15W (for Pi 4) | $12 |
Hardware Wiring & Pin Mapping
The Raspberry Pi's 40-pin header exposes the primary I2C bus (I2C1) on physical pins 3 and 5. The BME280 breakout board operates strictly at 3.3V logic, which perfectly matches the Pi's GPIO levels. Never connect 5V logic sensors directly to the Pi's SDA/SCL lines without a bidirectional logic level converter, or you risk frying the SoC's I2C pull-up resistors.
| Pi Physical Pin | BCM GPIO | Function | BME280 Breakout Pin |
|---|---|---|---|
| 1 | N/A | 3.3V Power | VIN / VCC |
| 6 | N/A | Ground | GND |
| 3 | GPIO 2 | I2C1 SDA | SDI / SDA |
| 5 | GPIO 3 | I2C1 SCL | SCK / SCL |
Software Setup & Compilable Python Code
Starting with Raspberry Pi OS Bookworm, Python environment management changed significantly due to PEP 668. You can no longer install packages globally via pip without breaking system dependencies. We will use a virtual environment.
1. Enable I2C and Install System Dependencies
Open your terminal and enable the I2C interface:
sudo raspi-config nonint do_i2c 0
sudo apt update
sudo apt install python3-smbus i2c-tools python3-venv -y
Verify the sensor is visible on the bus (default BME280 address is 0x77 or 0x76):
i2cdetect -y 1
2. Create Virtual Environment & Install Libraries
mkdir ~/climate_node && cd ~/climate_node
python3 -m venv venv
source venv/bin/activate
pip install smbus2 RPi.bme280 paho-mqtt
3. The Python Telemetry Script
Save the following code as mqtt_climate.py. This script includes explicit pin/bus definitions, handles I2C bus exceptions, and manages MQTT connection state.
#!/usr/bin/env python3
import time
import json
import smbus2
import bme280
import paho.mqtt.client as mqtt
import sys
# --- PIN & BUS DEFINITIONS ---
I2C_BUS_ID = 1 # Physical pins 3 (SDA) and 5 (SCL)
BME280_ADDRESS = 0x76 # Change to 0x77 if your breakout has the alternate addr
MQTT_BROKER_IP = "192.168.1.100"
MQTT_PORT = 1883
MQTT_TOPIC = "home/office/climate"
SAMPLE_INTERVAL_SEC = 30
# --- MQTT CALLBACKS ---
def on_connect(client, userdata, flags, rc, properties=None):
if rc == 0:
print(f"[MQTT] Connected to {MQTT_BROKER_IP}")
else:
print(f"[MQTT] Connection failed with code {rc}")
def on_publish(client, userdata, mid, rc, properties=None):
pass # Suppress publish logs to keep console clean
# --- MAIN EXECUTION ---
def main():
# Initialize I2C Bus
try:
bus = smbus2.SMBus(I2C_BUS_ID)
calibration_params = bme280.load_calibration_params(bus, BME280_ADDRESS)
print("[I2C] BME280 initialized successfully.")
except FileNotFoundError:
print("[FATAL] I2C bus not found. Did you enable I2C in raspi-config?")
sys.exit(1)
except OSError as e:
print(f"[FATAL] I2C Hardware Error: {e}")
sys.exit(1)
# Initialize MQTT Client (Paho v2.0+ API)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
client.on_connect = on_connect
client.on_publish = on_publish
try:
client.connect(MQTT_BROKER_IP, MQTT_PORT, 60)
client.loop_start()
except Exception as e:
print(f"[MQTT] Broker unreachable: {e}. Continuing in local-only mode.")
print(f"Starting telemetry loop (Interval: {SAMPLE_INTERVAL_SEC}s)...")
try:
while True:
try:
# Read Sensor Data
data = bme280.sample(bus, BME280_ADDRESS, calibration_params)
payload = {
"temperature_c": round(data.temperature, 2),
"humidity_pct": round(data.humidity, 2),
"pressure_hpa": round(data.pressure, 2),
"timestamp": time.time()
}
json_payload = json.dumps(payload)
print(f"[DATA] {json_payload}")
# Publish to MQTT
if client.is_connected():
client.publish(MQTT_TOPIC, json_payload, qos=1)
except OSError as e:
# Catching the infamous I2C dropout error
print(f"[ERROR] I2C Read Failure: {e}. Retrying next cycle.")
time.sleep(SAMPLE_INTERVAL_SEC)
except KeyboardInterrupt:
print("\n[SYSTEM] Shutting down gracefully...")
finally:
client.loop_stop()
client.disconnect()
bus.close()
print("[SYSTEM] I2C bus and MQTT disconnected.")
if __name__ == "__main__":
main()
Debugging: Fixing "OSError: [Errno 121] Remote I/O error"
When working with I2C on the Pi, you will inevitably encounter the following exact error string:
OSError: [Errno 121] Remote I/O error
This is a low-level kernel rejection indicating the SoC sent a clock pulse but received no ACKnowledge (ACK) bit back from the sensor. Here are the first three things to check when this occurs:
- Verify Kernel Module Loading: Run
lsmod | grep i2c. Ifi2c_devandi2c_bcm2835are missing, the interface is disabled or the overlay failed to load in/boot/firmware/config.txt. - Poll the Bus Address: Run
i2cdetect -y 1. If the grid is entirely empty, you have a wiring fault. If you seeUU, another process (like a system daemon) has already claimed the sensor. - Check Ground Continuity: Use a multimeter in continuity mode to test between the GND pin on the sensor breakout and the metal shield of the Pi's USB port. A floating ground will cause the I2C logic levels to drift, triggering Errno 121.
Ranked Causes for Persistent I2C Errors
| Rank | Cause | Fix |
|---|---|---|
| 1 | I2C interface disabled in OS | Run sudo raspi-config -> Interface Options -> I2C -> Enable. |
| 2 | Incorrect I2C Address in Code | BME280 breakouts default to 0x76 or 0x77. Check the silkscreen on your specific board and update the BME280_ADDRESS variable. |
| 3 | Parasitic Capacitance on SDA/SCL | Long jumper wires act as capacitors, rounding off the square clock waves. Keep I2C runs under 30cm, or add 4.7kΩ external pull-up resistors. |
| 4 | Insufficient Power Delivery | If the Pi's 3.3V rail sags under load, the sensor will brownout. Measure Pin 1 with a multimeter; it must read between 3.25V and 3.35V. |
Extending and Simplifying the Build
Depending on your deployment environment, you may need to scale this project up or down.
How to Simplify (Local Logging)
If you don't have an MQTT broker running and just want to log data for a science project or greenhouse monitor, strip out the paho-mqtt dependencies. Replace the publish block with Python's built-in csv module to append rows to a local data.csv file on the Pi's microSD card. This reduces network overhead and makes the node entirely self-contained.
How to Extend (Home Assistant Integration)
To make this node natively discoverable by Home Assistant, implement MQTT Discovery. Instead of publishing just to home/office/climate, publish a JSON configuration payload to homeassistant/sensor/office_climate/config. This tells Home Assistant exactly what the sensor is, its units of measurement, and the state topic to listen to, automatically generating dashboard entities without manual YAML configuration.
Frequently Asked Questions
What to do with Raspberry Pi when you don't have a monitor?
You can run the Pi "headless" by configuring it before the first boot. Flash the OS using the official Raspberry Pi Imager, click the gear icon (or OS Customisation menu), and enable SSH, set your WiFi credentials, and define your username/password. Once powered on, locate the Pi's IP address via your router's DHCP table and connect using ssh username@raspberrypi.local. For embedded nodes like this climate logger, headless operation is the standard deployment method.
What to do with old Raspberry Pi 2 or 3 boards in 2026?
While the Pi 2 and Pi 3 struggle with modern heavy web browsing, they are still excellent for low-overhead background tasks. A Raspberry Pi 3B+ is perfectly suited to run Pi-hole (network-wide ad blocking) or act as a dedicated local Mosquitto MQTT broker for your smart home devices. The Pi 2, with its limited 1GB RAM and slower CPU, is best repurposed as a lightweight OctoPrint server for 3D printer management or a basic digital signage driver using a lightweight framebuffer image viewer.
What to do with Raspberry Pi GPIO pins safely?
The golden rule of Pi GPIO is respecting the 3.3V logic limit and current constraints. Each GPIO pin can safely source or sink a maximum of 16mA, and the total current drawn from all 3.3V pins combined must not exceed 50mA. If you need to switch a 5V relay or a 12V solenoid, never connect it directly to the GPIO. Use a logic-level N-channel MOSFET (like the IRLZ44N) or an optocoupler to isolate the Pi's sensitive SoC from inductive kickback and higher voltage rails.






