Enhance 5G modem management with integrated web GUI and connection control
- Introduced a web GUI for managing 5G connections, replacing the standalone 5g-router service. - Updated scripts to ensure exclusive access to the AT port, preventing conflicts. - Improved troubleshooting documentation in 5G_MODEM_TROUBLESHOOTING.md, adding checks for processes using the AT port. - Enhanced connection management in the web app, including auto-connect and detailed status APIs. - Updated installation scripts to reflect changes in service management and dependencies.
This commit is contained in:
164
web/app.py
164
web/app.py
@@ -1,12 +1,17 @@
|
||||
"""
|
||||
Alpine 5G Router – Web GUI
|
||||
Login: admin/support with different permissions. Run: python app.py or gunicorn.
|
||||
Rev: 1 (see REVISION in repo root)
|
||||
Rev: 2 (see REVISION in repo root)
|
||||
|
||||
Now includes integrated 5G connection management (replaces standalone 5g-router service).
|
||||
"""
|
||||
|
||||
import atexit
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
from flask import (
|
||||
@@ -48,6 +53,16 @@ from db import (
|
||||
routes_update,
|
||||
)
|
||||
|
||||
# Connection management (replaces 5g-router service)
|
||||
from at_client import ATClient
|
||||
from connection_manager import ConnectionConfig, ConnectionManager
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
||||
)
|
||||
|
||||
app = Flask(__name__, static_folder="static", static_url_path="", template_folder="templates")
|
||||
app.secret_key = os.environ.get("SECRET_KEY", "change-me-in-production-alpine-5g")
|
||||
app.config["MAX_CONTENT_LENGTH"] = 512 * 1024 # 512KB max for config/log uploads
|
||||
@@ -56,11 +71,44 @@ CONFIG_PATH = "/etc/5g-router.conf"
|
||||
IPTABLES_RULES = "/etc/iptables/rules.v4"
|
||||
LOG_5G = "/var/log/5g-router.log"
|
||||
LOG_SPEEDTEST = "/var/log/speedtest-5g.log"
|
||||
CONNECT_SCRIPT = "/usr/local/bin/connect-5g.sh"
|
||||
CONNECT_SCRIPT = "/usr/local/bin/connect-5g.sh" # Kept for manual use
|
||||
STATUS_SCRIPT = "/usr/local/bin/status-5g.sh"
|
||||
MODEM_STATUS_SCRIPT = "/usr/local/bin/modem-status-at.sh"
|
||||
MODEM_STATUS_SCRIPT = "/usr/local/bin/modem-status-at.sh" # Deprecated: using ATClient
|
||||
SPEEDTEST_SCRIPT = "speedtest-cli"
|
||||
|
||||
# ---- Connection Manager (replaces 5g-router service) ----
|
||||
_conn_config = ConnectionConfig.from_file(CONFIG_PATH)
|
||||
_at_client = ATClient(port=_conn_config.at_port)
|
||||
_conn_manager = ConnectionManager(_at_client, _conn_config)
|
||||
_startup_connect_done = False
|
||||
|
||||
|
||||
def _startup_connect():
|
||||
"""Auto-connect on first request (deferred startup)."""
|
||||
global _startup_connect_done
|
||||
if _startup_connect_done:
|
||||
return
|
||||
_startup_connect_done = True
|
||||
|
||||
def do_connect():
|
||||
logging.info("Auto-connecting 5G on startup...")
|
||||
_conn_manager.connect()
|
||||
if _conn_config.watchdog_interval > 0:
|
||||
_conn_manager.start_watchdog()
|
||||
|
||||
# Run in background thread so startup doesn't block
|
||||
threading.Thread(target=do_connect, name="5g-startup", daemon=True).start()
|
||||
|
||||
|
||||
@atexit.register
|
||||
def _shutdown():
|
||||
"""Cleanup on shutdown."""
|
||||
try:
|
||||
_conn_manager.stop_watchdog()
|
||||
_at_client.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _current_user():
|
||||
if not session.get("username"):
|
||||
@@ -207,36 +255,33 @@ def api_me():
|
||||
def api_status():
|
||||
if not can_view_status(_current_user().get("role")):
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
|
||||
# Trigger auto-connect on first status request (deferred startup)
|
||||
_startup_connect()
|
||||
|
||||
try:
|
||||
out = subprocess.run(
|
||||
[STATUS_SCRIPT, "--json"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
if out.returncode != 0:
|
||||
return jsonify({"error": "status script failed", "stderr": out.stderr}), 500
|
||||
data = json.loads(out.stdout)
|
||||
# Enrich with modem AT status (AT_COMMANDS_REFERENCE.md)
|
||||
if Path(MODEM_STATUS_SCRIPT).exists():
|
||||
try:
|
||||
mod_out = subprocess.run(
|
||||
[MODEM_STATUS_SCRIPT],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
cwd="/",
|
||||
)
|
||||
if mod_out.returncode == 0 and mod_out.stdout.strip():
|
||||
mod_data = json.loads(mod_out.stdout)
|
||||
data["modem"] = mod_data
|
||||
except (FileNotFoundError, json.JSONDecodeError, subprocess.TimeoutExpired):
|
||||
data["modem"] = {}
|
||||
else:
|
||||
data["modem"] = {}
|
||||
# Get connection and modem status from ConnectionManager (native Python)
|
||||
full_status = _conn_manager.get_full_status()
|
||||
|
||||
# Also include basic system status from status script for interface info
|
||||
data = {}
|
||||
try:
|
||||
out = subprocess.run(
|
||||
[STATUS_SCRIPT, "--json"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
if out.returncode == 0:
|
||||
data = json.loads(out.stdout)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Merge connection manager status
|
||||
data["connection"] = full_status.get("connection", {})
|
||||
data["modem"] = full_status.get("modem", {})
|
||||
|
||||
return jsonify(data)
|
||||
except subprocess.TimeoutExpired:
|
||||
return jsonify({"error": "timeout"}), 504
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@@ -458,21 +503,58 @@ def api_firewall_apply():
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
# ---- API: 5G restart (support + admin) ----
|
||||
# ---- API: 5G Connection Control (support + admin) ----
|
||||
@app.route("/api/5g/restart", methods=["POST"])
|
||||
@_require_login
|
||||
def api_5g_restart():
|
||||
"""Restart 5G connection (disconnect then reconnect)."""
|
||||
if not can_restart_5g(_current_user().get("role")):
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
try:
|
||||
subprocess.Popen(
|
||||
[CONNECT_SCRIPT],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
return jsonify({"ok": True, "message": "Restart initiated"})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
def do_restart():
|
||||
_conn_manager.disconnect()
|
||||
_conn_manager.connect()
|
||||
|
||||
# Run in background so API returns immediately
|
||||
threading.Thread(target=do_restart, name="5g-restart", daemon=True).start()
|
||||
return jsonify({"ok": True, "message": "Restart initiated"})
|
||||
|
||||
|
||||
@app.route("/api/5g/connect", methods=["POST"])
|
||||
@_require_login
|
||||
def api_5g_connect():
|
||||
"""Manually connect 5G."""
|
||||
if not can_restart_5g(_current_user().get("role")):
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
|
||||
def do_connect():
|
||||
_conn_manager.connect()
|
||||
|
||||
threading.Thread(target=do_connect, name="5g-connect", daemon=True).start()
|
||||
return jsonify({"ok": True, "message": "Connect initiated"})
|
||||
|
||||
|
||||
@app.route("/api/5g/disconnect", methods=["POST"])
|
||||
@_require_login
|
||||
def api_5g_disconnect():
|
||||
"""Manually disconnect 5G."""
|
||||
if not can_restart_5g(_current_user().get("role")):
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
|
||||
def do_disconnect():
|
||||
_conn_manager.disconnect()
|
||||
|
||||
threading.Thread(target=do_disconnect, name="5g-disconnect", daemon=True).start()
|
||||
return jsonify({"ok": True, "message": "Disconnect initiated"})
|
||||
|
||||
|
||||
@app.route("/api/5g/status")
|
||||
@_require_login
|
||||
def api_5g_connection_status():
|
||||
"""Get detailed 5G connection status."""
|
||||
if not can_view_status(_current_user().get("role")):
|
||||
return jsonify({"error": "Forbidden"}), 403
|
||||
return jsonify(_conn_manager.get_full_status())
|
||||
|
||||
|
||||
# ---- API: Speedtest (support + admin) ----
|
||||
|
||||
Reference in New Issue
Block a user