98 lines
3.5 KiB
JavaScript
98 lines
3.5 KiB
JavaScript
// 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})`);
|
|
});
|