Skip to content

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 Bosch BME280 MEMS sensor (measuring temperature, relative humidity, and barometric pressure over hardware I2C1), calculate meteorological Dew Point and Sea-Level Pressure (QNH), and persist time-series readings into SQLite (WAL mode) via a supervised systemd service.

  • Hardware I2C1 Bus Addressing


    Daisy-chain both the Bosch BME280 (0x76) and an optional SSD1306 OLED (0x3c) across the same two SDA/SCL wires (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 systemd Daemon


    Run the telemetry collector under a dedicated unprivileged gpio/i2c service 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.

    IoT Web Server Lab

  • Workshop: Time-Lapse Camera Rig


    Pair your weather station with automated sky/cloud time-lapse photography using the Pi Camera and ffmpeg.

    Time-Lapse Rig Lab

  • Raspberry Pi Engineering Architecture


    Review ARM64 SoC architecture, NVMe SSD booting, and vcgencmd power/thermal diagnostics.

    Raspberry Pi Introduction