When makers search for things you can do with a raspberry pi, they are usually staring at a bare green board and wondering how to bridge the gap between a Linux computer and the physical world. While media centers and ad-blockers are fine, the true power of the Pi lies in its GPIO header. In this guide, we are bypassing the toy projects and building a production-grade IoT node: an MQTT Environment and Power Monitor. This node reads ambient temperature, humidity, and barometric pressure, while simultaneously monitoring the voltage and current draw of a 12V solar battery bank, publishing all telemetry to a local broker.
The Decision Path: Which Raspberry Pi Project Should You Build?
Not every project requires a full single-board computer. Before we wire up the GPIO, use this decision matrix to confirm that a Raspberry Pi is the right tool for your specific use case, rather than a simpler microcontroller.
| If your goal is... | And you need... | Then choose... |
|---|---|---|
| Simple 5V logic switching or basic sensor polling | Low power, instant boot, bare-metal C/MicroPython | ESP32 or Arduino Nano |
| Network-wide DNS sinkhole or Pi-hole | Headless Linux, Docker, minimal GPIO usage | Raspberry Pi Zero 2 W |
| Local media playback or retro emulation | HDMI out, USB controllers, heavy GPU rendering | Raspberry Pi 5 (8GB) |
| Bridging I2C/SPI sensors to cloud MQTT with local logging | Full Linux networking, Python ecosystem, robust I2C | Raspberry Pi 5 (4GB) [OUR PICK] |
Hardware Spec Sheet & Pin Mapping
This build targets the Raspberry Pi 5 (4GB variant) running Raspberry Pi OS Lite (64-bit, Bookworm). The Pi 5 operates its I2C bus at 3.3V logic, which perfectly matches our chosen sensor breakouts without needing a logic level converter.
Parts List
- Compute: Raspberry Pi 5 (4GB) with official 27W USB-C PD power supply.
- Environment Sensor: Adafruit BME280 I2C Breakout (Product ID: 2652). Measures temp, humidity, pressure.
- Power Monitor: Adafruit INA219 I2C High-Side DC Current Sensor Breakout (Product ID: 904). Measures bus voltage and shunt current up to 26V / 3.2A.
- Wiring: 4-pin JST-SH STEMMA QT cables or standard 28 AWG Dupont jumper wires.
- Storage: 32GB SanDisk Extreme microSD (A1 rated minimum for OS stability).
Pin Mapping Table (I2C Bus 1)
Both sensors share the same I2C bus. The Pi's internal pull-ups are sufficient for short runs (< 30cm), but if you extend the wires, add external 4.7kΩ pull-up resistors to the 3.3V line.
| Pi 5 GPIO Pin | Physical Pin # | Function | BME280 Pad | INA219 Pad |
|---|---|---|---|---|
| GPIO 2 | 3 | I2C1 SDA | SDI/SDA | SDA |
| GPIO 3 | 5 | I2C1 SCL | SCK/SCL | SCL |
| 3V3 Power | 1 | VCC (3.3V) | 3Vo | VCC |
| Ground | 6 | GND | GND | GND |
Note: The INA219 also has Vin+ and Vin- screw terminals. Connect these in series with the positive lead of the 12V battery bank you wish to monitor.
Step-by-Step Build & Software Configuration
Follow these numbered steps to prepare the OS and install the required Python libraries. We use adafruit-blinka to provide CircuitPython hardware API compatibility on standard Raspberry Pi OS.
- Flash the OS: Use Raspberry Pi Imager to flash Raspberry Pi OS Lite (64-bit) to your microSD card. Enable SSH and configure your Wi-Fi in the advanced settings (gear icon).
- Enable I2C: SSH into the Pi and run
sudo raspi-config. Navigate to Interface Options > I2C and enable it. Reboot the Pi. - Verify Hardware: After reboot, install I2C tools and scan the bus:
You should seesudo apt update && sudo apt install i2c-tools -y i2cdetect -y 140(INA219) and77(BME280 default) in the grid output. If you don't, check your wiring before proceeding. - Set up Python Environment: Create a virtual environment to keep dependencies clean.
sudo apt install python3-venv python3-pip -y mkdir ~/env_monitor && cd ~/env_monitor python3 -m venv venv source venv/bin/activate - Install Libraries: Install the Blinka compatibility layer, sensor drivers, and the Eclipse Paho MQTT client (Eclipse Mosquitto is the industry standard broker).
pip3 install adafruit-blinka adafruit-circuitpython-bme280 adafruit-circuitpython-ina219 paho-mqtt
Complete Python MQTT Monitor Code
This script initializes the I2C bus, configures the sensors, and publishes a JSON payload to an MQTT broker every 10 seconds. It includes robust error handling to prevent I2C bus lockups from crashing the daemon.
import time
import json
import board
import busio
import adafruit_bme280
from adafruit_ina219 import INA219, Range, Gain
import paho.mqtt.client as mqtt
# --- PIN & ADDRESS DEFINITIONS ---
I2C_SDA = board.SDA
I2C_SCL = board.SCL
BME_ADDR = 0x77 # Default for Adafruit BME280
INA_ADDR = 0x40 # Default for INA219
# --- MQTT CONFIGURATION ---
MQTT_BROKER = "192.168.1.100"
MQTT_PORT = 1883
MQTT_TOPIC = "pi5/sensors/env_power"
# Initialize I2C Bus
i2c = busio.I2C(I2C_SCL, I2C_SDA)
# Initialize Sensors
try:
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c, address=BME_ADDR)
bme280.sea_level_pressure = 1013.25
print("BME280 initialized successfully.")
except ValueError as e:
print(f"Failed to find BME280 at 0x{BME_ADDR:02X}: {e}")
exit(1)
try:
ina219 = INA219(i2c, address=INA_ADDR)
ina219.configure(Range.VOLT_16, Gain.AMP_400)
print("INA219 initialized successfully.")
except ValueError as e:
print(f"Failed to find INA219 at 0x{INA_ADDR:02X}: {e}")
exit(1)
# MQTT Callbacks
def on_connect(client, userdata, flags, reason_code, properties):
if reason_code == 0:
print(f"Connected to MQTT Broker at {MQTT_BROKER}")
else:
print(f"MQTT Connection failed with code: {reason_code}")
def on_disconnect(client, userdata, flags, reason_code, properties):
print(f"MQTT Disconnected (Code: {reason_code}). Attempting auto-reconnect...")
# Setup MQTT Client (Paho v2.0 API)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="Pi5_EnvNode")
client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.connect(MQTT_BROKER, MQTT_PORT, 60)
client.loop_start()
print("Starting telemetry loop...")
try:
while True:
payload = {}
# Read BME280 with error handling for I2C NACKs
try:
payload["temperature_c"] = round(bme280.temperature, 2)
payload["humidity"] = round(bme280.relative_humidity, 2)
payload["pressure_hpa"] = round(bme280.pressure, 2)
except OSError as e:
print(f"BME280 Read Error: {e}")
payload["bme280_error"] = str(e)
# Read INA219 with error handling
try:
payload["bus_voltage_v"] = round(ina219.bus_voltage, 2)
payload["current_ma"] = round(ina219.current, 2)
payload["power_mw"] = round(ina219.power, 2)
except OSError as e:
print(f"INA219 Read Error: {e}")
payload["ina219_error"] = str(e)
# Publish JSON payload
json_payload = json.dumps(payload)
client.publish(MQTT_TOPIC, json_payload)
time.sleep(10)
except KeyboardInterrupt:
print("\nShutting down gracefully...")
client.loop_stop()
client.disconnect()
Debugging: First Three Things to Check When It Fails
When moving from a breadboard prototype to a deployed node, I2C and network errors are inevitable. Here are the first three things to check when the script fails, ranked by occurrence frequency.
1. The I2C Bus Lockup
Exact Error String: OSError: [Errno 121] Remote I/O error
Ranked Causes:
- Loose Dupont Connections: Vibration causes momentary SDA/SCL disconnects. The Pi's I2C hardware controller does not automatically recover from a mid-byte clock stretch failure. Fix: Solder header pins or use JST connectors.
- Missing Pull-up Resistors: The Pi 5 internal pull-ups are around 50kΩ, which is too weak for long wire runs, causing slow rise times that the INA219 misinterprets. Fix: Add 4.7kΩ external pull-ups to 3.3V.
- Address Collision: You accidentally wired a second device to 0x77. Fix: Run
i2cdetect -y 1to verify unique addresses.
2. The Missing Interface
Exact Error String: FileNotFoundError: [Errno 2] No such file or directory: '/dev/i2c-1'
Ranked Causes:
- I2C Disabled in OS: You forgot to enable it in
raspi-configor thedtparam=i2c_arm=online is missing from/boot/firmware/config.txt. - Wrong Bus Number: The code is hardcoded to bus 1, but you are using a compute module or a Pi variant where the primary header I2C is mapped to bus 3 or 4. Fix: Check
ls /dev/i2c*and update the code.
3. The MQTT Broker Rejection
Exact Error String: ConnectionRefusedError: [Errno 111] Connection refused or reason_code == 5 (Connection Refused: not authorised)
Ranked Causes:
- Mosquitto Listener Config: Modern Mosquitto (v2.0+) defaults to local-loopback only. You must explicitly define
listener 1883andallow_anonymous trueinmosquitto.confto accept external Pi connections. - Firewall Rules: UFW on the broker host is blocking port 1883. Fix:
sudo ufw allow 1883/tcp. - Wrong IP: The DHCP lease for your broker expired and changed. Fix: Assign a static IP reservation in your router for the MQTT server.
Extending and Simplifying the Build
A good embedded design scales to your actual requirements. Here is how to adjust this build based on your deployment constraints.
How to Simplify (The Minimalist Node)
If you only care about server room ambient conditions and don't need power monitoring:
- Hardware: Drop the INA219 entirely. Switch to a Raspberry Pi Zero 2 W to cut BOM costs by roughly 70% and reduce idle power draw from ~2.5W to ~1.2W.
- Software: Replace the MQTT library with the
requestslibrary. Have the Pi Zero push a simple HTTP POST to a webhook (like a Home Assistant REST API endpoint) every 60 seconds. This eliminates the need to maintain a local MQTT broker entirely.
How to Extend (The Off-Grid Solar Sentinel)
If you are deploying this in a remote shed to monitor a solar setup:
- Hardware: Add a Pi Camera Module 3. Wire a 12V-to-5V buck converter (like the Pololu D24V50F5) to power the Pi directly from the battery bank you are monitoring.
- Software: Implement a watchdog trigger in the Python script. If the INA219 reports bus voltage dropping below 11.8V (indicating a deeply discharged lead-acid battery), trigger the Pi Camera to snap a photo of the physical charge controller LCD screen and publish it via MQTT base64 encoding. This gives you visual remote debugging when the telemetry data doesn't match the expected charge profile.
- Storage: Use the Pi 5's PCIe connector to add an NVMe SSD via the official M.2 HAT+. Log raw CSV data locally using Python's
sqlite3library so you don't lose telemetry when the Wi-Fi link drops during storms.
By treating the Raspberry Pi as a robust edge-compute node rather than just a desktop replacement, you unlock industrial-grade telemetry capabilities right from your workbench. Stick to the I2C pinouts, handle your bus errors gracefully, and your MQTT environment monitor will run for years without intervention.






