projekt:python_fastapi1
Unterschiede
Hier werden die Unterschiede zwischen zwei Versionen angezeigt.
| Beide Seiten der vorigen RevisionVorhergehende ÜberarbeitungNächste Überarbeitung | Vorhergehende Überarbeitung | ||
| projekt:python_fastapi1 [2026/02/21 11:53] – torsten.roehl | projekt:python_fastapi1 [2026/02/21 13:22] (aktuell) – gelöscht torsten.roehl | ||
|---|---|---|---|
| Zeile 1: | Zeile 1: | ||
| - | OK. MINIMAL-ERWEITERUNG OHNE JAVASCRIPT/ | ||
| - | - Verlauf wird als TEXTDATEI gespeichert (course_web/ | ||
| - | - Aufzeichnung läuft im Hintergrund (Thread) im laufenden FastAPI-Prozess (also auch ohne geöffneten Browser) | ||
| - | - Anzeige im Browser: einfaches HTML + vorformatierter Text (letzte N Zeilen), plus Start/ | ||
| - | - “API” bleibt in hardware.py: | ||
| - | - app.py bekommt nur neue Routen + startet den Collector-Thread (keine “komische” Logik im HTML) | ||
| - | ================================================================================ | ||
| - | 1) PROJEKTSTRUKTUR (wie bei dir) | ||
| - | course_web/ | ||
| - | |── data/ | ||
| - | | | ||
| - | └── src/ | ||
| - | ├── app.py | ||
| - | ├── core/ | ||
| - | │ | ||
| - | │ | ||
| - | └── html/ | ||
| - | ├── led.html | ||
| - | ├── temp.html | ||
| - | └── history.html | ||
| - | ================================================================================ | ||
| - | |||
| - | ================================================================================ | ||
| - | 2) DATEI: / | ||
| - | ERGÄNZEN (unten dranhängen, | ||
| - | ================================================================================ | ||
| - | - Ziel: simple Methoden: | ||
| - | - history_append(temp, | ||
| - | - history_read(file, | ||
| - | - history_reset(file) | ||
| - | - history_set_recording(True/ | ||
| - | - history_set_interval_minutes(x) | ||
| - | - history_start_collector(get_temperature_func) | ||
| - | - history_status() | ||
| - | |||
| - | <code python / | ||
| - | # ============================= | ||
| - | # Temperaturverlauf (History) | ||
| - | # ============================= | ||
| - | import os | ||
| - | import threading | ||
| - | from datetime import datetime | ||
| - | |||
| - | HISTORY_FILE_DEFAULT = "/ | ||
| - | |||
| - | _history_lock = threading.Lock() | ||
| - | _history_recording = False | ||
| - | _history_interval_seconds = 60 | ||
| - | _history_thread_started = False | ||
| - | |||
| - | |||
| - | def history_status(): | ||
| - | with _history_lock: | ||
| - | return { | ||
| - | " | ||
| - | " | ||
| - | } | ||
| - | |||
| - | |||
| - | def history_set_recording(on: | ||
| - | global _history_recording | ||
| - | with _history_lock: | ||
| - | _history_recording = bool(on) | ||
| - | |||
| - | |||
| - | def history_set_interval_minutes(minutes: | ||
| - | global _history_interval_seconds | ||
| - | if minutes < 1: | ||
| - | raise ValueError(" | ||
| - | with _history_lock: | ||
| - | _history_interval_seconds = int(minutes) * 60 | ||
| - | |||
| - | |||
| - | def history_reset(file_path: | ||
| - | os.makedirs(os.path.dirname(file_path), | ||
| - | with open(file_path, | ||
| - | f.write("" | ||
| - | |||
| - | |||
| - | def history_append(temp_c: | ||
| - | os.makedirs(os.path.dirname(file_path), | ||
| - | ts = datetime.now().strftime(" | ||
| - | with open(file_path, | ||
| - | f.write(f" | ||
| - | |||
| - | |||
| - | def history_read(file_path: | ||
| - | # gibt Liste von Zeilen (Strings) zurück, ohne Parsing-Zauber | ||
| - | try: | ||
| - | with open(file_path, | ||
| - | lines = [ln.rstrip(" | ||
| - | except FileNotFoundError: | ||
| - | return [] | ||
| - | |||
| - | if max_lines is not None and max_lines > 0: | ||
| - | return lines[-max_lines: | ||
| - | return lines | ||
| - | |||
| - | |||
| - | def history_start_collector(get_temp_func, | ||
| - | # get_temp_func: | ||
| - | global _history_thread_started | ||
| - | |||
| - | def _loop(): | ||
| - | while True: | ||
| - | with _history_lock: | ||
| - | rec = _history_recording | ||
| - | sleep_s = _history_interval_seconds | ||
| - | |||
| - | if rec: | ||
| - | try: | ||
| - | t = get_temp_func() | ||
| - | if t is not None: | ||
| - | history_append(float(t), | ||
| - | except Exception: | ||
| - | # bewusst still, damit Anfänger nicht “zufällig” alles crashen | ||
| - | # (wenn gewünscht: print(...) oder logging später) | ||
| - | pass | ||
| - | |||
| - | time.sleep(sleep_s) | ||
| - | |||
| - | with _history_lock: | ||
| - | if _history_thread_started: | ||
| - | return | ||
| - | th = threading.Thread(target=_loop, | ||
| - | th.start() | ||
| - | _history_thread_started = True | ||
| - | </ | ||
| - | |||
| - | WICHTIG: Oben in hardware.py ist bereits " | ||
| - | ================================================================================ | ||
| - | |||
| - | ================================================================================ | ||
| - | 3) DATEI: / | ||
| - | OHNE JAVASCRIPT, NUR HTML (Status + Buttons + Textausgabe) | ||
| - | ================================================================================ | ||
| - | <code html / | ||
| - | < | ||
| - | < | ||
| - | < | ||
| - | < | ||
| - | <meta charset=" | ||
| - | </ | ||
| - | < | ||
| - | |||
| - | < | ||
| - | |||
| - | < | ||
| - | |||
| - | < | ||
| - | <a href="/ | ||
| - | <a href="/ | ||
| - | <a href="/ | ||
| - | |||
| - | < | ||
| - | <a href="/ | ||
| - | <a href="/ | ||
| - | <a href="/ | ||
| - | |||
| - | < | ||
| - | < | ||
| - | |||
| - | < | ||
| - | <a href="/"> | ||
| - | |||
| - | </ | ||
| - | </ | ||
| - | </ | ||
| - | ================================================================================ | ||
| - | |||
| - | ================================================================================ | ||
| - | 4) DATEI: / | ||
| - | ERWEITERN (bestehende Routen bleiben; nur History dazu) | ||
| - | ================================================================================ | ||
| - | - Startup: hardware.init() bleibt | ||
| - | - zusätzlich: | ||
| - | - neue Routen: | ||
| - | - /history | ||
| - | - / | ||
| - | - / | ||
| - | - / | ||
| - | - / | ||
| - | |||
| - | <code python / | ||
| - | from fastapi import FastAPI, HTTPException | ||
| - | from fastapi.responses import HTMLResponse, | ||
| - | from core import hardware | ||
| - | |||
| - | app = FastAPI() | ||
| - | |||
| - | |||
| - | @app.on_event(" | ||
| - | def startup(): | ||
| - | hardware.init() | ||
| - | hardware.history_start_collector(hardware.get_temperature) | ||
| - | |||
| - | |||
| - | def load_template(name, | ||
| - | try: | ||
| - | with open(f" | ||
| - | html = f.read() | ||
| - | except FileNotFoundError: | ||
| - | raise HTTPException(status_code=500, | ||
| - | |||
| - | for key, value in replacements.items(): | ||
| - | html = html.replace(key, | ||
| - | |||
| - | return html | ||
| - | |||
| - | |||
| - | @app.get("/ | ||
| - | def led_page(): | ||
| - | r, y, g = hardware.status() | ||
| - | return HTMLResponse( | ||
| - | load_template(" | ||
| - | " | ||
| - | " | ||
| - | " | ||
| - | }) | ||
| - | ) | ||
| - | |||
| - | |||
| - | @app.get("/ | ||
| - | def set_led(color: | ||
| - | |||
| - | if value not in (0, 1): | ||
| - | raise HTTPException(status_code=400) | ||
| - | |||
| - | if color == " | ||
| - | hardware.setRedLED(value) | ||
| - | elif color == " | ||
| - | hardware.setYellowLED(value) | ||
| - | elif color == " | ||
| - | hardware.setGreenLED(value) | ||
| - | else: | ||
| - | raise HTTPException(status_code=400) | ||
| - | |||
| - | return RedirectResponse(url="/ | ||
| - | |||
| - | |||
| - | @app.get("/ | ||
| - | def temp_page(): | ||
| - | t = hardware.get_temperature() | ||
| - | |||
| - | if t is None: | ||
| - | value = " | ||
| - | else: | ||
| - | value = f" | ||
| - | |||
| - | return HTMLResponse( | ||
| - | load_template(" | ||
| - | " | ||
| - | }) | ||
| - | ) | ||
| - | |||
| - | |||
| - | # ============================= | ||
| - | # Temperaturverlauf (History) | ||
| - | # ============================= | ||
| - | |||
| - | @app.get("/ | ||
| - | def history_page(): | ||
| - | st = hardware.history_status() | ||
| - | lines = hardware.history_read(max_lines=200) | ||
| - | |||
| - | rec_txt = " | ||
| - | int_s = st[" | ||
| - | |||
| - | return HTMLResponse( | ||
| - | load_template(" | ||
| - | " | ||
| - | " | ||
| - | " | ||
| - | " | ||
| - | }) | ||
| - | ) | ||
| - | |||
| - | |||
| - | @app.get("/ | ||
| - | def history_start(): | ||
| - | hardware.history_set_recording(True) | ||
| - | return RedirectResponse(url="/ | ||
| - | |||
| - | |||
| - | @app.get("/ | ||
| - | def history_stop(): | ||
| - | hardware.history_set_recording(False) | ||
| - | return RedirectResponse(url="/ | ||
| - | |||
| - | |||
| - | @app.get("/ | ||
| - | def history_reset(): | ||
| - | hardware.history_reset() | ||
| - | return RedirectResponse(url="/ | ||
| - | |||
| - | |||
| - | @app.get("/ | ||
| - | def history_interval(minutes: | ||
| - | try: | ||
| - | hardware.history_set_interval_minutes(minutes) | ||
| - | except ValueError: | ||
| - | raise HTTPException(status_code=400) | ||
| - | return RedirectResponse(url="/ | ||
| - | </ | ||
| - | |||
| - | HINWEIS: Das ist “ohne Umbau”: nur ergänzt. | ||
| - | ================================================================================ | ||
| - | |||
| - | ================================================================================ | ||
| - | 5) APACHE STARTSEITE index.html | ||
| - | NUR LINK ERGÄNZEN | ||
| - | ================================================================================ | ||
| - | - in / | ||
| - | < | ||
| - | |||
| - | ================================================================================ | ||
| - | 6) APACHE PROXY (WICHTIG: PREFIX /history/ ) | ||
| - | ================================================================================ | ||
| - | In / | ||
| - | |||
| - | < | ||
| - | ProxyPreserveHost On | ||
| - | |||
| - | ProxyPass | ||
| - | ProxyPassReverse /led http:// | ||
| - | |||
| - | ProxyPass | ||
| - | ProxyPassReverse /temp | ||
| - | |||
| - | ProxyPass | ||
| - | ProxyPassReverse / | ||
| - | ProxyPass | ||
| - | ProxyPassReverse / | ||
| - | </ | ||
| - | |||
| - | (Die 2 letzten Zeilen sind für /history ohne Slash; der Prefix ist für / | ||
| - | ================================================================================ | ||
| - | |||
| - | ================================================================================ | ||
| - | 7) SYSTEMD bleibt wie gehabt (course_web.service) | ||
| - | ================================================================================ | ||
| - | |||
| - | ================================================================================ | ||
| - | 8) TEST (ohne “Sonderweg”) | ||
| - | ================================================================================ | ||
| - | 1) Browser: http://< | ||
| - | 2) Start klicken | ||
| - | 3) 1–2 Minuten warten (oder Intervall 1) | ||
| - | 4) Seite neu laden: Zeilen erscheinen | ||
| - | 5) Stop -> es kommen keine neuen Zeilen | ||
| - | 6) Reset -> Datei leer | ||
| - | |||
| - | CLI-Check: | ||
| - | <code bash> | ||
| - | tail -n 20 / | ||
| - | </ | ||
| - | |||
| - | ================================================================================ | ||
| - | DAS IST DIE “SIMPLE METHODEN IN hardware.py”-VARIANTE: | ||
| - | - kein chart.js | ||
| - | - kein javascript | ||
| - | - keine daten “nur wenn browser offen” | ||
| - | - Start/ | ||
| - | - Anzeige grafisch? NEIN (bewusst), weil sonst JS/PNG/Libs nötig wären | ||
| - | |||
| - | Wenn “grafisch” zwingend ist, dann gibt’s genau 2 saubere Anfänger-Optionen: | ||
| - | A) serverseitig PNG (matplotlib) + <img src="/ | ||
| - | B) JS im Browser (Chart.js) | ||
projekt/python_fastapi1.1771674820.txt.gz · Zuletzt geändert: von torsten.roehl
