Files
server-26/README.md
T
2026-04-06 02:47:02 -04:00

12 KiB

DRB Server

Full-stack backend for the Discord Radio Bot (DRB) system. Receives telemetry from SDR edge nodes over MQTT, stores data in Firestore, orchestrates Discord voice bots through a token pool, and serves a real-time admin dashboard.

System Overview

DRB is a distributed SDR (Software Defined Radio) monitoring platform. Edge nodes (small Linux machines with RTL-SDR dongles) decode radio systems and stream audio. The server coordinates those nodes, manages which Discord voice channels receive audio, and stores call history.

Edge Node (client machine)
    │
    ├── MQTT checkin/status/metadata ──► mosquitto ──► c2-core ──► Firestore
    │
    └── MQTT commands ◄─────────────────────────────── c2-core
              │
              └── discord_join ──► edge node joins Discord voice + streams Icecast

Discord User
    └── /join /leave /status /help ──► discord-bot ──► c2-core ──► MQTT ──► edge node

Browser (admin)
    └── frontend ──► Firestore (real-time reads, Firebase Auth)
                └── c2-core REST API (writes, commands, token pool)

Services

Service Container Description Port
mosquitto eclipse-mosquitto MQTT broker — receives telemetry from edge nodes, dispatches commands 1883
c2-core Python 3.11 / FastAPI Command & control API — MQTT subscriber, Firestore writes, node/system/call/token management 8000
discord-bot Python 3.11 / discord.py Server-side Discord slash command bot — /join, /leave, /status, /help
frontend Node.js / Next.js 14 Admin web dashboard — real-time node map, call logs, system & token management 3000

Directory Structure

Server/
├── docker-compose.yml              # Orchestrates all four services
├── Makefile                        # Convenience targets (setup/build/up/down/logs)
│
├── drb-c2-core/                    # FastAPI command & control backend
│   ├── app/
│   │   ├── main.py                 # App entry point — router registration, MQTT startup, node sweeper
│   │   ├── config.py               # Pydantic settings (reads from .env)
│   │   ├── models.py               # Shared Pydantic models (CommandPayload, etc.)
│   │   ├── routers/
│   │   │   ├── nodes.py            # Node CRUD, approve/reject, command dispatch, system assignment
│   │   │   ├── systems.py          # Radio system CRUD (P25/DMR/NBFM configs)
│   │   │   ├── calls.py            # Call log retrieval
│   │   │   ├── tokens.py           # Discord bot token pool — add/delete/list; assign_token/release_token helpers
│   │   │   └── upload.py           # Audio file upload endpoint (called by edge nodes)
│   │   └── internal/
│   │       ├── auth.py             # Auth — Firebase ID token OR service key (Bearer)
│   │       ├── firestore.py        # Async Firestore wrappers (doc_get, doc_set, doc_update, collection_list)
│   │       ├── mqtt_handler.py     # MQTT publisher — commands, config pushes, API key provisioning
│   │       ├── node_sweeper.py     # Background task — marks nodes offline after 90s without heartbeat
│   │       └── storage.py          # GCS audio file uploads
│   ├── scripts/
│   │   └── set_admin.py            # CLI to grant/revoke Firebase admin custom claim
│   ├── gcp-key.json                # GCP service account key — place here, NOT committed to git
│   └── .env.example
│
├── drb-server-discord-bot/         # Discord slash command bot (server-side)
│   ├── app/
│   │   ├── main.py                 # Bot startup, cog loading, guild sync
│   │   ├── config.py               # Settings (Discord token, C2 URL, service key)
│   │   ├── commands/
│   │   │   └── radio.py            # /join (with token autocomplete), /leave, /status, /help
│   │   └── internal/
│   │       └── c2_client.py        # Async HTTP client to c2-core — includes service key auth header
│   └── .env.example
│
└── drb-frontend/                   # Next.js 14 admin dashboard
    ├── app/
    │   ├── dashboard/              # Overview: active node grid + live call feed
    │   ├── map/                    # Leaflet map — node locations, status colors, active call popups
    │   ├── calls/                  # Full call history with duration, audio playback
    │   ├── nodes/                  # Node list + per-node detail (approve, reject, assign system)
    │   ├── systems/                # Radio system CRUD
    │   ├── tokens/                 # Discord bot token pool management
    │   └── login/                  # Firebase Auth login page
    ├── components/
    │   ├── MapView.tsx             # react-leaflet map with status-colored markers
    │   ├── NodeCard.tsx            # Node status card with action buttons
    │   ├── NodeConfigModal.tsx     # Modal for assigning a radio system to a node
    │   ├── CallRow.tsx             # Call history row — talkgroup, duration, audio player
    │   ├── AuthProvider.tsx        # Firebase Auth context + middleware guard
    │   ├── Nav.tsx                 # Sidebar navigation
    │   └── StatusBadge.tsx         # Color-coded status pill (online/recording/offline/unconfigured)
    ├── lib/
    │   ├── firebase.ts             # Firebase JS SDK initialization
    │   ├── c2api.ts                # Fetch wrapper for c2-core REST API (attaches Firebase ID token)
    │   ├── types.ts                # Shared TypeScript types (NodeRecord, CallRecord, SystemRecord, etc.)
    │   ├── useNodes.ts             # Firestore real-time subscription to nodes collection
    │   ├── useCalls.ts             # Firestore real-time subscription to calls (active + history)
    │   └── useSystems.ts           # Firestore real-time subscription to systems collection
    ├── next.config.mjs             # Next.js config — standalone output, transpilePackages for Leaflet
    └── .env.example

