When building reliable network Raspberry Pi projects for a home lab or small office rack, the Raspberry Pi 5’s upgraded I/O and PCIe lane make it a viable replacement for entry-level x86 micro-servers. However, moving from a simple desktop toy to a headless, always-on network hub requires rigorous power management, robust sensor integration, and bulletproof telemetry scripts.

This guide walks through building a rack-mounted MQTT broker and environmental monitor. We will wire a BME280 sensor to the Pi 5’s hardware I2C bus, configure Mosquitto MQTT with modern security defaults, and write a fault-tolerant Python script to publish telemetry. The code and configurations specifically target the Raspberry Pi 5 (8GB variant) running Raspberry Pi OS Bookworm 64-bit.

Project Overview and Hardware Specifications

A common failure point in embedded network hubs is power loss corrupting the SD card, or thermal throttling dropping network packets. This build solves both by integrating a UPS HAT and moving the OS to an NVMe drive via the Pi 5's PCIe 2.0 interface. The BME280 provides critical temperature, humidity, and barometric pressure data to ensure your server rack isn't quietly baking your equipment.

Difficulty Rating: Intermediate (Requires basic Linux CLI, I2C wiring, and Python debugging)
Estimated Build Time: 2 hours hardware, 1 hour software

Hardware Bill of Materials (2026 Pricing)

Component Exact Model / Variant Interface Est. Price
Compute Board Raspberry Pi 5 (8GB RAM) PCIe / 40-pin Header $80.00
Power Backup Waveshare UPS HAT (E) for Pi 5 I2C / pogo pins $38.00
Env. Sensor Adafruit BME280 Breakout I2C (Hardware) $19.50
Storage HAT Geekworm X1001 NVMe Shield PCIe 2.0 x1 $25.00
Storage Drive WD Blue SN580 1TB NVMe (2242) M.2 M-Key $65.00

Pin Mapping and Physical Wiring

The Raspberry Pi 5 routes its primary I2C bus through the new RP1 southbridge chip. Unlike the Pi 4, the Pi 5 includes onboard 1.8kΩ pull-up resistors for the primary I2C1 bus, meaning you generally do not need external pull-ups for short wire runs. However, you must ensure your sensor breakout is configured for I2C and not SPI.

BME280 to Pi 5 40-Pin Header Mapping

BME280 Pin Pi 5 Physical Pin BCM GPIO / Function Wire Color (Standard)
VIN (3-5V) Pin 1 3.3V Power Red
GND Pin 6 Ground Black
SCK (SCL) Pin 5 GPIO 3 (SCL1) Yellow
SDI (SDA) Pin 3 GPIO 2 (SDA1) Blue
CSB Not Connected Floating (Sets I2C Addr 0x77) N/A
SDO Not Connected Floating N/A
Callout Tip: Address Selection. The Adafruit BME280 defaults to I2C address 0x77. If you are using a generic Amazon/eBay breakout board, the address is often 0x76. You will need to adjust the BME280_ADDR variable in the Python script below accordingly. Always verify with i2cdetect -y 1 before running your code.

Software Stack and Configuration Steps

Before writing code, the underlying OS and broker must be configured. The biggest stumbling block in modern network Raspberry Pi projects is the Mosquitto 2.0 update, which changed default listener behavior to enhance security, breaking countless legacy tutorials.

  1. Flash and Boot: Flash Raspberry Pi OS Bookworm 64-bit (Lite) to your NVMe drive using Raspberry Pi Imager. Ensure you enable SSH and set a strong password in the Imager settings.
  2. Enable I2C: Run sudo raspi-config, navigate to Interface Options > I2C, and enable it. Reboot.
  3. Install Dependencies: Update your package manager and install the required Python and MQTT tools:
    sudo apt update && sudo apt install -y python3-pip python3-venv mosquitto mosquitto-clients i2c-tools
    python3 -m venv ~/hub-env
    source ~/hub-env/bin/activate
    pip install paho-mqtt smbus2 bme280
  4. Configure Mosquitto 2.0+ Listener: By default, Mosquitto 2.0+ only binds to localhost and requires authentication. For a local lab hub, we explicitly define the listener. Create a config file:
    sudo nano /etc/mosquitto/conf.d/local_hub.conf
    Paste the following:
    listener 1883 0.0.0.0
    allow_anonymous true
    Save, exit, and restart the service: sudo systemctl restart mosquitto.

For deeper configuration options on ACLs (Access Control Lists) and TLS encryption for production environments, refer to the official Mosquitto documentation.

Complete Python Telemetry Script

This script reads the BME280 sensor via the smbus2 and bme280 libraries, formats the data as a JSON payload, and publishes it to the local MQTT broker. It includes robust error handling for both I2C bus dropouts and network disconnects.

import time
import json
import paho.mqtt.client as mqtt
import smbus2
import bme280
import sys

# --- PIN & CONFIG DEFINITIONS ---
# BME280 is wired to Pi 5 Hardware I2C1 (Pin 3 = SDA1, Pin 5 = SCL1)
I2C_BUS_ID = 1
# Use 0x77 for Adafruit, 0x76 for most generic breakouts
BME280_I2C_ADDR = 0x77 

MQTT_BROKER_IP = "127.0.0.1"
MQTT_PORT = 1883
MQTT_TOPIC = "home_lab/rack/environment"
QOS_LEVEL = 1  # QoS 1 ensures at-least-once delivery
POLL_INTERVAL_SEC = 15

def on_connect(client, userdata, flags, rc, properties=None):
    if rc == 0:
        print(f"[MQTT] Connected to broker at {MQTT_BROKER_IP}")
    else:
        print(f"[MQTT] Connection failed with result code {rc}")

