Initial commit — DRB client (edge node) stack

Includes edge-node (FastAPI/MQTT/Discord voice), op25-container (SDR decoder),
and icecast (audio streaming).
This commit is contained in:
Logan
2026-04-05 19:01:51 -04:00
commit 1a9c92b6db
47 changed files with 2496 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
from fastapi import APIRouter, HTTPException
from app.config import settings
from app.models import SystemConfig
from app.internal.op25_client import op25_client
from app.internal.config_manager import load_node_config, save_node_config, apply_system_config
from app.internal.call_recorder import call_recorder
from app.internal.discord_radio import radio_bot
from app.internal.metadata_watcher import metadata_watcher
router = APIRouter(prefix="/api", tags=["api"])
@router.get("/status")
async def get_status():
node_cfg = load_node_config()
op25_status = await op25_client.status()
return {
"node_id": settings.node_id,
"node_name": settings.node_name,
"lat": settings.node_lat,
"lon": settings.node_lon,
"configured": node_cfg.configured,
"assigned_system_id": node_cfg.assigned_system_id,
"is_recording": call_recorder.is_recording,
"active_tgid": metadata_watcher.current_tgid,
"active_call_id": metadata_watcher.active_call_id,
"discord_connected": radio_bot.is_connected,
"icecast_url": (
f"http://{settings.icecast_host}:{settings.icecast_port}{settings.icecast_mount}"
),
"op25": op25_status,
}
@router.post("/op25/start")
async def start_op25():
ok = await op25_client.start()
if not ok:
raise HTTPException(500, "Failed to start OP25")
return {"ok": True}
@router.post("/op25/stop")
async def stop_op25():
ok = await op25_client.stop()
if not ok:
raise HTTPException(500, "Failed to stop OP25")
return {"ok": True}
@router.get("/config")
async def get_config():
return load_node_config()
@router.post("/config/system")
async def set_system_config(config: SystemConfig):
"""
Apply a system config locally — called by the web UI or pushed by C2.
Writes the OP25 config and persists the node config.
"""
node_cfg = load_node_config()
node_cfg.assigned_system_id = config.system_id
node_cfg.system_config = config
node_cfg.configured = True
save_node_config(node_cfg)
apply_system_config(config)
return {"ok": True}
@router.post("/discord/join")
async def discord_join(guild_id: int, channel_id: int):
ok = await radio_bot.join(guild_id, channel_id)
if not ok:
raise HTTPException(500, "Failed to join voice channel")
return {"ok": True}
@router.post("/discord/leave")
async def discord_leave():
await radio_bot.leave()
return {"ok": True}
+12
View File
@@ -0,0 +1,12 @@
from pathlib import Path
from fastapi import APIRouter
from fastapi.responses import HTMLResponse
router = APIRouter(tags=["ui"])
_TEMPLATE = Path(__file__).parent.parent / "templates" / "index.html"
@router.get("/", response_class=HTMLResponse)
async def index():
return _TEMPLATE.read_text()