ESP-01 E1-Fehler dokumentiert, Python-Backend, robusterer CCS811-Start

- README: Hardware-Belegung, Start im Hotspot und Fehlerbehebung "WLAN Fehler E1"
  (TX/RX muessen gekreuzt sein, gemeinsame Masse, 3,3-V-Versorgung, Durchreiche-Test)
- backend: Node-Server durch Python-Server ersetzt (laeuft unter Windows ohne Installation),
  prueft beim Start SERVER_IP gegen die Laptop-IP; start.bat, firewall_freigeben.bat
- firmware: CCS811 erst nach dem WLAN starten und bei Fehler erneut versuchen,
  WLAN-Neuverbindung hoechstens einmal pro Minute; wifi_config.example.h; ccs811_diagnose

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Phase1Deliverables
Matias Mas Viehl 2026-10-27 12:40:55 +01:00
parent 5fd9615e5f
commit 6a6061aa26
13 changed files with 487 additions and 155 deletions

View File

@ -7,6 +7,11 @@ Das vollständige Konzept steht in [docs/aggregationskonzept.md](docs/aggregatio
## Datenfluss ## Datenfluss
Sensoren (Arduino UNO) ➔ WiFi/Bluetooth ➔ Backend (Aggregation & Regeln) ➔ Frontend (Webinterface + 3D-Zwilling) Sensoren (Arduino UNO) ➔ WiFi/Bluetooth ➔ Backend (Aggregation & Regeln) ➔ Frontend (Webinterface + 3D-Zwilling)
Aktuell umgesetzt:
```
DHT11 + CCS811 ─▶ Arduino Uno ─Serial (Pin 0/1)─▶ ESP-01 ─WLAN (Handy-Hotspot)─▶ Laptop: backend/server.py :3000 ◀── Browser
```
## Repo-Struktur ## Repo-Struktur
| Ordner | Inhalt | | Ordner | Inhalt |
|---|---| |---|---|
@ -16,4 +21,47 @@ Sensoren (Arduino UNO) ➔ WiFi/Bluetooth ➔ Backend (Aggregation & Regeln) ➔
| `frontend/` | Responsive Webinterface (Live-Werte, Präferenz-Slider) und 3D-Raum | | `frontend/` | Responsive Webinterface (Live-Werte, Präferenz-Slider) und 3D-Raum |
## Status ## Status
Repo-Grundgerüst; noch kein Code. Tech-Stack offen (siehe Offene Entscheidungen in [docs/offene-punkte.md](docs/offene-punkte.md)). Live-Messwerte laufen Ende-zu-Ende: Der Arduino misst Temperatur, Luftfeuchte, eCO₂ und TVOC, zeigt sie auf dem LCD und sendet sie per ESP-01 an `backend/server.py`; die Website (`frontend/`) zeigt sie live mit Verlauf und CO₂-Alarm. Aggregation/Präferenzen und 3D-Zwilling fehlen noch (siehe [docs/offene-punkte.md](docs/offene-punkte.md)).
## Hardware
| Bauteil | Anschluss am Uno |
|---|---|
| DHT11 | Daten → Pin 2 |
| LCD 16×2 | RS 7, E 8, D4–D7 → 9–12, RW → GND, V0 → Poti |
| CCS811 (CJMCU-811) | VCC → 3,3 V, GND, SDA → A4, SCL → A5, WAK → GND, ADD → GND (Adresse 0x5A) |
| ESP-01 | **TX → Pin 0, RX → Pin 1 (gekreuzt!)**, VCC + EN → 3,3 V, GND; RST, GPIO0, GPIO2 frei |
Der 3,3-V-Pin des Uno liefert nur ca. 150 mA, der ESP-01 braucht beim Start kurz bis zu 300 mA. Wir versorgen den ESP deshalb aus einer eigenen 3,3-V-Quelle (zweiter Uno); deren **GND muss mit dem GND des Haupt-Uno verbunden** sein.
## Starten
1. `firmware/dht11_lcd/wifi_config.example.h` als `wifi_config.h` kopieren und Hotspot-Name, Passwort und Laptop-IP eintragen (die Datei ist in `.gitignore`).
2. iPhone-Hotspot mit **„Maximale Kompatibilität“** einschalten (ESP-01 kann nur 2,4 GHz), Laptop verbinden.
3. Einmalig `backend/firewall_freigeben.bat` als Administrator ausführen.
4. `backend/start.bat` starten – muss „OK: SERVER_IP … passt“ melden, sonst die angezeigte IP in `wifi_config.h` eintragen.
5. ESP-Datenkabel von Pin 0/1 abziehen, `firmware/dht11_lcd` hochladen, Kabel wieder anstecken, Reset.
6. LCD zeigt „WLAN OK“, Website: http://localhost:3000
## Fehlerbehebung: „WLAN Fehler E1“ (ESP antwortet nicht)
**Symptom:** Das LCD zeigte nach dem Start immer „WLAN Fehler E1“, egal wie TX/RX gesteckt waren; `esp_test` zeigte bei jeder Baudrate „keine Antwort“. Zeitweise meldete außerdem der CCS811 „CCS811 Fehler!“.
**Ursachen (es waren mehrere gleichzeitig):**
1. **TX/RX nach Instructables-Anleitung verkabelt.** Die Anleitung „Add WiFi to Arduino UNO“ benutzt den Uno nur als USB-Adapter (leerer Sketch, der Laptop spricht mit dem ESP). Dafür wird *gerade* verbunden (ESP TX → Pin 1, ESP RX → Pin 0), weil der USB-Chip des Uno intern schon kreuzt. In unserem Projekt spricht aber der **Uno selbst** mit dem ESP – dann muss **gekreuzt** werden: **ESP TX → Pin 0 (RX), ESP RX → Pin 1 (TX)**.
2. **Keine gemeinsame Masse.** Der ESP hing an der 3,3 V eines zweiten Uno, dessen GND nicht mit dem Haupt-Uno verbunden war. Ohne gemeinsame Masse kommen auf TX/RX keine sauberen Signale an (der ESP läuft trotzdem, die blaue LED blinkt beim Start).
3. **Zu schwache 3,3-V-Versorgung.** Hingen ESP und CCS811 gemeinsam am 3,3-V-Pin des Uno, brach die Spannung beim Start des ESP ein: Der ESP startete unzuverlässig, und der CCS811 antwortete beim Initialisieren nicht („CCS811 Fehler!“). Der Sketch startet den CCS811 deshalb jetzt erst nach dem WLAN und versucht es bei Fehlern erneut.
**So haben wir es eingegrenzt (Durchreiche-Test, ohne eigenen Sketch):**
1. Leeren Sketch (Datei → Beispiele → 01.Basics → BareMinimum) auf den Uno laden.
2. ESP **gerade** anschließen: TX → Pin 1, RX → Pin 0, gemeinsame Masse.
3. Serial Monitor auf **74880 Baud**, ESP neu einstecken → lesbare Bootmeldung (`SDK ver: …`) heißt: ESP lebt, TX-Leitung ok.
4. Serial Monitor auf **115200 Baud** und „Sowohl NL als auch CR“, `AT` senden → `OK` heißt: ESP voll funktionsfähig.
5. `AT+CWMODE=1` und `AT+CWJAP="<SSID>","<Passwort>"` → `WIFI CONNECTED`, `WIFI GOT IP`, `OK` heißt: Hotspot ok.
6. Optional `AT+UART_DEF=9600,8,1,0,0`: ESP dauerhaft auf 9600 Baud (vom Uno zuverlässiger empfangen, der Sketch erkennt 9600 automatisch).
7. Danach für `dht11_lcd` wieder **gekreuzt** anschließen.
| LCD | Bedeutung |
|---|---|
| WLAN Fehler E1 | ESP antwortet nicht: TX/RX nicht gekreuzt, GND fehlt, Strom zu schwach |
| WLAN Fehler E2 | ESP antwortet, Station-Modus nicht setzbar |
| WLAN Fehler E3 | Hotspot nicht erreichbar: SSID/Passwort, „Maximale Kompatibilität“ aus, Hotspot aus |
| CCS811 Fehler! | Sensor antwortet nicht am I2C: Verkabelung prüfen, `firmware/ccs811_diagnose` zeigt den genauen Schritt |

View File

@ -1,17 +1,23 @@
# Backend (Empfang + Auslieferung des Frontends) # Backend (Empfang + Auslieferung des Frontends)
Minimaler Server mit Node-Bordmitteln (Node ≥ 18, kein `npm install`). Er nimmt die Messwerte des ESP8266 entgegen, hält die letzten 100 Messungen im Speicher und liefert zusätzlich das Frontend aus. Die Aggregationslogik (Präferenzen, CO2-Hard-Constraint, Sensor-Veto) ist hier noch **nicht** enthalten. Minimaler Server mit Python-Bordmitteln (Python ≥ 3.8, kein `pip install`, läuft direkt unter Windows). Er nimmt die Messwerte des ESP8266 entgegen, hält die letzten 100 Messungen im Speicher und liefert zusätzlich das Frontend aus. Die Aggregationslogik (Präferenzen, CO2-Hard-Constraint, Sensor-Veto) ist hier noch **nicht** enthalten.
``` ```
ESP8266 ──POST /api/messwerte──▶ backend/server.js ◀──GET /api/messwerte── Browser (frontend/) ESP8266 ──POST /api/messwerte──▶ backend/server.py ◀──GET /api/messwerte── Browser (frontend/)
``` ```
## Starten ## Starten
Doppelklick auf `start.bat` oder:
```bash ```bash
node backend/server.js # Port 3000 (oder: node backend/server.js 8080) python backend/server.py # Port 3000 (oder: python backend/server.py 8080)
node backend/simulator.js # optional: Fake-Sensordaten ohne Hardware python backend/simulator.py # optional: Fake-Sensordaten ohne Hardware
``` ```
Frontend: http://localhost:3000 Frontend: http://localhost:3000, im Hotspot von anderen Geräten: `http://<Laptop-IP>:3000`
Beim Start zeigt der Server die IP-Adressen des Laptops und prüft, ob `SERVER_IP` in `firmware/dht11_lcd/wifi_config.h` dazu passt („OK“ bzw. „ACHTUNG“). Jeder empfangene Messwert erscheint mit Absender-IP im Fenster (`127.0.0.1` = Simulator, `172.20.10.x` = ESP im Hotspot).
## Firewall (einmalig)
Windows stuft den Handy-Hotspot meist als „öffentliches Netz“ ein und blockiert eingehende Verbindungen. `firewall_freigeben.bat` per Rechtsklick → „Als Administrator ausführen“ gibt Port 3000 frei.
## Schnittstelle (Datenvertrag) ## Schnittstelle (Datenvertrag)
`POST /api/messwerte`, `Content-Type: application/json`, reines HTTP (kein TLS): `POST /api/messwerte`, `Content-Type: application/json`, reines HTTP (kein TLS):
@ -31,17 +37,8 @@ Alle Felder sind Zahlen; ein Feld darf fehlen oder `null` sein (z. B. CCS811 noc
`GET /api/messwerte` liefert `{"aktuell": {...} | null, "verlauf": [...]}`. `GET /api/messwerte` liefert `{"aktuell": {...} | null, "verlauf": [...]}`.
Test ohne Hardware: Test ohne Hardware (PowerShell):
```bash
curl -X POST localhost:3000/api/messwerte -H 'Content-Type: application/json' \
-d '{"temperatur":22.5,"luftfeuchte":45,"eco2":1150,"tvoc":30}'
```
## ESP8266 erreicht den Server nicht? (WSL2)
Läuft der Server in WSL2, hat er eine eigene IP, die im WLAN nicht erreichbar ist. Der ESP muss die **Windows-IP** des Rechners ansprechen (`ipconfig`), und der Port muss durchgereicht werden (PowerShell als Administrator):
```powershell ```powershell
netsh interface portproxy add v4tov4 listenport=3000 listenaddress=0.0.0.0 connectport=3000 connectaddress=<WSL-IP> Invoke-RestMethod -Method Post -Uri http://localhost:3000/api/messwerte -ContentType "application/json" `
New-NetFirewallRule -DisplayName "Lernklima 3000" -Direction Inbound -Protocol TCP -LocalPort 3000 -Action Allow -Body '{"temperatur":22.5,"luftfeuchte":45,"eco2":1150,"tvoc":30}'
``` ```
Die WSL-IP zeigt `wsl hostname -I`. Alternativ in `%UserProfile%\.wslconfig` den Modus `networkingMode=mirrored` setzen und die Firewall-Regel anlegen.

View File

@ -0,0 +1,5 @@
@echo off
REM Einmalig als Administrator ausfuehren (Rechtsklick > Als Administrator ausfuehren):
REM erlaubt dem ESP8266 im Hotspot (Windows stuft ihn meist als "oeffentliches Netz" ein) den Zugriff auf Port 3000.
netsh advfirewall firewall add rule name="Arduino Messwerte 3000" dir=in action=allow protocol=TCP localport=3000 profile=any
pause

View File

@ -1,97 +0,0 @@
// Minimaler Empfangs-Server: ESP8266 ➔ POST /api/messwerte ➔ Frontend (GET /api/messwerte)
// Nur Node-Bordmittel, kein npm install nötig. Start: node backend/server.js [Port]
const http = require("http");
const fs = require("fs");
const path = require("path");
const PORT = Number(process.argv[2] || process.env.PORT || 3000);
const FRONTEND_DIR = path.join(__dirname, "..", "frontend");
const MAX_VERLAUF = 100;
const MAX_BODY_BYTES = 1024;
// Erlaubte Felder des Datenvertrags (siehe backend/README.md)
const FELDER = ["temperatur", "luftfeuchte", "eco2", "tvoc"];
const verlauf = []; // { zeit: ISO-String, temperatur, luftfeuchte, eco2, tvoc }
const MIME = {
".html": "text/html; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".svg": "image/svg+xml",
};
function sendJson(res, status, obj) {
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
res.end(JSON.stringify(obj));
}
function leseBody(req, callback) {
let body = "";
let zuGross = false;
req.on("data", (chunk) => {
body += chunk;
if (body.length > MAX_BODY_BYTES) {
zuGross = true;
req.destroy();
}
});
req.on("end", () => callback(zuGross ? null : body));
}
function messwertAnnehmen(req, res) {
leseBody(req, (body) => {
if (body === null) return sendJson(res, 413, { fehler: "Anfrage zu gross" });
let daten;
try {
daten = JSON.parse(body);
} catch {
return sendJson(res, 400, { fehler: "Ungueltiges JSON" });
}
const eintrag = { zeit: new Date().toISOString() };
for (const feld of FELDER) {
// Fehlende Sensoren (z. B. CCS811 noch nicht bereit) dürfen fehlen/null sein
const wert = daten[feld];
if (wert === undefined || wert === null) {
eintrag[feld] = null;
} else if (typeof wert === "number" && Number.isFinite(wert)) {
eintrag[feld] = wert;
} else {
return sendJson(res, 400, { fehler: `Feld '${feld}' muss eine Zahl sein` });
}
}
verlauf.push(eintrag);
if (verlauf.length > MAX_VERLAUF) verlauf.shift();
console.log(new Date().toLocaleTimeString("de-DE"), JSON.stringify(eintrag));
sendJson(res, 200, { ok: true });
});
}
function statischAusliefern(req, res) {
const pfad = new URL(req.url, "http://x").pathname;
const datei = path.normalize(path.join(FRONTEND_DIR, pfad === "/" ? "index.html" : pfad));
if (!datei.startsWith(FRONTEND_DIR + path.sep)) return sendJson(res, 403, { fehler: "Verboten" });
fs.readFile(datei, (err, inhalt) => {
if (err) return sendJson(res, 404, { fehler: "Nicht gefunden" });
res.writeHead(200, { "Content-Type": MIME[path.extname(datei)] || "application/octet-stream" });
res.end(inhalt);
});
}
const server = http.createServer((req, res) => {
const pfad = new URL(req.url, "http://x").pathname;
if (pfad === "/api/messwerte") {
if (req.method === "POST") return messwertAnnehmen(req, res);
if (req.method === "GET") {
return sendJson(res, 200, { aktuell: verlauf[verlauf.length - 1] || null, verlauf });
}
return sendJson(res, 405, { fehler: "Methode nicht erlaubt" });
}
if (req.method === "GET") return statischAusliefern(req, res);
sendJson(res, 405, { fehler: "Methode nicht erlaubt" });
});
// 0.0.0.0, damit der ESP8266 im WLAN den Server erreicht
server.listen(PORT, "0.0.0.0", () => {
console.log(`Server laeuft auf http://0.0.0.0:${PORT} (Frontend: http://localhost:${PORT})`);
});

135
backend/server.py 100644
View File

@ -0,0 +1,135 @@
# Minimaler Empfangs-Server: ESP8266 -> POST /api/messwerte -> Website (GET /api/messwerte)
# Nur Python-Bordmittel, kein pip install noetig. Start: python server.py [Port] (oder start.bat)
import json
import math
import re
import socket
import sys
from collections import deque
from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 3000
HIER = Path(__file__).resolve().parent
FRONTEND_DIR = HIER.parent / "frontend"
WIFI_CONFIG = HIER.parent / "firmware" / "dht11_lcd" / "wifi_config.h"
MAX_VERLAUF = 100
MAX_BODY_BYTES = 1024
# Erlaubte Felder des Datenvertrags (so sendet esp_wifi.h)
FELDER = ["temperatur", "luftfeuchte", "eco2", "tvoc"]
verlauf = deque(maxlen=MAX_VERLAUF) # { zeit: ISO-String, temperatur, luftfeuchte, eco2, tvoc }
MIME = {
".html": "text/html; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".svg": "image/svg+xml",
}
class Handler(BaseHTTPRequestHandler):
def sende_json(self, status, obj):
daten = json.dumps(obj).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Cache-Control", "no-store")
self.send_header("Content-Length", str(len(daten)))
self.end_headers()
self.wfile.write(daten)
def pfad(self):
return self.path.split("?", 1)[0]
def do_POST(self):
if self.pfad() != "/api/messwerte":
return self.sende_json(405, {"fehler": "Methode nicht erlaubt"})
laenge = int(self.headers.get("Content-Length") or 0)
if laenge > MAX_BODY_BYTES:
return self.sende_json(413, {"fehler": "Anfrage zu gross"})
try:
daten = json.loads(self.rfile.read(laenge))
except (ValueError, UnicodeDecodeError):
return self.sende_json(400, {"fehler": "Ungueltiges JSON"})
if not isinstance(daten, dict):
return self.sende_json(400, {"fehler": "JSON-Objekt erwartet"})
eintrag = {"zeit": datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")}
for feld in FELDER:
# Fehlende Sensoren (z. B. CCS811 noch nicht bereit) duerfen fehlen/null sein
wert = daten.get(feld)
if wert is None:
eintrag[feld] = None
elif isinstance(wert, (int, float)) and not isinstance(wert, bool) and math.isfinite(wert):
eintrag[feld] = wert
else:
return self.sende_json(400, {"fehler": f"Feld '{feld}' muss eine Zahl sein"})
verlauf.append(eintrag)
print(datetime.now().strftime("%H:%M:%S"), self.client_address[0], json.dumps(eintrag), flush=True)
self.sende_json(200, {"ok": True})
def do_GET(self):
pfad = self.pfad()
if pfad == "/api/messwerte":
return self.sende_json(200, {"aktuell": verlauf[-1] if verlauf else None, "verlauf": list(verlauf)})
datei = (FRONTEND_DIR / ("index.html" if pfad == "/" else pfad.lstrip("/"))).resolve()
if FRONTEND_DIR not in datei.parents:
return self.sende_json(403, {"fehler": "Verboten"})
if not datei.is_file():
return self.sende_json(404, {"fehler": "Nicht gefunden"})
inhalt = datei.read_bytes()
self.send_response(200)
self.send_header("Content-Type", MIME.get(datei.suffix, "application/octet-stream"))
self.send_header("Content-Length", str(len(inhalt)))
self.end_headers()
self.wfile.write(inhalt)
def log_message(self, *args):
pass # keine Zeile pro Seitenabruf, nur empfangene Messwerte werden ausgegeben
def eigene_ips():
"""IPv4-Adressen dieses PCs (ohne 127.x), z. B. 172.20.10.x im iPhone-Hotspot."""
try:
infos = socket.getaddrinfo(socket.gethostname(), None, socket.AF_INET)
except socket.gaierror:
return []
return sorted({info[4][0] for info in infos if not info[4][0].startswith("127.")})
def konfigurierte_server_ip():
try:
treffer = re.search(r'#define\s+SERVER_IP\s+"([^"]+)"', WIFI_CONFIG.read_text(encoding="utf-8"))
except OSError:
return None
return treffer.group(1) if treffer else None
def ip_hinweis():
ips = eigene_ips()
soll = konfigurierte_server_ip()
print("IP-Adressen dieses PCs:", ", ".join(ips) or "keine (nicht verbunden?)")
if soll is None:
return
if soll in ips:
print(f"OK: SERVER_IP in wifi_config.h ({soll}) passt zu diesem PC.")
else:
print(f"ACHTUNG: wifi_config.h sendet an {soll}, diese IP hat der PC gerade nicht!")
print(" -> Mit dem Hotspot verbinden bzw. SERVER_IP auf die Hotspot-IP oben aendern und neu flashen.")
if __name__ == "__main__":
# 0.0.0.0, damit der ESP8266 im WLAN/Hotspot den Server erreicht
server = ThreadingHTTPServer(("0.0.0.0", PORT), Handler)
print(f"Server laeuft auf Port {PORT} (Website: http://localhost:{PORT})")
ip_hinweis()
print("Beenden mit Strg+C\n", flush=True)
try:
server.serve_forever()
except KeyboardInterrupt:
pass

View File

@ -1,27 +0,0 @@
// Simuliert den ESP8266: schickt alle 2 s Fantasiewerte an den Server (Entwicklung ohne Hardware).
// Start: node backend/simulator.js [Server-URL] z. B. http://localhost:3000
const basis = process.argv[2] || "http://localhost:3000";
let schritt = 0;
async function sende() {
schritt++;
const messwert = {
temperatur: Math.round((22 + 2 * Math.sin(schritt / 20)) * 10) / 10,
luftfeuchte: Math.round((45 + 5 * Math.sin(schritt / 15)) * 10) / 10,
eco2: Math.round(800 + 400 * Math.sin(schritt / 25)), // überschreitet zeitweise 1000 ppm
tvoc: Math.round(40 + 30 * Math.sin(schritt / 10) + 30),
};
try {
await fetch(basis + "/api/messwerte", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(messwert),
});
console.log("gesendet", JSON.stringify(messwert));
} catch (fehler) {
console.log("Server nicht erreichbar:", fehler.message);
}
}
setInterval(sende, 2000);
sende();

View File

@ -0,0 +1,27 @@
# Simuliert den ESP8266: schickt alle 2 s Fantasiewerte an den Server (Entwicklung ohne Hardware).
# Start: python simulator.py [Server-URL] z. B. http://localhost:3000
import json
import math
import sys
import time
import urllib.request
basis = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:3000"
schritt = 0
while True:
schritt += 1
messwert = {
"temperatur": round(22 + 2 * math.sin(schritt / 20), 1),
"luftfeuchte": round(45 + 5 * math.sin(schritt / 15), 1),
"eco2": round(800 + 400 * math.sin(schritt / 25)), # ueberschreitet zeitweise 1000 ppm
"tvoc": round(70 + 30 * math.sin(schritt / 10)),
}
anfrage = urllib.request.Request(basis + "/api/messwerte", data=json.dumps(messwert).encode(),
headers={"Content-Type": "application/json"})
try:
urllib.request.urlopen(anfrage, timeout=3).read()
print("gesendet", json.dumps(messwert))
except OSError as fehler:
print("Server nicht erreichbar:", fehler)
time.sleep(2)

View File

@ -0,0 +1,5 @@
@echo off
REM Startet den Webserver. Website: http://localhost:3000
cd /d "%~dp0"
python server.py %*
pause

View File

@ -1,7 +1,7 @@
# Offene Entscheidungen # Offene Entscheidungen
- [ ] Backend-Sprache/Framework (z. B. Node.js, Python/FastAPI) - [ ] Backend-Sprache/Framework (z. B. Node.js, Python/FastAPI)
- [ ] Transport Sensoren ➔ Backend (HTTP, MQTT, WebSocket; WiFi vs. Bluetooth). Hinweis: Arduino UNO hat kein WiFi – ggf. ESP-Modul/UNO R4 WiFi. - [x] Transport Sensoren ➔ Backend: HTTP-POST über ESP-01 (AT-Befehle) im WLAN/Handy-Hotspot
- [ ] Live-Updates im Frontend (WebSocket / SSE) - [ ] Live-Updates im Frontend (WebSocket / SSE)
- [ ] Frontend-Framework und 3D-Bibliothek (z. B. three.js) - [ ] Frontend-Framework und 3D-Bibliothek (z. B. three.js)
- [ ] Wetter-API (Außentemperatur, Windvorhersage, Feinstaub) - [ ] Wetter-API (Außentemperatur, Windvorhersage, Feinstaub)
@ -9,4 +9,4 @@
- [ ] Schwellwerte: CO2-Limit (1000 ppm), „extremer Lärm", „extremer Wind" - [ ] Schwellwerte: CO2-Limit (1000 ppm), „extremer Lärm", „extremer Wind"
- [ ] Gewichtung: Dozent vs. Studierende beim Durchschnitt? Zeitfenster/Verfall alter Eingaben? - [ ] Gewichtung: Dozent vs. Studierende beim Durchschnitt? Zeitfenster/Verfall alter Eingaben?
- [ ] Authentifizierung/Anonymität der Nutzer (Mehrfachabgaben verhindern) - [ ] Authentifizierung/Anonymität der Nutzer (Mehrfachabgaben verhindern)
- [ ] Simulation von Sensordaten für Entwicklung ohne Hardware - [x] Simulation von Sensordaten für Entwicklung ohne Hardware (`backend/simulator.py`)

View File

@ -0,0 +1,211 @@
// Schritt-fuer-Schritt-Diagnose fuer den CCS811 (CJMCU-811), ohne Adafruit-Bibliothek, direkt ueber I2C.
// Ausgabe im Serial Monitor (9600 Baud). ESP nicht angeschlossen.
// Verdrahtung: VCC -> 3,3 V, GND -> GND, SDA -> A4, SCL -> A5, WAK -> GND, ADD -> GND (Adresse 0x5A)
#include <Wire.h>
// CCS811-Register (Datenblatt)
const uint8_t REG_STATUS = 0x00;
const uint8_t REG_MEAS_MODE = 0x01;
const uint8_t REG_ALG_RESULT = 0x02;
const uint8_t REG_HW_ID = 0x20;
const uint8_t REG_HW_VERSION = 0x21;
const uint8_t REG_FW_BOOT_VERSION = 0x23;
const uint8_t REG_FW_APP_VERSION = 0x24;
const uint8_t REG_ERROR_ID = 0xE0;
const uint8_t REG_APP_START = 0xF4;
const uint8_t REG_SW_RESET = 0xFF;
uint8_t adresse = 0;
bool bereit = false;
bool schreibe(uint8_t reg, const uint8_t* daten, uint8_t n) {
Wire.beginTransmission(adresse);
Wire.write(reg);
Wire.write(daten, n);
return Wire.endTransmission() == 0;
}
bool lese(uint8_t reg, uint8_t* daten, uint8_t n) {
Wire.beginTransmission(adresse);
Wire.write(reg);
if (Wire.endTransmission() != 0) return false;
if (Wire.requestFrom(adresse, n) != n) return false;
for (uint8_t i = 0; i < n; i++) daten[i] = Wire.read();
return true;
}
void hex(uint8_t wert) {
Serial.print(F("0x"));
if (wert < 0x10) Serial.print('0');
Serial.print(wert, HEX);
}
void zeigeStatus(uint8_t s) {
Serial.print(F(" STATUS = "));
hex(s);
Serial.print(F(" ERROR="));
Serial.print(s & 0x01);
Serial.print(F(" DATA_READY="));
Serial.print((s >> 3) & 1);
Serial.print(F(" APP_VALID="));
Serial.print((s >> 4) & 1);
Serial.print(F(" FW_MODE="));
Serial.println((s >> 7) & 1);
}
void zeigeFehler() {
uint8_t e;
if (!lese(REG_ERROR_ID, &e, 1)) {
Serial.println(F(" ERROR_ID nicht lesbar"));
return;
}
Serial.print(F(" ERROR_ID = "));
hex(e);
Serial.println();
if (e & 0x01) Serial.println(F(" - WRITE_REG_INVALID"));
if (e & 0x02) Serial.println(F(" - READ_REG_INVALID"));
if (e & 0x04) Serial.println(F(" - MEASMODE_INVALID"));
if (e & 0x08) Serial.println(F(" - MAX_RESISTANCE: Sensorelement defekt oder extremer Wert"));
if (e & 0x10) Serial.println(F(" - HEATER_FAULT: Heizer defekt"));
if (e & 0x20) Serial.println(F(" - HEATER_SUPPLY: Versorgungsspannung zu schwach -> 3,3 V / GND pruefen"));
}
// Liefert false und Erklaerung, wenn ein Schritt scheitert
bool initialisiere() {
// 1) Pegel der Leitungen ohne interne Pull-ups: die Platine zieht SDA/SCL auf ihre 3,3 V
pinMode(A4, INPUT);
pinMode(A5, INPUT);
delay(10);
Serial.print(F("\n[1] Pegel: SDA(A4)="));
Serial.print(digitalRead(A4) ? F("HIGH") : F("LOW"));
Serial.print(F(" SCL(A5)="));
Serial.println(digitalRead(A5) ? F("HIGH") : F("LOW"));
if (!digitalRead(A4) || !digitalRead(A5)) {
Serial.println(F(" -> LOW = Leitung haengt auf GND oder Modul hat keinen Strom (VCC an 3,3 V?)"));
}
// 2) I2C-Scan
Wire.begin();
Wire.setClock(100000);
Wire.setWireTimeout(25000, true); // kein Haengenbleiben bei blockiertem Bus
Serial.println(F("\n[2] I2C-Scan"));
uint8_t gefunden = 0;
for (uint8_t a = 1; a < 127; a++) {
Wire.beginTransmission(a);
uint8_t r = Wire.endTransmission();
if (r == 0) {
Serial.print(F(" Geraet bei "));
hex(a);
Serial.println();
gefunden++;
if (a == 0x5A || a == 0x5B) adresse = a;
} else if (r == 5) {
Serial.println(F(" Timeout: Bus blockiert (SDA/SCL Kurzschluss oder vertauscht?)"));
return false;
}
}
if (adresse == 0) {
Serial.println(gefunden ? F(" Kein CCS811 bei 0x5A/0x5B (andere Geraete gefunden)")
: F(" KEIN Geraet: SDA/SCL vertauscht? VCC/GND? Loetstellen der Stiftleiste?"));
return false;
}
// 3) Software-Reset -> Bootloader-Modus
const uint8_t reset[] = {0x11, 0xE5, 0x72, 0x8A};
schreibe(REG_SW_RESET, reset, 4);
delay(100);
// 4) Chip-ID und Versionen
uint8_t id, hw, v[2];
Serial.println(F("\n[3] Chip"));
if (!lese(REG_HW_ID, &id, 1)) {
Serial.println(F(" HW_ID nicht lesbar (I2C-Fehler)"));
return false;
}
Serial.print(F(" HW_ID = "));
hex(id);
Serial.println(id == 0x81 ? F(" (richtig)") : F(" (FALSCH, erwartet 0x81: kein CCS811 oder gestoerter Bus)"));
if (id != 0x81) return false;
if (lese(REG_HW_VERSION, &hw, 1)) {
Serial.print(F(" HW_VERSION = "));
hex(hw);
Serial.println();
}
if (lese(REG_FW_BOOT_VERSION, v, 2)) {
Serial.print(F(" Bootloader = "));
Serial.print(v[0] >> 4); Serial.print('.'); Serial.print(v[0] & 0x0F); Serial.print('.'); Serial.println(v[1]);
}
if (lese(REG_FW_APP_VERSION, v, 2)) {
Serial.print(F(" App-Firmware = "));
Serial.print(v[0] >> 4); Serial.print('.'); Serial.print(v[0] & 0x0F); Serial.print('.'); Serial.println(v[1]);
}
// 5) Firmware starten
uint8_t s;
Serial.println(F("\n[4] Status vor APP_START"));
lese(REG_STATUS, &s, 1);
zeigeStatus(s);
if (!(s & 0x10)) {
Serial.println(F(" APP_VALID=0: keine gueltige Firmware auf dem Sensor (muesste neu geflasht werden)"));
return false;
}
schreibe(REG_APP_START, nullptr, 0);
delay(100);
Serial.println(F("\n[5] Status nach APP_START"));
lese(REG_STATUS, &s, 1);
zeigeStatus(s);
if (s & 0x01) zeigeFehler();
if (!(s & 0x80)) {
Serial.println(F(" FW_MODE=0: Firmware startet nicht"));
return false;
}
// 6) Messmodus 1: jede Sekunde ein Wert
const uint8_t modus = 0x10;
schreibe(REG_MEAS_MODE, &modus, 1);
delay(10);
lese(REG_STATUS, &s, 1);
if (s & 0x01) {
Serial.println(F("\n[6] Fehler beim Setzen des Messmodus"));
zeigeFehler();
return false;
}
Serial.println(F("\n[6] Messung laeuft (die ersten ~20 min sind 400 ppm / 0 ppb normal)\n"));
return true;
}
void setup() {
Serial.begin(9600);
Serial.println(F("\n===== CCS811 Diagnose ====="));
delay(500); // CCS811 braucht nach dem Einschalten ~20 ms
bereit = initialisiere();
if (!bereit) {
Serial.println(F("\n##### Abbruch, siehe letzten Schritt #####"));
}
}
void loop() {
if (!bereit) return;
delay(1000);
uint8_t d[6];
if (!lese(REG_ALG_RESULT, d, 6)) {
Serial.println(F("Lesefehler (I2C)"));
return;
}
uint8_t status = d[4];
if (status & 0x01) {
zeigeStatus(status);
zeigeFehler();
return;
}
if (!(status & 0x08)) {
Serial.println(F("noch keine neuen Daten"));
return;
}
Serial.print(F("eCO2: "));
Serial.print(((uint16_t)d[0] << 8) | d[1]);
Serial.print(F(" ppm TVOC: "));
Serial.print(((uint16_t)d[2] << 8) | d[3]);
Serial.println(F(" ppb"));
}

View File

@ -60,6 +60,16 @@ void baselineSpeichern() {
EEPROM.put(BASELINE_ADRESSE, b); EEPROM.put(BASELINE_ADRESSE, b);
} }
// CCS811 starten: erst 0x5A (ADDR an GND), dann 0x5B (ADDR an VCC).
// Wird in loop() wiederholt, falls der Sensor nicht antwortet (z. B. weil der ESP
// beim Hochfahren die 3,3 V kurz einbrechen laesst).
void ccsStarten() {
ccsOk = ccs.begin(0x5A) || ccs.begin(0x5B);
if (ccsOk) {
baselineLaden();
}
}
// Anzeige wechselt zwischen zwei Seiten, da das LCD nur 2 Zeilen hat // Anzeige wechselt zwischen zwei Seiten, da das LCD nur 2 Zeilen hat
bool zeigeLuftqualitaet = false; bool zeigeLuftqualitaet = false;
@ -72,12 +82,6 @@ void setup() {
// DHT Sensor starten // DHT Sensor starten
dht.begin(); dht.begin();
// CCS811 starten: erst 0x5A (ADDR an GND), dann 0x5B (ADDR an VCC)
ccsOk = ccs.begin(0x5A) || ccs.begin(0x5B);
if (ccsOk) {
baselineLaden();
}
// Kurze Startnachricht // Kurze Startnachricht
lcd.setCursor(0, 0); lcd.setCursor(0, 0);
lcd.print("Sensor startet.."); lcd.print("Sensor startet..");
@ -95,6 +99,9 @@ void setup() {
lcd.print(" "); lcd.print(" ");
} }
// CCS811 erst nach dem ESP starten, dann ist dessen Einschalt-Stromspitze vorbei
ccsStarten();
// Der DHT11 braucht nach dem Start einen Moment // Der DHT11 braucht nach dem Start einen Moment
delay(2000); delay(2000);
lcd.clear(); lcd.clear();
@ -122,6 +129,9 @@ void loop() {
zeigeLuftqualitaet = false; zeigeLuftqualitaet = false;
lcd.clear(); lcd.clear();
if (!ccsOk) {
ccsStarten(); // neuer Versuch, statt dauerhaft "Fehler" zu zeigen
}
if (!ccsOk) { if (!ccsOk) {
lcd.setCursor(0, 0); lcd.setCursor(0, 0);
lcd.print("CCS811 Fehler!"); lcd.print("CCS811 Fehler!");

View File

@ -1,11 +1,11 @@
// ESP8266 (ESP-01) per AT-Befehle am Arduino Uno: sendet die Messwerte als HTTP-POST // ESP8266 (ESP-01) per AT-Befehle am Arduino Uno: sendet die Messwerte als HTTP-POST
// an das Backend (POST /api/messwerte, Datenvertrag siehe backend/README.md). // an das Backend (POST /api/messwerte, Empfaenger: backend/server.py).
#include "wifi_config.h" #include "wifi_config.h"
// --- ESP-01 Verdrahtung (wie Instructables "Add WiFi to Arduino UNO") --- // --- ESP-01 Verdrahtung (GEKREUZT, anders als Instructables "Add WiFi to Arduino UNO", siehe README) ---
// ESP TX -> Uno RX (Pin 0) \ Hardware-Serial! Das Modul teilt sich die Leitung mit dem USB-Anschluss: // ESP TX -> Uno RX (Pin 0) \ Hardware-Serial! Das Modul teilt sich die Leitung mit dem USB-Anschluss:
// ESP RX -> Uno TX (Pin 1) / Beim Hochladen des Sketches ESP TX/RX kurz abziehen. // ESP RX -> Uno TX (Pin 1) / Beim Hochladen des Sketches ESP TX/RX kurz abziehen.
// ESP VIN und ENABLE -> 3,3 V, ESP GND -> GND // ESP VCC und EN -> 3,3 V, ESP GND -> GND (gemeinsame Masse mit dem Uno, auch bei fremder 3,3-V-Quelle)
// Serial.print() darf im Sketch NICHT fuer Debugging genutzt werden, die Ausgabe ginge an das ESP. // Serial.print() darf im Sketch NICHT fuer Debugging genutzt werden, die Ausgabe ginge an das ESP.
#define espSerial Serial #define espSerial Serial
@ -13,6 +13,11 @@ bool wifiOk = false;
uint8_t sendeFehler = 0; uint8_t sendeFehler = 0;
const uint8_t MAX_SENDEFEHLER = 3; // danach wird das WLAN neu verbunden const uint8_t MAX_SENDEFEHLER = 3; // danach wird das WLAN neu verbunden
// Ein Verbindungsversuch blockiert 10-30 s. Ohne WLAN deshalb hoechstens einmal pro Minute
// neu verbinden, damit das LCD in der Zwischenzeit normal weiterlaeuft.
const unsigned long WIFI_NEUVERSUCH_MS = 60000UL;
unsigned long letzterWifiVersuch = 0;
// Verwirft alles, was der ESP noch geschickt hat // Verwirft alles, was der ESP noch geschickt hat
void espLeeren() { void espLeeren() {
while (espSerial.available()) { while (espSerial.available()) {
@ -66,6 +71,7 @@ uint8_t wifiFehler = 0;
bool wifiVerbinden() { bool wifiVerbinden() {
wifiOk = false; wifiOk = false;
letzterWifiVersuch = millis();
if (!espBaudrateFinden()) { if (!espBaudrateFinden()) {
wifiFehler = 1; wifiFehler = 1;
@ -92,8 +98,10 @@ bool wifiVerbinden() {
// Schickt einen Messwertsatz per HTTP-POST an das Backend. true, wenn der Server mit 200 antwortet. // Schickt einen Messwertsatz per HTTP-POST an das Backend. true, wenn der Server mit 200 antwortet.
bool sendeMesswerte(float temperatur, float luftfeuchte, uint16_t eco2, uint16_t tvoc) { bool sendeMesswerte(float temperatur, float luftfeuchte, uint16_t eco2, uint16_t tvoc) {
if (!wifiOk && !wifiVerbinden()) { if (!wifiOk) {
return false; if (millis() - letzterWifiVersuch < WIFI_NEUVERSUCH_MS || !wifiVerbinden()) {
return false;
}
} }
char temp[8], feuchte[8]; char temp[8], feuchte[8];

View File

@ -0,0 +1,10 @@
// Vorlage: Kopie als "wifi_config.h" im selben Ordner anlegen und Werte eintragen.
// wifi_config.h steht in .gitignore, damit das WLAN-Passwort nicht ins Repo kommt.
// Der ESP-01 kann nur 2,4 GHz: beim iPhone-Hotspot "Maximale Kompatibilitaet" einschalten.
#define WLAN_SSID "Name des Hotspots"
#define WLAN_PASSWORT "Passwort"
// IP des Laptops im Hotspot, auf dem backend/server.py laeuft (zeigt server.py beim Start an)
#define SERVER_IP "172.20.10.2"
#define SERVER_PORT 3000