94 lines
3.4 KiB
Python
94 lines
3.4 KiB
Python
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:
|
|
try:
|
|
self.network_manager.refresh_state()
|
|
return jsonify(self.state.snapshot())
|
|
except Exception as exc:
|
|
self.state.update_status("Status update failed", str(exc))
|
|
return jsonify(self.state.snapshot()), 503
|
|
|
|
@self.app.route("/api/scan", methods=["POST", "GET"])
|
|
def scan() -> Response:
|
|
try:
|
|
ssids = self.network_manager.scan_networks()
|
|
return jsonify({"ok": True, "ssids": ssids})
|
|
except Exception as exc:
|
|
self.state.update_status("Scan failed", str(exc))
|
|
return jsonify({"ok": False, "ssids": [], "message": str(exc)}), 503
|
|
|
|
@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)
|