A dedicated Raspberry Pi command center moves your critical monitoring off fragile desktop widgets and onto a hardened, always-on kiosk. This build targets the Raspberry Pi 5 (8GB variant), leveraging its dual I2C buses and PCIe-gen-unlocked throughput to run a local PyQt6 dashboard. The system polls a BME280 environmental sensor via I2C, displays real-time temperature and humidity, and triggers a 5V GPIO relay to activate an exhaust fan when thermal thresholds are breached.
Time to Build: 2 hours (hardware) + 1 hour (software config)
Target Board: Raspberry Pi 5 8GB (Running Pi OS Bookworm 64-bit)
Hardware Spec Sheet & GPIO Pin Mapping
Before cutting wires, verify your bill of materials. The Pi 5 has stricter power delivery requirements than the Pi 4; using a legacy 15W USB-C supply will trigger USB current limiting and cause peripheral brownouts under load. You must use a 27W USB-C PD supply.
| Component | Exact Model / Variant | Interface | Est. Cost (2026) | GPIO / Pins Used |
|---|---|---|---|---|
| Compute Board | Raspberry Pi 5 (8GB RAM) | N/A | $80.00 | 40-pin header |
| Display | Waveshare 5" DSI LCD (800x480) | MIPI DSI | $45.00 | 15-pin DSI ribbon |
| Sensor | Adafruit BME280 Breakout | I2C (Addr 0x77) | $19.95 | GPIO 2 (SDA), GPIO 3 (SCL) |
| Actuator | Songle SRD-05VDC-SL-C Relay Module | GPIO Digital Out | $6.50 | GPIO 17 (IN), 5V, GND |
| Power Supply | Official Pi 27W USB-C PD (White) | USB-C PD 5V/5A | $12.00 | USB-C Power In |
Wiring Pinout Table
The Pi 5 maintains backward compatibility with the standard 40-pin header for I2C1, but be aware that the EEPROM/ID pins on the DSI ribbon must be seated flush to allow the firmware to load the correct display overlay at boot.
| Sensor / Module Pin | Pi 5 Physical Pin | Pi 5 GPIO / Function | Wire Color (Recommended) |
|---|---|---|---|
| BME280 VIN | Pin 1 | 3.3V Power | Red |
| BME280 GND | Pin 6 | Ground | Black |
| BME280 SCK (SDA) | Pin 3 | GPIO 2 (I2C1 SDA) | Blue |
| BME280 SDI (SCL) | Pin 5 | GPIO 3 (I2C1 SCL) | Yellow |
| Relay VCC | Pin 2 | 5V Power | Red |
| Relay GND | Pin 9 | Ground | Black |
| Relay IN | Pin 11 | GPIO 17 | Green |
Assembly & Bookworm OS Configuration
- Flash the OS: Use Raspberry Pi Imager to flash Raspberry Pi OS (64-bit) Bookworm to a high-endurance microSD card (e.g., SanDisk High Endurance 64GB). In the advanced settings (Ctrl+Shift+X), enable SSH, set your username/password, and configure WiFi.
- Connect the DSI Display: With the Pi powered off, lift the black retaining collar on the DSI port. Insert the 15-pin ribbon cable with the metal contacts facing inward (towards the USB ports). Push the collar down to lock.
- Wire the I2C Sensor: Connect the BME280 to Physical Pins 1, 3, 5, and 6. Bench note: The Adafruit BME280 includes onboard 3.3V pull-up resistors. If you use a generic clone board lacking pull-ups, I2C will fail silently or throw intermittent errors; add 4.7kΩ pull-ups to SDA and SCL.
- Enable I2C: Boot the Pi, open a terminal, and run
sudo raspi-config. Navigate to Interface Options > I2C and enable it. Reboot. - Verify Hardware: Run
sudo i2cdetect -y 1. You should see77(or76) in the grid. If the grid is empty, check your physical wiring before proceeding to code. - Install Dependencies: The Pi 5 uses the
lgpiobackend for GPIO control. Install the required Python packages:sudo apt update && sudo apt install python3-pyqt6 python3-smbus2 python3-gpiozero python3-lgpio i2c-toolspip3 install pimoroni-bme280 --break-system-packages
Complete Python Command Center Code
This script initializes a PyQt6 GUI, polls the BME280 every 2 seconds via a non-blocking QTimer, and toggles the GPIO 17 relay if the temperature exceeds 28.0°C. It includes explicit pin definitions and try/except blocks to handle I2C bus lockups without crashing the kiosk.
import sys
import smbus2
import bme280
from PyQt6.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget
from PyQt6.QtCore import QTimer, Qt
from PyQt6.QtGui import QFont
from gpiozero import OutputDevice
# --- PIN & CONFIGURATION DEFINITIONS ---
I2C_BUS = 1
BME280_ADDRESS = 0x77 # Use 0x76 if your breakout board has the alternate jumper
RELAY_GPIO_PIN = 17
TEMP_THRESHOLD_C = 28.0
POLL_INTERVAL_MS = 2000
# Initialize GPIO Relay (Active LOW for most Songle relay modules)
# Pi 5 uses lgpio pin factory automatically via gpiozero in Bookworm
fan_relay = OutputDevice(RELAY_GPIO_PIN, active_high=False, initial_value=False)
# Initialize I2C Bus
bus = smbus2.SMBus(I2C_BUS)
calibration_params = bme280.load_calibration_params(bus, BME280_ADDRESS)
class CommandCenter(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
self.init_timers()
def init_ui(self):
self.setWindowTitle('Pi 5 Environmental Command Center')
self.setGeometry(100, 100, 800, 480) # Matched to 5" DSI display
self.setStyleSheet('background-color: #1e1e1e; color: #ffffff;')
layout = QVBoxLayout()
self.title_label = QLabel('Rack Command Center')
self.title_label.setFont(QFont('Arial', 24, QFont.Weight.Bold))
self.title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.temp_label = QLabel('Temp: --.- °C')
self.temp_label.setFont(QFont('Arial', 48))
self.temp_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.hum_label = QLabel('Humidity: --.- %')
self.hum_label.setFont(QFont('Arial', 32))
self.hum_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.status_label = QLabel('Fan Status: OFF')
self.status_label.setFont(QFont('Arial', 20))
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.status_label.setStyleSheet('color: #4caf50;')
layout.addWidget(self.title_label)
layout.addWidget(self.temp_label)
layout.addWidget(self.hum_label)
layout.addWidget(self.status_label)
self.setLayout(layout)
def init_timers(self):
self.poll_timer = QTimer()
self.poll_timer.timeout.connect(self.read_sensors)
self.poll_timer.start(POLL_INTERVAL_MS)
def read_sensors(self):
try:
data = bme280.sample(bus, BME280_ADDRESS, calibration_params)
temp_c = data.temperature
humidity = data.humidity
self.temp_label.setText(f'Temp: {temp_c:.1f} °C')
self.hum_label.setText(f'Humidity: {humidity:.1f} %')
# Thermal threshold logic
if temp_c >= TEMP_THRESHOLD_C:
if not fan_relay.value:
fan_relay.on()
self.status_label.setText('Fan Status: EXHAUSTING')
self.status_label.setStyleSheet('color: #f44336;')
else:
if fan_relay.value:
fan_relay.off()
self.status_label.setText('Fan Status: NOMINAL')
self.status_label.setStyleSheet('color: #4caf50;')
except OSError as e:
# Catches I2C bus lockups or disconnected sensors
self.temp_label.setText('I2C ERROR')
self.temp_label.setStyleSheet('color: #f44336;')
print(f'Sensor Read Failure: {e}')
# Attempt to reset the bus object on critical failure
self.reset_i2c_bus()
def reset_i2c_bus(self):
global bus
try:
bus.close()
except Exception:
pass
bus = smbus2.SMBus(I2C_BUS)
print('I2C Bus reset attempted.')
if __name__ == '__main__':
app = QApplication(sys.argv)
# Force kiosk styling / remove window decorations if running headless-to-screen
# app.setStyle('Fusion')
center = CommandCenter()
center.showFullScreen()
sys.exit(app.exec())
Debugging: I2C Errors & Boot Failures
When deploying I2C sensors on the Pi 5, the most common failure mode during bench testing is the OSError: [Errno 121] Remote I/O error. This occurs when the SMBus controller sends a read request to the BME280 address, but the sensor fails to pull the SDA line low to acknowledge (ACK) the transaction.
The First Three Things to Check
- Verify the Address: Run
i2cdetect -y 1. If you see76instead of77, update theBME280_ADDRESSvariable in the Python script. Generic Amazon/eBay clones often default to 0x76. - Check Pull-up Resistors: Measure the voltage on the SDA and SCL lines with a multimeter. Both should read ~3.3V when idle. If they read 0V or float around 1.2V, your breakout board lacks pull-ups, and the Pi 5's internal pull-ups (which are ~50kΩ) are too weak for reliable I2C at 400kHz.
- Inspect the Ribbon Cable: The Pi 5's 40-pin header is slightly tighter than the Pi 4's. Ensure the dupont wires are fully seated and not shorting against the adjacent metal shield of the USB ports.
Ranked Causes for 'Remote I/O error'
| Rank | Cause | Fix / Measurement Threshold |
|---|---|---|
| 1 | Incorrect I2C Address in Code | Match code to i2cdetect output (0x76 vs 0x77). |
| 2 | Missing Hardware Pull-ups | Add 4.7kΩ resistors from SDA/SCL to 3.3V. Measure > 3.1V idle. |
| 3 | BME280 vs BMP280 Confusion | BMP280 lacks humidity. The bme280 library will throw an ID mismatch error. Swap the chip. |
| 4 | I2C Bus Speed Too High | Add dtparam=i2c_baudrate=100000 to /boot/firmware/config.txt to drop to 100kHz. |
Extending or Simplifying the Build
Not every command center needs a local touchscreen. Depending on your deployment environment, you should adapt this architecture to fit the physical constraints of your server rack or workshop.
How to Simplify (Headless MQTT Node)
If the Pi 5 is mounted out of sight in a rack, strip the PyQt6 GUI entirely. Replace the QTimer loop with a standard Python while True: time.sleep(2) loop, and publish the sensor data to an MQTT broker using the paho-mqtt library. This reduces RAM usage from ~350MB to under 40MB, allowing you to downgrade the hardware to a Raspberry Pi Zero 2 W to save on BOM costs and power draw. You can then view the data on your phone via Home Assistant or Node-RED.
How to Extend (Multi-Sensor & Camera Integration)
To scale this into a full visual command center:
- Add Visual Monitoring: Connect an official Raspberry Pi Camera Module 3 via the CSI port. Use
libcamera-vidto stream an RTSP feed, and embed aQMediaPlayerwidget into the PyQt6 grid layout to show a live view of your server aisle or workbench. - Dual I2C Buses: The Pi 5 exposes a second I2C bus (I2C0) on pins 27 (SDA) and 28 (SCL). You can enable this in
config.txt(dtparam=i2c_vc=on) to run a second BME280 at the opposite end of a large enclosure without dealing with I2C address collision jumpers or multiplexers. - NVMe Storage for Logging: Utilize the Pi 5's PCIe 2.0 x1 lane by adding an M.2 HAT+ and a 256GB NVMe SSD. This allows you to run a local Prometheus database and Grafana instance directly on the Pi, storing months of high-resolution thermal telemetry without degrading a microSD card.