Prerequisites

  • Docker + Docker Compose
  • A Firebase project with:
    • Firestore enabled (Native mode)
    • Firebase Authentication enabled (Email/Password provider)
    • A GCP service account key with Firestore + Firebase Auth Admin SDK permissions
  • A Discord application with:
    • One bot token for the server command bot (handles /join, /leave, etc.)
    • One or more additional bot tokens for the pool (edge nodes use these to join voice channels)

Setup

# 1. Copy all env files
make setup

# 2. Place your GCP service account key
cp /path/to/your-service-account-key.json drb-c2-core/gcp-key.json

# 3. Fill in secrets — see tables below
nano drb-c2-core/.env
nano drb-server-discord-bot/.env
nano drb-frontend/.env

# 4. Build and start
make build
make up

# 5. Grant admin access to your Firebase user
cd drb-c2-core
python scripts/set_admin.py grant your@email.com
# Sign out and back in to the frontend for the claim to take effect

Environment Variables

drb-c2-core/.env

Variable Required Description
MQTT_BROKER Yes Hostname of the MQTT broker (default: mosquitto)
MQTT_PORT No MQTT broker port (default: 1883)
FIRESTORE_DATABASE No Firestore database name (default: (default))
GCP_CREDENTIALS_PATH Yes Path to the service account key inside the container (e.g. /secrets/gcp-key.json)
GCS_BUCKET No GCS bucket name for storing call audio recordings
SERVICE_KEY Yes* Shared secret for internal service-to-service auth. Must match C2_SERVICE_KEY in the discord-bot env

*Required for the server Discord bot to authenticate against the C2 API.

drb-server-discord-bot/.env

Variable Required Description
DISCORD_TOKEN Yes Bot token for the server-side slash command bot
C2_URL Yes Internal URL of c2-core (e.g. http://c2-core:8000)
C2_SERVICE_KEY Yes* Must match SERVICE_KEY in drb-c2-core — sent as a Bearer token on all C2 API calls
DEV_GUILD_ID No Discord guild ID to sync slash commands instantly during development

drb-frontend/.env

Variable Required Description
NEXT_PUBLIC_FIREBASE_API_KEY Yes Firebase web API key
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN Yes Firebase auth domain
NEXT_PUBLIC_FIREBASE_PROJECT_ID Yes GCP/Firebase project ID
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET Yes Firebase storage bucket
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID Yes Firebase messaging sender ID
NEXT_PUBLIC_FIREBASE_APP_ID Yes Firebase app ID
NEXT_PUBLIC_FIRESTORE_DATABASE No Firestore database name — must match c2-core (default: (default))
NEXT_PUBLIC_C2_URL Yes C2 API URL reachable from the user's browser

Authentication

Two auth mechanisms are supported on all C2 routes:

  1. Firebase ID tokens — issued by Firebase Auth to browser users. The frontend attaches these automatically via c2api.ts. Required for the dashboard to call the C2 API.
  2. Service key — a static shared secret for internal service calls. The server Discord bot sends this as a Bearer token. Set SERVICE_KEY in c2-core and C2_SERVICE_KEY in the discord-bot to the same value.

Admin-only routes (node approve/reject) additionally require the Firebase user to have an admin: true custom claim:

python drb-c2-core/scripts/set_admin.py grant your@email.com
python drb-c2-core/scripts/set_admin.py revoke your@email.com

Discord Bot Token Pool

Edge nodes join Discord voice channels using bot tokens managed by the server. Add tokens in the Tokens page of the frontend (or POST /tokens). Each token has:

  • A friendly name
  • The actual Discord bot token string (masked in list views)
  • in_use flag and the node it's currently assigned to

Flow when a user runs /join:

  1. Discord bot sends a discord_join command to c2-core with the target guild and channel IDs.
  2. c2-core picks a free token (optionally the one requested via autocomplete), marks it in_use, and forwards the token + channel info to the target edge node over MQTT.
  3. The edge node starts a discord.py bot with that token and joins the voice channel.
  4. The bot streams live Icecast audio — only when radio is actively transmitting (speaking ring on = call active).
  5. On /leave, c2-core releases the token back to the pool.

Node Lifecycle

  1. Edge node starts → sends MQTT checkin → appears as pending in the dashboard
  2. Admin clicks Approve → c2-core generates a random API key, stores it in node_keys, publishes it to the node over MQTT, updates node to approved
  3. Admin assigns a radio system via Config → c2-core pushes the full system config (frequencies, talkgroups, Icecast settings) over MQTT
  4. Edge node writes the config and (re)starts OP25
  5. Node sends online / recording / unconfigured status via 30s MQTT heartbeats
  6. node_sweeper background task marks any node offline after 90s without a heartbeat

Frontend Pages

Page URL Description
Dashboard /dashboard Live node grid + active call stream
Map /map Leaflet map — nodes color-coded by status, click for active call details
Calls /calls Full call history — talkgroup, duration, audio playback
Nodes /nodes Node list; per-node page for approve/reject/assign system
Systems /systems Create and manage radio system configurations
Tokens /tokens Manage Discord bot token pool

Makefile Targets

make setup          — copy .env.example files to .env
make build          — docker compose build
make up             — docker compose up -d
make down           — docker compose down
make logs           — follow all service logs
make logs-c2        — c2-core logs only
make logs-bot       — discord-bot logs only
make logs-frontend  — frontend logs only