Raspberry Pi Workshop: I2C Mini Weather Station with BME280, OLED & Systemd Telemetry
Created: 2026-11-06 Last updated: 2026-11-06
Executive Summary: Cheap consumer weather sensors often rely on slow, bit-banged single-wire protocols (
DHT11/DHT22) that drop checksums under Linux scheduling jitter. In this engineering workshop, we build a calibration-grade I2C Environmental Telemetry Station on a Raspberry Pi (Debian 12 Bookworm 64-bit) using the BoschBME280MEMS sensor (measuring temperature, relative humidity, and barometric pressure over hardwareI2C1), calculate meteorological Dew Point and Sea-Level Pressure (QNH), and persist time-series readings into SQLite (WAL mode) via a supervisedsystemdservice.
-
Hardware
I2C1Bus Addressing
Daisy-chain both the Bosch
BME280(0x76) and an optionalSSD1306OLED (0x3c) across the same twoSDA/SCLwires (GPIO 2&GPIO 3). -
Meteorological Physics
Derive exact Magnus-Tetens Dew Point (
°C) and altitude-compensated Mean Sea-Level Barometric Pressure (QNH hPa) from raw station metrics. -
SQLite WAL Time-Series Store
Log timestamped sensor samples atomically with zero SD-card corruption risk using SQLite
PRAGMA journal_mode=WAL. -
Unattended
systemdDaemon
Run the telemetry collector under a dedicated unprivileged
gpio/i2cservice account with automatic crash recovery.
1. Hardware Architecture & Why BME280 Beats DHT22
Never use legacy DHT11/DHT22 sensors on a multi-tasking Linux OS: they require microsecond bit-banging in user space, lack barometric pressure, and drift after high-humidity exposure. The Bosch BME280 features an on-chip hardware I2C/SPI controller, factory-trimmed calibration registers, and ±1.0 hPa pressure accuracy.
%%{init: {'flowchart': {'wrappingWidth': 600, 'subGraphTitleMargin': {'top': 15, 'bottom': 15}}}}%%
flowchart TB
subgraph PiHeader["📌 Raspberry Pi 40-Pin Header"]
P1["Pin 1: 3.3V DC Power"]
P3["Pin 3: GPIO 2 (I2C1 SDA)"]
P5["Pin 5: GPIO 3 (I2C1 SCL)"]
P6["Pin 6: Ground (GND)"]
end
subgraph I2CBus["🔗 Shared 400 kHz I2C1 Bus"]
BME["🌡️ Bosch BME280 Sensor<br/>I2C Address: 0x76 (or 0x77)<br/>Temp / Humidity / Pressure"]
OLED["🖥️ SSD1306 128x64 OLED<br/>I2C Address: 0x3c<br/>Local Live Readout"]
end
P1 --> BME
P1 --> OLED
P3 <-->|"SDA Data"| BME
P3 <-->|"SDA Data"| OLED
P5 -->|"SCL Clock"| BME
P5 -->|"SCL Clock"| OLED
P6 --- BME
P6 --- OLED
Pin-to-Pin Wiring Matrix (3.3V Logic Only!)
BME280 / SSD1306 Pin |
Raspberry Pi Physical Pin | BCM GPIO Name | Wire Color Convention | Electrical Function |
|---|---|---|---|---|
VCC / VIN |
Pin 1 (or Pin 17) | 3V3 Power |
🔴 Red | Strictly regulated 3.3V DC supply |
GND |
Pin 6 (or Pin 9, 14, 20) | Ground |
⚫ Black | Common signal and power ground |
SDA / SDI |
Pin 3 | GPIO 2 (SDA1) |
🔵 Blue | Bidirectional I2C Serial Data line (internal 1.8 kΩ pull-up on Pi) |
SCL / SCK |
Pin 5 | GPIO 3 (SCL1) |
🟡 Yellow | Synchronized I2C Serial Clock line (400 kHz Fast-Mode) |
2. Enabling Hardware I2C1 & Verifying Bus Addresses
Enable the hardware ARM I2C controller in /boot/firmware/config.txt and verify that the sensor responds on bus 1 (/dev/i2c-1) using i2cdetect:
# File: /opt/pi-weather/01_enable_verify_i2c.sh
#!/usr/bin/env bash
set -euo pipefail
# 1. Install I2C diagnostic utilities and Python virtual environment tooling
sudo apt update && sudo apt install -y i2c-tools python3-venv python3-pip sqlite3
# 2. Grant current user non-root access to /dev/i2c-* character devices
sudo usermod -aG i2c "$USER"
# 3. Scan I2C Bus 1 (Expect 0x76 or 0x77 for BME280, and 0x3c if OLED is attached)
sudo i2cdetect -y 1
Expected output from sudo i2cdetect -y 1:
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- 3c -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- 76 --
3. Meteorological Physics & SQLite Telemetry Collector
Raw sensor pressure (station pressure) drops by roughly 12 hPa for every 100 meters of elevation above sea level. To compare your station's barometric trend with official aviation/weather maps, we compute Mean Sea-Level Pressure (QNH) using the international barometric formula and Dew Point (°C) using the Magnus-Tetens approximation.
Create an isolated virtual environment in /opt/pi-weather and install RPi.bme280 and smbus2:
# File: /opt/pi-weather/02_setup_venv.sh
sudo mkdir -p /opt/pi-weather /var/lib/pi-weather
sudo chown -R "$USER:$USER" /opt/pi-weather /var/lib/pi-weather
python3 -m venv /opt/pi-weather/.venv
/opt/pi-weather/.venv/bin/pip install --upgrade pip smbus2 RPi.bme280
Now create the production telemetry collector (/opt/pi-weather/weather_collector.py):
# File: /opt/pi-weather/weather_collector.py
#!/usr/bin/env python3
"""Production I2C BME280 telemetry collector with Dew Point, QNH Pressure, and SQLite WAL storage."""
import math
import sqlite3
import time
from datetime import datetime, timezone
from pathlib import Path
import bme280
import smbus2
I2C_BUS = 1
I2C_ADDRESS = 0x76
STATION_ALTITUDE_M = 115.0 # Set to your exact local elevation above sea level (meters)
DB_PATH = Path("/var/lib/pi-weather/weather_telemetry.sqlite3")
SAMPLE_INTERVAL_SEC = 60
def calculate_dew_point_c(temp_c: float, humidity_pct: float) -> float:
"""Compute Dew Point (°C) using the Magnus-Tetens formula."""
a, b = 17.625, 243.04
alpha = math.log(max(humidity_pct, 0.1) / 100.0) + (a * temp_c) / (b + temp_c)
return round((b * alpha) / (a - alpha), 2)
def calculate_sea_level_qnh(station_hpa: float, temp_c: float, altitude_m: float) -> float:
"""Convert local station pressure (hPa) to Mean Sea-Level Pressure (QNH hPa)."""
temp_kelvin = temp_c + 273.15
qnh = station_hpa * math.pow(
1.0 - (0.0065 * altitude_m) / (temp_kelvin + 0.0065 * altitude_m), -5.257
)
return round(qnh, 2)
def init_database(db_path: Path) -> sqlite3.Connection:
conn = sqlite3.connect(db_path)
conn.execute("PRAGMA journal_mode=WAL;")
conn.execute("PRAGMA synchronous=NORMAL;")
conn.execute(
"""
CREATE TABLE IF NOT EXISTS sensor_readings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
recorded_utc TEXT NOT NULL,
temp_c REAL NOT NULL,
humidity_pct REAL NOT NULL,
dew_point_c REAL NOT NULL,
station_hpa REAL NOT NULL,
qnh_hpa REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_readings_utc ON sensor_readings (recorded_utc);
"""
)
conn.commit()
return conn
def run_collector() -> None:
bus = smbus2.SMBus(I2C_BUS)
calibration_params = bme280.load_calibration_params(bus, I2C_ADDRESS)
conn = init_database(DB_PATH)
print(f"[INFO] BME280 initialized at 0x{I2C_ADDRESS:02x}. Logging to {DB_PATH}...")
try:
while True:
sample = bme280.sample(bus, I2C_ADDRESS, calibration_params)
temp_c = round(sample.temperature, 2)
humidity_pct = round(sample.humidity, 2)
station_hpa = round(sample.pressure, 2)
dew_c = calculate_dew_point_c(temp_c, humidity_pct)
qnh_hpa = calculate_sea_level_qnh(station_hpa, temp_c, STATION_ALTITUDE_M)
now_iso = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
conn.execute(
"""
INSERT INTO sensor_readings
(recorded_utc, temp_c, humidity_pct, dew_point_c, station_hpa, qnh_hpa)
VALUES (?, ?, ?, ?, ?, ?)
""",
(now_iso, temp_c, humidity_pct, dew_c, station_hpa, qnh_hpa),
)
conn.commit()
print(
f"[{now_iso}] Temp: {temp_c:5.2f}°C | RH: {humidity_pct:5.1f}% | "
f"Dew: {dew_c:5.2f}°C | QNH: {qnh_hpa:7.2f} hPa"
)
time.sleep(SAMPLE_INTERVAL_SEC)
finally:
conn.close()
bus.close()
if __name__ == "__main__":
run_collector()
4. Deploying as a Hardened systemd Background Service
Create a systemd unit so the weather collector starts automatically on boot and restarts cleanly if an I2C transient occurs:
# File: /etc/systemd/system/pi-weather.service
[Unit]
Description=Raspberry Pi I2C BME280 Weather Telemetry Collector
After=local-fs.target
[Service]
Type=simple
User=nejat
SupplementaryGroups=i2c gpio
WorkingDirectory=/opt/pi-weather
ExecStart=/opt/pi-weather/.venv/bin/python /opt/pi-weather/weather_collector.py
Restart=on-failure
RestartSec=10
# Security Sandboxing
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/lib/pi-weather
[Install]
WantedBy=multi-user.target
Enable and inspect live telemetry logs:
# File: /opt/pi-weather/03_enable_service.sh
sudo systemctl daemon-reload
sudo systemctl enable --now pi-weather.service
journalctl -u pi-weather.service -f
5. Hardware Troubleshooting Matrix
| Symptom / Error Message | Root Cause | Immediate Remediation |
|---|---|---|
i2cdetect -y 1 shows all -- blanks |
SDA/SCL wires swapped, missing GND, or 3.3V power not connected |
Verify physical Pin 1 (3.3V), Pin 3 (SDA), Pin 5 (SCL), and Pin 6 (GND) with a multimeter. |
OSError: [Errno 121] Remote I/O error |
Sensor address is 0x77 instead of 0x76 (controlled by SDO pin on breakout board) |
Check i2cdetect -y 1 output and update I2C_ADDRESS = 0x77 in weather_collector.py. |
Temperature reads 2°C–4°C higher than room thermometer |
Sensor is mounted directly above the hot Raspberry Pi CPU heatsink | Use a 20 cm shielded 4-wire cable to position the BME280 away from the Pi's thermal plume. |
Where to Go Next
Stream your SQLite weather database to a live web dashboard or explore camera automation:
-
Workshop: IoT Sensor Web Server
Expose your SQLite sensor database via a real-time JSON REST API and interactive dashboard in Python.
-
Workshop: Time-Lapse Camera Rig
Pair your weather station with automated sky/cloud time-lapse photography using the Pi Camera and
ffmpeg. -
Raspberry Pi Engineering Architecture
Review ARM64 SoC architecture, NVMe SSD booting, and
vcgencmdpower/thermal diagnostics.