def main():
    # Initialize I2C Bus and Sensor Calibration
    try:
        bus = smbus2.SMBus(I2C_BUS_ID)
        calibration_params = bme280.load_calibration_params(bus, BME280_I2C_ADDR)
        print(f"[I2C] BME280 initialized at address {hex(BME280_I2C_ADDR)}")
    except Exception as e:
        print(f"[FATAL] I2C Initialization failed: {e}")
        sys.exit(1)

    # Initialize MQTT Client (Paho v2.0 API format)
    client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="pi5_rack_monitor")
    client.on_connect = on_connect
    
    try:
        client.connect(MQTT_BROKER_IP, MQTT_PORT, keepalive=60)
        client.loop_start()
    except Exception as e:
        print(f"[FATAL] MQTT Broker connection failed: {e}")
        sys.exit(1)

    print(f"[MAIN] Publishing to {MQTT_TOPIC} every {POLL_INTERVAL_SEC}s. Press Ctrl+C to exit.")

    try:
        while True:
            try:
                # Read Sensor Data
                data = bme280.sample(bus, BME280_I2C_ADDR, calibration_params)
                
                payload = {
                    "temp_c": round(data.temperature, 2),
                    "humidity": round(data.humidity, 2),
                    "pressure_hpa": round(data.pressure, 2),
                    "timestamp": int(time.time())
                }
                
                # Publish to MQTT
                result = client.publish(MQTT_TOPIC, json.dumps(payload), qos=QOS_LEVEL)
                result.wait_for_publish()
                print(f"[TX] {payload}")
                
            except OSError as ioe:
                print(f"[ERROR] I2C Read Fault: {ioe}. Check wiring.")
            except ValueError as ve:
                print(f"[ERROR] Sensor Data Corruption: {ve}.")
            except Exception as tx_err:
                print(f"[ERROR] MQTT Publish Fault: {tx_err}")
                
            time.sleep(POLL_INTERVAL_SEC)

    except KeyboardInterrupt:
        print("\n[MAIN] Shutting down gracefully.")
        client.loop_stop()
        client.disconnect()
        bus.close()

if __name__ == "__main__":
    main()

Debugging I2C and Network Failures

When working with embedded network hubs, hardware and software faults often mask each other. Below are the exact error strings Python will throw, ranked by their most likely root causes on the Pi 5 platform.

Error: OSError: [Errno 121] Remote I/O error

This is the most common I2C failure. It means the Pi's RP1 southbridge attempted to clock data, but the sensor did not acknowledge (NACK) the request.

  • Cause 1 (Most Likely): The BME280 I2C address is wrong. You are polling 0x77 but the board is hardcoded to 0x76.
  • Cause 2: Loose Dupont jumper wires on the SDA/SCL pins. The Pi 5 header pins can be slightly shorter than Pi 4; ensure a firm push.
  • Cause 3: The sensor breakout is in SPI mode. If the CSB (Chip Select) pin on your specific breakout is tied to GND instead of floating or pulled high, it disables the I2C interface.

Error: ConnectionRefusedError: [Errno 111] Connection refused

The Python script cannot reach the Mosquitto broker on port 1883.

  • Cause 1 (Most Likely): You skipped the Mosquitto 2.0+ listener configuration step, and the broker is silently ignoring external or unauthenticated requests.
  • Cause 2: The Mosquitto service crashed or isn't running. Check with systemctl status mosquitto.
  • Cause 3: A local firewall (like ufw) is blocking port 1883.

The First 3 Things to Check When It Fails

Before rewriting code or swapping parts, run this exact diagnostic sequence on the Pi 5 CLI:

  1. Verify I2C Hardware: Run i2cdetect -y 1. You must see a 76 or 77 in the grid. If the grid is empty, your wiring or sensor is dead. If you see UU, another driver has claimed the bus.
  2. Verify I2C Overlay: Run cat /boot/firmware/config.txt | grep i2c. Ensure dtparam=i2c_arm=on is present and uncommented.
  3. Verify MQTT Listener: Run mosquitto_sub -h 127.0.0.1 -t "#" -v in a separate terminal. If it immediately exits with a connection error, your Mosquitto config file is malformed or not loaded.

For a deeper understanding of the Pi 5's RP1 chip I/O architecture and edge cases with clock stretching, consult the official Raspberry Pi hardware documentation.

Extending or Simplifying the Build

Not every network Raspberry Pi project needs to be a rack-mounted enterprise clone. Here is how to scale this build up or down based on your actual requirements.

How to Simplify (The Desktop Node)

If you just need a remote room monitor and don't care about NVMe speeds or power outages:

  • Drop the NVMe and UPS HAT: Boot directly from a high-endurance microSD card (e.g., SanDisk High Endurance 64GB). This cuts the hardware cost by over 50%.
  • Use a Pi Zero 2 W: The code provided above is entirely compatible with the Pi Zero 2 W running Bookworm Lite. Just ensure you solder the 40-pin header to the Zero.

How to Extend (The Fleet Manager)

If you are building a fleet of network nodes across a large property:

  • Add Prometheus Exporter: Instead of just pushing to MQTT, wrap the Python script in a lightweight Flask/FastAPI web server to expose a /metrics endpoint. This allows Prometheus to scrape the Pi 5 natively for Grafana dashboards.
  • Home Assistant Integration: Modify the MQTT payload to include Home Assistant Discovery topics. By publishing a specific JSON config payload to homeassistant/sensor/pi5_rack/config, Home Assistant will automatically detect and create the temperature and humidity entities without manual YAML configuration.
  • Add a UPS Serial Monitor: The Waveshare UPS HAT (E) exposes battery telemetry via I2C address 0x2D. You can expand the Python payload dictionary to read registers 0x01 (Voltage) and 0x0A (Capacity), giving you remote visibility into your rack's battery backup health.