On PoE sites the MIPI-DSI panel can lose its init on a supply dip while the RK3566 keeps running; the UI may also hang or exit. Three layers: - hardware watchdog: systemd RuntimeWatchdogSec=20s (system.conf.d) - GUI: SdWatchdog pings systemd per swapped frame (WATCHDOG_USEC=30s, enabled by the binary so older versions run under the same unit) and writes run/qt.frame; buttontask.service gets Restart=always, NotifyAccess=main, TimeoutAbortSec=10s - bt-displaywatch (root, buttontask-displaywatch.service): kernel DRM/DSI timeouts, eth link down->up (PoE dip hint), stale UI frames, restart storms -> panel re-init (stop UI, fb0 blank 4->0, start UI), repeated -> reboot (max 3/day, never during OTA); incident log in /var/log/buttontask/ Manual recovery: hold one finger 10 s in the bottom-left corner (panels are single-touch). Delivered by OTA via bt-update sync_system; the web service runs `bt-update sync-system` at start for devices updated by an older bt-update. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
96 lines
2.9 KiB
Python
96 lines
2.9 KiB
Python
#!/usr/bin/env python3
|
|
"""ButtonTask v2 web configurator (Flask).
|
|
|
|
Application factory that wires together the modular blueprints:
|
|
- auth : login / logout / session
|
|
- api : buttons, layout, background, feedback, settings, icons
|
|
- network : Ethernet configuration via nmcli
|
|
- updater : OTA software update with rollback
|
|
|
|
Reads/writes the same JSON config used by the Qt5 application.
|
|
"""
|
|
import os
|
|
import time
|
|
|
|
from flask import Flask, g, jsonify, render_template, request
|
|
|
|
from api import api_bp
|
|
from auth import auth_bp, load_or_create_secret, login_required
|
|
from config_store import list_icons, load_config
|
|
from network import network_bp
|
|
from rest_api import rest_bp
|
|
from sensors import sensors_bp
|
|
from system_info import (apply_saved_brightness, record_request, sync_system_units,
|
|
system_bp)
|
|
from updater import current_version, updater_bp
|
|
|
|
|
|
def create_app():
|
|
app = Flask(__name__, template_folder="templates", static_folder="static")
|
|
app.secret_key = load_or_create_secret()
|
|
app.config.update(
|
|
SESSION_COOKIE_HTTPONLY=True,
|
|
SESSION_COOKIE_SAMESITE="Lax",
|
|
PERMANENT_SESSION_LIFETIME=60 * 60 * 8, # 8 hours
|
|
MAX_CONTENT_LENGTH=512 * 1024 * 1024, # 512 MB upload cap (OTA packages)
|
|
)
|
|
|
|
app.register_blueprint(auth_bp)
|
|
app.register_blueprint(api_bp)
|
|
app.register_blueprint(network_bp)
|
|
app.register_blueprint(updater_bp)
|
|
app.register_blueprint(system_bp)
|
|
app.register_blueprint(sensors_bp)
|
|
app.register_blueprint(rest_bp)
|
|
|
|
# Restore LCD brightness from config (best-effort; needs bt-brightness + sudoers).
|
|
apply_saved_brightness()
|
|
# Install watchdog units if an older bt-update skipped them (idempotent).
|
|
sync_system_units()
|
|
|
|
@app.before_request
|
|
def _request_start():
|
|
g._req_start = time.time()
|
|
|
|
@app.after_request
|
|
def _request_log(response):
|
|
started = getattr(g, "_req_start", None)
|
|
if started is not None:
|
|
record_request(
|
|
request.method,
|
|
request.path,
|
|
response.status_code,
|
|
(time.time() - started) * 1000.0,
|
|
request.remote_addr or "",
|
|
)
|
|
return response
|
|
|
|
@app.route("/")
|
|
@login_required
|
|
def index():
|
|
cfg = load_config()
|
|
return render_template("index.html", config=cfg, icons=list_icons(),
|
|
version=current_version())
|
|
|
|
@app.route("/healthz")
|
|
def healthz():
|
|
# Unauthenticated, lightweight: used by the OTA health-check.
|
|
return jsonify({"ok": True, "ts": time.time(),
|
|
"version": current_version().get("version")})
|
|
|
|
return app
|
|
|
|
|
|
app = create_app()
|
|
|
|
|
|
def main():
|
|
cfg = load_config()
|
|
port = int(cfg.get("settings", {}).get("webPort", 8080))
|
|
host = os.environ.get("BUTTONTASK_HOST", "0.0.0.0")
|
|
app.run(host=host, port=port, debug=False)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|