Add Pi Zero headless serial bridge with AP portal and daily RTC-based logs
This commit is contained in:
85
src/webapp.py
Normal file
85
src/webapp.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import json
|
||||
import queue
|
||||
from typing import Any, Dict
|
||||
|
||||
from flask import Flask, Response, jsonify, render_template, request, stream_with_context
|
||||
from waitress import serve
|
||||
|
||||
from app_state import AppState
|
||||
from network_manager import NetworkManager
|
||||
from serial_bridge import SerialBroadcaster
|
||||
|
||||
|
||||
class WebPortal:
|
||||
def __init__(
|
||||
self,
|
||||
state: AppState,
|
||||
network_manager: NetworkManager,
|
||||
broadcaster: SerialBroadcaster,
|
||||
template_folder: str = "../templates",
|
||||
static_folder: str = "../static",
|
||||
) -> None:
|
||||
self.state = state
|
||||
self.network_manager = network_manager
|
||||
self.broadcaster = broadcaster
|
||||
self.app = Flask(__name__, template_folder=template_folder, static_folder=static_folder)
|
||||
self._register_routes()
|
||||
|
||||
def _register_routes(self) -> None:
|
||||
@self.app.route("/")
|
||||
def index() -> str:
|
||||
return render_template("index.html")
|
||||
|
||||
@self.app.route("/serial")
|
||||
def serial_page() -> str:
|
||||
return render_template("serial.html")
|
||||
|
||||
@self.app.route("/api/status", methods=["GET"])
|
||||
def status() -> Response:
|
||||
self.network_manager.refresh_state()
|
||||
return jsonify(self.state.snapshot())
|
||||
|
||||
@self.app.route("/api/scan", methods=["POST", "GET"])
|
||||
def scan() -> Response:
|
||||
ssids = self.network_manager.scan_networks()
|
||||
return jsonify({"ok": True, "ssids": ssids})
|
||||
|
||||
@self.app.route("/api/connect", methods=["POST"])
|
||||
def connect() -> Response:
|
||||
payload: Dict[str, Any] = request.get_json(silent=True) or {}
|
||||
ssid = (payload.get("ssid") or "").strip()
|
||||
password = payload.get("password") or ""
|
||||
|
||||
ok, message = self.network_manager.connect_to_wifi(ssid, password)
|
||||
if not ok:
|
||||
self.state.update_status("Connect failed", message)
|
||||
try:
|
||||
self.network_manager.start_ap()
|
||||
except Exception:
|
||||
pass
|
||||
return jsonify({"ok": False, "message": message}), 400
|
||||
|
||||
self.network_manager.refresh_state()
|
||||
return jsonify({"ok": True, "message": message})
|
||||
|
||||
@self.app.route("/events/serial")
|
||||
def serial_events() -> Response:
|
||||
@stream_with_context
|
||||
def generate():
|
||||
q = self.broadcaster.subscribe()
|
||||
try:
|
||||
yield "retry: 2000\n\n"
|
||||
while True:
|
||||
try:
|
||||
line = q.get(timeout=15)
|
||||
data = json.dumps({"line": line})
|
||||
yield f"data: {data}\n\n"
|
||||
except queue.Empty:
|
||||
yield ": keepalive\n\n"
|
||||
finally:
|
||||
self.broadcaster.unsubscribe(q)
|
||||
|
||||
return Response(generate(), mimetype="text/event-stream")
|
||||
|
||||
def run(self, host: str = "0.0.0.0", port: int = 80) -> None:
|
||||
serve(self.app, host=host, port=port, threads=6)
|
||||
Reference in New Issue
Block a user