When exploring diy projects with raspberry pi, environmental data logging is a staple. However, most online tutorials treat the I2C bus as a plug-and-play afterthought and ignore the realities of network dropouts. If you are deploying a sensor node in a greenhouse, server room, or attic, a script that crashes on a momentary I2C clock-stretching glitch or an MQTT broker timeout is useless.
This guide details a production-grade environmental logger using the Raspberry Pi 5 (8GB variant) running Raspberry Pi OS Bookworm (64-bit). We will interface a Bosch BME280 sensor via I2C, read temperature, humidity, and barometric pressure, and publish the telemetry to an MQTT broker using Eclipse Paho with robust error handling.
Component Spec Sheet & Pin Mapping
Before wiring anything, verify your exact hardware. The Pi 5 features a new power delivery architecture and a dedicated I2C bus for the RP1 southbridge, but the primary user-accessible I2C bus remains on the standard 40-pin header GPIO 2 and GPIO 3. Below is the exact bill of materials and the physical pin mapping required for this build.
| Component | Exact Model / Variant | Approx. Cost | Critical Specification |
|---|---|---|---|
| Microcontroller | Raspberry Pi 5 (8GB RAM) | $80.00 | Requires 5V/5A (27W) PD PSU for full peripheral current |
| Sensor Breakout | Adafruit BME280 I2C/SPI (PID: 2652) | $19.95 | Includes onboard 10kΩ I2C pull-up resistors to 3.3V |
| Power Supply | Official Pi 27W USB-C PD Power Supply | $12.00 | Must support 5V/5A PDO to prevent brownout warnings |
| Thermal Mgmt | Raspberry Pi Active Cooler | $5.00 | PWM controlled via RP1; required for sustained loads |
| Wiring | 28 AWG Stranded Jumper Wires | $6.00 | Keep I2C runs under 30cm to avoid bus capacitance issues |
Hardware Pin Mapping Table
The BME280 breakout operates strictly at 3.3V logic. Never connect the SDA/SCL lines to 5V tolerant pins on a different microcontroller without a level shifter, but the Pi 5's RP1 chip natively handles the 3.3V logic on the primary GPIO header.
| Pi 5 Physical Pin | BCM GPIO | Function | BME280 Breakout Pin |
|---|---|---|---|
| Pin 1 | N/A (Power) | 3.3VDC | VIN |
| Pin 6 | N/A (Ground) | GND | GND |
| Pin 3 | GPIO 2 | I2C1 SDA | SDI |
| Pin 5 | GPIO 3 | I2C1 SCL | SCK |
Assembly & I2C Bus Configuration
With the hardware mapped, follow these numbered steps to configure the Bookworm OS environment and verify the physical layer before writing any code.
- Wire the Breakout: Connect the four jumper wires between the Pi 5 40-pin header and the BME280 breakout as defined in the pin mapping table above. Ensure the breadboard power rails are continuous.
- Enable the I2C Interface: Open a terminal on your Pi 5. Run
sudo raspi-config, navigate to Interface Options > I2C, and select Yes to enable the ARM I2C interface. Reboot the Pi. - Install I2C Tools: Once rebooted, install the user-space I2C debugging tools by running
sudo apt update && sudo apt install i2c-tools -y. - Verify the Hardware Address: Run the bus scan command:
i2cdetect -y 1. You should see a single device appear at address0x76or0x77(Adafruit's PID 2652 defaults to 0x77). If the grid is empty, check your wiring. - Install Python Dependencies: Install the required libraries for I2C communication and MQTT publishing. Run:
sudo apt install python3-pip python3-venv -ypython3 -m venv logger_env && source logger_env/bin/activatepip install smbus2 RPi.bme280 paho-mqtt - Prepare the MQTT Broker: Ensure you have an MQTT broker (like Mosquitto) running on your network. If testing locally on the Pi, install it via
sudo apt install mosquitto mosquitto-clients -yand ensure the service is active.
The Python MQTT Logger Script
Below is the complete, production-ready Python script. It targets the Raspberry Pi 5, utilizes the smbus2 library for I2C transactions, and implements Eclipse Paho MQTT v2.0 API standards. Crucially, it includes try/except blocks to handle I2C read faults and network disconnects without crashing the daemon.
import time
import signal
import sys
import json
import smbus2
import bme280
import paho.mqtt.client as mqtt
# --- PIN & CONFIGURATION DEFINITIONS ---
I2C_BUS_ID = 1 # Pi 5 primary user I2C bus is /dev/i2c-1
BME280_ADDR = 0x77 # Adafruit PID 2652 default address
MQTT_BROKER = '192.168.1.50'
MQTT_PORT = 1883
MQTT_TOPIC = 'sensors/greenhouse/node_01'
READ_INTERVAL_SEC = 15
# Graceful shutdown handling
running = True
def signal_handler(sig, frame):
global running
print('\n[INFO] Shutdown signal received. Exiting safely...')
running = False
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# --- MQTT CALLBACKS (Paho v2 API) ---
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 code: {reason_code}')
def on_publish(client, userdata, mid, reason_code, properties):
# Optional: verify QoS 1/2 acknowledgments
pass
# --- MAIN EXECUTION ---
def main():
# Initialize I2C Bus
try:
bus = smbus2.SMBus(I2C_BUS_ID)
calibration_params = bme280.load_calibration_params(bus, BME280_ADDR)
print(f'[I2C] BME280 initialized at address {hex(BME280_ADDR)}')
except FileNotFoundError:
print('[FATAL] I2C bus not found. Is I2C enabled in raspi-config?')
sys.exit(1)
except OSError as e:
print(f'[FATAL] I2C Hardware Error: {e}. Check wiring and pull-ups.')
sys.exit(1)
# Initialize MQTT Client (Using Paho v2 Callback API)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id='Pi5_Logger_01')
client.on_connect = on_connect
client.on_publish = on_publish
try:
client.connect(MQTT_BROKER, MQTT_PORT, keepalive=60)
client.loop_start() # Non-blocking network loop
except Exception as e:
print(f'[FATAL] MQTT Broker unreachable: {e}')
sys.exit(1)
print('[SYSTEM] Logger started. Press Ctrl+C to stop.')
# Main Telemetry Loop
global running
while running:
try:
# Read Sensor Data
data = bme280.sample(bus, BME280_ADDR, calibration_params)
payload = {
'temp_c': round(data.temperature, 2),
'humidity': round(data.humidity, 2),
'pressure_hpa': round(data.pressure, 2),
'timestamp': time.time()
}
# Publish to MQTT (QoS 1 ensures delivery guarantee)
json_payload = json.dumps(payload)
result = client.publish(MQTT_TOPIC, json_payload, qos=1)
if result.rc != mqtt.MQTT_ERR_SUCCESS:
print(f'[WARN] MQTT publish failed with code: {result.rc}')
else:
print(f'[TX] {payload}')
except OSError as e:
# Catches I2C bus drops / clock stretching timeouts
print(f'[ERROR] I2C Read Fault: {e}. Retrying next cycle.')
except Exception as e:
print(f'[ERROR] Unexpected fault: {e}')
# Sleep in small increments to allow rapid signal handling
for _ in range(READ_INTERVAL_SEC * 10):
if not running:
break
time.sleep(0.1)
# Cleanup
client.loop_stop()
client.disconnect()
bus.close()
print('[SYSTEM] Resources released. Goodbye.')
if __name__ == '__main__':
main()client.loop_start() implementation. Using a background thread for the MQTT network loop prevents the script from hanging if your Wi-Fi drops or the broker temporarily rejects connections, which is a common failure point in DIY smart home nodes.Debugging Common I2C & MQTT Failures
Hardware interfaces rarely work perfectly on the first boot in real-world environments. When your script fails, do not guess. Look at the exact traceback string and follow the ranked causes below.
1. The 'First Three Things' Diagnostic Check
Before digging into code, run through this physical and network triage sequence:
- Verify I2C Bus Visibility: Run
i2cdetect -y 1. If the address (0x77) is missing, you have a physical layer problem (swapped SDA/SCL, broken jumper wire, or dead sensor). - Measure the 3.3V Rail: Use a multimeter to measure voltage between Pin 1 (3.3V) and Pin 6 (GND). The Pi 5's 3.3V regulator can sag below 3.1V if you are backpowering the board or drawing too much current from the 3.3V rail. The BME280 will brownout and drop off the I2C bus if VCC drops.
- Ping the MQTT Broker: Run
ping 192.168.1.50(or your broker IP). If it times out, your Python script will throw a connection refused error regardless of how perfect your code is.
2. Exact Error Strings & Ranked Causes
| Exact Error String | Ranked Causes (Most to Least Likely) | Fix / Action |
|---|---|---|
FileNotFoundError: [Errno 2] No such file or directory: '/dev/i2c-1' | 1. I2C interface disabled in OS. 2. Running on wrong OS kernel. | Run sudo raspi-config and enable I2C. Reboot. Verify dtparam=i2c_arm=on is in /boot/firmware/config.txt. |
OSError: [Errno 121] Remote I/O error | 1. SDA and SCL wires swapped. 2. Missing pull-up resistors. 3. I2C address mismatch. | Swap GPIO 2 and GPIO 3 wires. Verify breakout has pull-ups. Check i2cdetect for the correct hex address. |
ConnectionRefusedError: [Errno 111] Connection refused | 1. Mosquitto broker service stopped. 2. Firewall blocking port 1883. 3. Wrong IP in script. | Run sudo systemctl status mosquitto. Check UFW/iptables rules. Verify MQTT_BROKER IP constant. |
OSError: [Errno 110] Connection timed out | 1. I2C bus capacitance too high. 2. Sensor locked in clock-stretch. | Shorten I2C wires to <30cm. Add external 4.7kΩ pull-ups to 3.3V if wire run exceeds 50cm. |
Scaling the Build: Simplify or Extend
One of the greatest advantages of building diy projects with raspberry pi is the modularity of the ecosystem. Depending on your deployment environment, you may need to alter the complexity of this node.
How to Extend for Industrial or Remote Use
If you are moving this logger from a breadboard to a remote, off-grid greenhouse, Wi-Fi and standard I2C will fail you. Range and power become the bottlenecks.
- Add LoRaWAN: Swap the Wi-Fi MQTT publishing for a LoRaWAN HAT (like the Dragino LoRa/GPS HAT). You will need to write a Python wrapper for the RN2483 radio module to send the JSON payload over 915MHz (US) or 868MHz (EU) to a local gateway, dropping your power consumption to milliamps.
- Use RS485 for Long Wire Runs: Standard I2C fails at wire lengths over 1 meter due to parasitic capacitance. If your Pi 5 is in a shed and the sensor is 50 meters away in a greenhouse, use an I2C-to-RS485 extender module (like the PCA9615 or a generic MAX485 setup) to transmit the data differentially over twisted-pair CAT5 cable.
- Downgrade the Compute: The Pi 5 8GB is overkill for polling a single sensor. Once your Python script is debugged and stable, migrate the code to a Raspberry Pi Zero 2 W ($15). The GPIO pinout and I2C bus addresses are identical, but the Zero 2 W draws a fraction of the idle current, making solar-battery operation viable.
How to Simplify for Local Dashboards
If setting up an MQTT broker and integrating with Home Assistant feels like over-engineering for a simple desk thermometer, strip the network layer out entirely.
- Local SQLite Logging: Remove the Paho MQTT imports. Import Python's native
sqlite3library. Create a localenvironment.dbfile and insert the sensor dictionaries as rows. You can then point a local Grafana instance or a simple Flask web server directly at the SQLite database. - Direct CSV Export: For quick-and-dirty data analysis, open a CSV file in append mode (
with open('log.csv', 'a') as f:) and write the timestamped comma-separated values. This requires zero network configuration and guarantees data persistence even if your router dies.
By understanding the physical limitations of the I2C bus and implementing defensive coding practices against network dropouts, your Raspberry Pi environmental logger will transition from a weekend toy to a reliable, always-on data acquisition node.
References:
1. Raspberry Pi OS Configuration & I2C Documentation
2. Eclipse Paho MQTT Python Client Repository
3. Bosch BME280 Datasheet & I2C Timing Specifications






