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:
nearxos
2026-02-02 10:34:25 +02:00
parent 78f7ccc6db
commit 9dc35a57a2
14 changed files with 1224 additions and 115 deletions

View File

@@ -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) ----