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
parent
5fd9615e5f
commit
6a6061aa26
50
README.md
50
README.md
|
|
@ -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 |
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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})`);
|
|
||||||
});
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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();
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
@echo off
|
||||||
|
REM Startet den Webserver. Website: http://localhost:3000
|
||||||
|
cd /d "%~dp0"
|
||||||
|
python server.py %*
|
||||||
|
pause
|
||||||
|
|
@ -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`)
|
||||||
|
|
|
||||||
|
|
@ -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"));
|
||||||
|
}
|
||||||
|
|
@ -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!");
|
||||||
|
|
|
||||||
|
|
@ -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];
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in New Issue