Raspberry Pi Workshop: Entkoppelter IoT-Telemetrie-Webserver (SQLite WAL, Flask/Gunicorn & Prometheus)
Erstellt: 06.11.2026 Aktualisiert: 06.11.2026
Management Summary: Ein klassischer Anfängerfehler in Raspberry Pi IoT-Tutorials besteht darin, physische
I2C- oder1-Wire-Sensoren direkt innerhalb eines HTTP-Request-Handlers abzufragen. Wenn fünf Browser-Tabs die Seite gleichzeitig neu laden, kollidieren parallele Prozesse auf/dev/i2c-1, blockieren den Sensorbus und lassen HTTP-Antworten sekundenlang einfrieren. Dieser Workshop implementiert eine entkoppelte Producer-Consumer-IoT-Architektur: Unser Hintergrund-Kollektor schreibt zeitgesteuert in eine SQLite Write-Ahead Log (WAL) Datenbank, während eine Multi-Worker Gunicorn + Flask API Sub-Millisekunden-JSON-Historien, ein interaktives HTML5-Dashboard und einen Prometheus/metrics-Endpunkt bereitstellt.
-
Sperrfreies Producer/Consumer-Modell
Physisches
I2C-Hardware-Polling (1 Messung/Min.) über schreibgeschützte SQLiteWAL-URI-Verbindungen (?mode=ro) vollständig von parallelen HTTP-Lesern trennen. -
Gunicorn + Flask REST-API
Bereitstellung von
/api/v1/latest,/api/v1/history?hours=24und/healthzüber einen produktiven WSGI-Worker-Pool – niemals imflask run-Entwicklungsmodus! -
Prometheus
/metrics-Endpunkt
Echtzeit-Messwerte des
BME280(Temperatur, Feuchte, QNH-Druck) sowie die SoC-Temperatur des Raspberry Pi im nativen OpenMetrics / Prometheus-Format bereitstellen. -
Responsives Live-Dashboard
Darstellung eines Dark-Mode-Telemetrie-Dashboards mit reinem HTML5/CSS Grid und leichtgewichtigen clientseitigen
fetch()-Aktualisierungen.
1. Warum direktes Sensor-Polling in HTTP-Handlern scheitert
In industriellen Gateways oder anspruchsvollen Homelab-Umgebungen ist Hardware-I/O (I2C/SPI/UART) relativ langsam (5–50 ms) und strikt single-threaded. Die Trennung des Hardware-Producer-Daemons von den HTTP-Consumer-Workern garantiert, dass selbst 100 gleichzeitige Dashboard-Anfragen in < 1 ms direkt aus dem Linux-Page-Cache bedient werden, ohne jemals die physischen GPIO-Pins zu blockieren.
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
BME["🌡️ Bosch BME280<br/>(Physischer I2C1-Bus)"]
PROD["⚙️ Producer-Daemon<br/>pi-weather.service<br/>(Liest alle 60 Sek.)"]
WAL["🗄️ SQLite3 WAL-Speicher<br/>weather_telemetry.sqlite3<br/>(1 Writer / N Reader)"]
GUNI["🚀 Gunicorn WSGI-Pool<br/>(2 Worker-Prozesse)<br/>127.0.0.1:8000"]
CLI["🌐 Browser-Dashboard<br/>& Prometheus-Scraper"]
BME -->|"smbus2 Lesezugriff"| PROD
PROD ==>|"Atomares INSERT"| WAL
WAL ==>|"Read-Only URI (?mode=ro)"| GUNI
GUNI <-->|"JSON & /metrics"| CLI
2. Den produktiven WSGI-Stack auf dem Raspberry Pi installieren
Aufbauend auf der /opt/pi-weather-Umgebung aus unserem Mini-Wetterstation-Workshop installieren wir Flask und Gunicorn in der isolierten virtuellen Umgebung:
# Datei: /opt/pi-weather/04_install_web_stack.sh
#!/usr/bin/env bash
set -euo pipefail
/opt/pi-weather/.venv/bin/pip install --upgrade Flask gunicorn
mkdir -p /opt/pi-weather/templates
3. Read-Only Telemetrie-API & Prometheus-Exporter erstellen
Erstelle die Datei /opt/pi-weather/web_server.py. Achte besonders darauf, wie die Verbindung zu SQLite über uri=True mit dem Flag ?mode=ro aufgebaut wird – dadurch kann der Webserver die Sensordatenbank weder versehentlich verändern noch sperren:
# Datei: /opt/pi-weather/web_server.py
#!/usr/bin/env python3
"""Produktionsreifer IoT-Telemetrie-Webserver & Prometheus-Exporter (Read-Only SQLite WAL Consumer)."""
import sqlite3
from pathlib import Path
from flask import Flask, Response, jsonify, render_template, request
DB_PATH = Path("/var/lib/pi-weather/weather_telemetry.sqlite3")
app = Flask(__name__)
def query_db(sql: str, params: tuple = ()) -> list[dict]:
"""Führt eine strikt lesende Abfrage auf der SQLite WAL-Datenbank aus."""
db_uri = f"file:{DB_PATH}?mode=ro"
with sqlite3.connect(db_uri, uri=True, timeout=5.0) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.execute(sql, params)
return [dict(row) for row in cursor.fetchall()]
def read_pi_soc_temp_c() -> float:
"""Liest die thermische Zone des Raspberry Pi ARM-SoC direkt aus sysfs."""
try:
raw = Path("/sys/class/thermal/thermal_zone0/temp").read_text().strip()
return round(int(raw) / 1000.0, 2)
except OSError:
return 0.0
@app.get("/healthz")
def health_check() -> tuple[Response, int]:
return jsonify({"status": "healthy", "soc_temp_c": read_pi_soc_temp_c()}), 200
@app.get("/api/v1/telemetry/latest")
def api_latest() -> tuple[Response, int]:
rows = query_db(
"""
SELECT recorded_utc, temp_c, humidity_pct, dew_point_c, station_hpa, qnh_hpa
FROM sensor_readings
ORDER BY id DESC
LIMIT 1
"""
)
if not rows:
return jsonify({"error": "Noch keine Sensordaten erfasst"}), 404
payload = rows[0]
payload["pi_soc_temp_c"] = read_pi_soc_temp_c()
return jsonify(payload), 200
@app.get("/api/v1/telemetry/history")
def api_history() -> tuple[Response, int]:
hours = min(max(int(request.args.get("hours", 24)), 1), 168)
limit_rows = hours * 60
rows = query_db(
"""
SELECT recorded_utc, temp_c, humidity_pct, dew_point_c, qnh_hpa
FROM sensor_readings
ORDER BY id DESC
LIMIT ?
""",
(limit_rows,),
)
rows.reverse()
return jsonify({"hours": hours, "count": len(rows), "series": rows}), 200
@app.get("/metrics")
def prometheus_metrics() -> Response:
"""Nativer Prometheus / OpenMetrics Exporter-Endpunkt."""
rows = query_db(
"SELECT temp_c, humidity_pct, dew_point_c, qnh_hpa FROM sensor_readings ORDER BY id DESC LIMIT 1"
)
soc_c = read_pi_soc_temp_c()
lines = [
"# HELP pi_soc_temperature_celsius Raspberry Pi ARM SoC temperature in Celsius",
"# TYPE pi_soc_temperature_celsius gauge",
f"pi_soc_temperature_celsius {soc_c}",
]
if rows:
r = rows[0]
lines.extend(
[
"# HELP weather_temperature_celsius Ambient BME280 temperature in Celsius",
"# TYPE weather_temperature_celsius gauge",
f"weather_temperature_celsius {r['temp_c']}",
"# HELP weather_humidity_percent Ambient relative humidity percentage",
"# TYPE weather_humidity_percent gauge",
f"weather_humidity_percent {r['humidity_pct']}",
"# HELP weather_dew_point_celsius Calculated Magnus dew point in Celsius",
"# TYPE weather_dew_point_celsius gauge",
f"weather_dew_point_celsius {r['dew_point_c']}",
"# HELP weather_qnh_pressure_hpa Mean sea-level barometric pressure in hPa",
"# TYPE weather_qnh_pressure_hpa gauge",
f"weather_qnh_pressure_hpa {r['qnh_hpa']}",
]
)
return Response("\n".join(lines) + "\n", mimetype="text/plain; version=0.0.4")
@app.get("/")
def dashboard_index() -> str:
return render_template("index.html")
4. Responsives Dark-Mode HTML5 Telemetrie-Dashboard
Erstelle /opt/pi-weather/templates/index.html zur Anzeige von Live-Metrikkarten, die sich über asynchrones fetch('/api/v1/telemetry/latest') alle 15 Sekunden automatisch aktualisieren:
<!-- Datei: /opt/pi-weather/templates/index.html -->
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Raspberry Pi IoT Telemetrie Gateway</title>
<style>
:root { --bg: #0f172a; --card: #1e293b; --accent: #14b8a6; --text: #f8fafc; --muted: #94a3b8; }
body { font-family: system-ui, sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 2rem; }
.header { max-width: 960px; margin: 0 auto 1.5rem; border-bottom: 1px solid #334155; padding-bottom: 1rem; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; max-width: 960px; margin: 0 auto; }
.card { background: var(--card); border: 1px solid #334155; border-radius: 0.75rem; padding: 1.25rem; }
.label { color: var(--muted); font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; }
.val { font-size: 2rem; font-weight: 700; color: var(--accent); margin-top: 0.5rem; }
</style>
</head>
<body>
<div class="header">
<h1>🌡️ Raspberry Pi Umwelt-Telemetrie</h1>
<p id="updated-at" style="color: var(--muted);">Verbindung zur SQLite WAL API wird aufgebaut...</p>
</div>
<div class="grid">
<div class="card"><div class="label">Umgebungstemperatur</div><div class="val" id="temp">-- °C</div></div>
<div class="card"><div class="label">Relative Feuchte</div><div class="val" id="hum">-- %</div></div>
<div class="card"><div class="label">Taupunkt</div><div class="val" id="dew">-- °C</div></div>
<div class="card"><div class="label">Meereshöhendruck (QNH)</div><div class="val" id="qnh">-- hPa</div></div>
<div class="card"><div class="label">Pi SoC Kerntemperatur</div><div class="val" id="soc">-- °C</div></div>
</div>
<script>
async function refreshTelemetry() {
const res = await fetch('/api/v1/telemetry/latest');
if (!res.ok) return;
const d = await res.json();
document.getElementById('temp').textContent = `${d.temp_c.toFixed(1)} °C`;
document.getElementById('hum').textContent = `${d.humidity_pct.toFixed(1)} %`;
document.getElementById('dew').textContent = `${d.dew_point_c.toFixed(1)} °C`;
document.getElementById('qnh').textContent = `${d.qnh_hpa.toFixed(1)} hPa`;
document.getElementById('soc').textContent = `${d.pi_soc_temp_c.toFixed(1)} °C`;
document.getElementById('updated-at').textContent = `Letzter Messwert (UTC): ${d.recorded_utc}`;
}
refreshTelemetry();
setInterval(refreshTelemetry, 15000);
</script>
</body>
</html>
5. Produktive Bereitstellung mit Gunicorn & systemd
Verwende in der Produktion niemals flask run (single-threaded und ungehärtet). Wir starten Gunicorn mit 2 Worker-Prozessen und 2 Threads gebunden an Port 8000:
# Datei: /etc/systemd/system/pi-weather-web.service
[Unit]
Description=Gunicorn WSGI Server for Raspberry Pi Sensor Telemetry
After=network.target pi-weather.service
[Service]
User=nejat
Group=nejat
WorkingDirectory=/opt/pi-weather
ExecStart=/opt/pi-weather/.venv/bin/gunicorn \
--workers 2 \
--threads 2 \
--bind 0.0.0.0:8000 \
--access-logfile - \
web_server:app
Restart=on-failure
RestartSec=5
# Security-Sandboxing (Wichtig: ReadOnlyPaths auf /var/lib/pi-weather!)
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadOnlyPaths=/var/lib/pi-weather
[Install]
WantedBy=multi-user.target
Dienst aktivieren und JSON- sowie Prometheus-Endpunkte abfragen:
# Datei: /opt/pi-weather/05_verify_endpoints.sh
sudo systemctl daemon-reload
sudo systemctl enable --now pi-weather-web.service
curl -s http://127.0.0.1:8000/api/v1/telemetry/latest | jq .
curl -s http://127.0.0.1:8000/metrics
Weiterführende Workshops & Architektur
Erkunde weitere Hardware-Labs auf dem Raspberry Pi oder schalte einen TLS-Reverse-Proxy vor deinen Gunicorn-Dienst:
-
Workshop: I2C Mini-Wetterstation
Hardware-Verdrahtung und
BME280-Producer-Daemon einrichten, der/var/lib/pi-weather/weather_telemetry.sqlite3befüllt. -
Workshop: Automatisiertes Zeitraffer-Rig
Automatisierte hochauflösende
rpicam-still-Aufnahmen erfassen und flackerfreie H.264/HEVC-Videos mitffmpegrendern. -
Reverse Proxy mit Caddy / Traefik
HTTPS/TLS 1.3 vor dem Gunicorn-Dienst auf Port
8000via Caddy oder Nginx Proxy Manager terminieren.