TALOS Architecture¶
System Overview¶
TALOS (Tracking, Acquisition, and Link Operation System) is a distributed satellite ground station controller. It coordinates multiple antenna stations in real time from a centralized mission control interface, running SGP4 orbital propagation at 2 Hz with Doppler correction.
The system consists of five core components connected by an MQTT message bus, with an optional streaming pipeline to external YAMCS mission control instances:
Components¶
Core API (core/main.py)
FastAPI web application serving the mission control dashboard, REST API, and SatNOGS data synchronization. Handles user authentication (signed session cookies), organization management, campaign CRUD, station provisioning, and satellite catalog maintenance. All API endpoints are org-scoped with role-based access control. Communicates with the Director and Agents via MQTT, and persists state to PostgreSQL (managed by Alembic migrations).
Director (director/mission_director.py)
Real-time physics engine running a 2 Hz control loop. Reads active campaign assignments from the database, performs SGP4 orbital propagation via Skyfield for each assigned satellite, computes topocentric coordinates (azimuth, elevation) for the assigned station, calculates Doppler-corrected frequencies, predicts upcoming passes, and publishes pointing and tuning commands to agents over org-scoped MQTT topics. Supports concurrent multi-satellite tracking -- different stations can track different satellites simultaneously. Runs as a separate process from the Core API.
Agent (agent/agent.py)
Lightweight edge client deployed on ground station hardware (typically Raspberry Pi). Subscribes to MQTT command topics for its station, translates commands into Hamlib protocol calls, and forwards them to rotctld (antenna rotator) and rigctld (radio receiver) over TCP. Reports station telemetry back to the Director. Additionally manages GNU Radio flowgraph subprocesses for signal demodulation, collecting decoded frames over local ZMQ and publishing them to MQTT for the stream router.
Stream Router (core/stream_router.py)
Background MQTT subscriber running within the Core process. Receives decoded frames from agents, looks up the campaign's MissionLink configuration, and forwards frames to the appropriate external YAMCS instance via UDP. Follows the phasma-operations convention of prepending a 4-byte frame identifier to each datagram.
MQTT Broker (ops/mosquitto/)
Eclipse Mosquitto message broker providing pub/sub communication between all components. Supports both TCP (port 1883) and WebSocket (port 9001) listeners. Configured with username/password authentication and topic ACLs.
PostgreSQL Database Stores organizations, memberships, user accounts, campaigns, per-station assignments, ground station definitions (location, capabilities, API keys), and the satellite catalog (metadata, transmitter info, TLE cache synced from SatNOGS DB). Schema is versioned and managed by Alembic migrations.
Data Flow¶
+---------------------+
| Browser |
| (Mission Control |
| Dashboard) |
+---------+-----------+
|
HTTP / WebSocket
|
+---------v-----------+
| Core API |
| (FastAPI) |
+--+------+------+----+
| | |
DB read/ | | | MQTT publish/subscribe
write | | | (system events, viz relay)
| | |
+--------v--+ | +----------+
| PostgreSQL | | |
+--------+--+ | |
| | |
DB read | | |
| | |
+--------v------v------+ |
| Director |<---------+
| (Physics Engine) |
+--------+-------------+
|
MQTT publish (az/el, freq, session, viz)
|
+------------+-------------+
| | |
+------v----+ +-----v-----+ +----v------+
| Agent 1 | | Agent 2 | | Agent N |
+------+----+ +-----+-----+ +----+------+
| | |
TCP (Hamlib) TCP (Hamlib) TCP (Hamlib)
| | |
+------v----+ +-----v-----+ +----v------+
| rotctld / | | rotctld / | | rotctld / |
| rigctld | | rigctld | | rigctld |
+-----------+ +-----------+ +-----------+
| | |
Hardware Hardware Hardware
(Rotator, (Rotator, (Rotator,
Receiver) Receiver) Receiver)
Flow Description¶
- User opens the dashboard in a browser, manages their organization, creates campaigns, and assigns stations.
- Core API writes configuration to PostgreSQL, publishes
talos/system/refreshto notify the Director. - Director reads active campaign assignments from PostgreSQL, runs the SGP4 propagation loop at 2 Hz for each assigned satellite, and publishes pointing commands (
cmd/rot), tuning commands (cmd/rig), session control (cmd/session), and visualization data (mission/viz) to org-scoped per-station topics. - Agents subscribe to their station-specific command topics, translate commands to Hamlib TCP protocol, and forward to
rotctld/rigctld. Agents publish telemetry back ontelemetry/rot. - Dashboard receives visualization data via the Core API (WebSocket relay) for multi-campaign satellite tracking display with color-coded satellite identification.
MQTT Topic Hierarchy¶
All MQTT topics use the talos/ prefix. As of v0.2, topics support org-scoped prefixes: talos/{org_slug}/gs/{station_id}/.... Legacy non-org-scoped topics (talos/gs/{station_id}/...) are still supported for backward compatibility. Topic definitions live in shared/topics.py.
System Topics¶
| Topic | Direction | QoS | Payload Schema | Description |
|---|---|---|---|---|
talos/system/refresh |
Core -> Director, Dashboard | 1 | SystemRefresh |
System event broadcast (sync complete, campaign change, station update) |
talos/director/heartbeat |
Director -> All | 0 | DirectorHeartbeat |
Periodic heartbeat proving the physics loop is alive |
Org-Scoped Station Control Topics¶
| Topic | Direction | QoS | Payload Schema | Description |
|---|---|---|---|---|
talos/{org_slug}/gs/{station_id}/info |
Agent -> Director | 1 (retained) | StationInfo |
Station handshake and registration announcement |
talos/{org_slug}/gs/{station_id}/cmd/config |
Director -> Agent | 1 (retained) | StationConfig |
Station configuration (rotator/rig addresses, capabilities) |
talos/{org_slug}/gs/{station_id}/cmd/session |
Director -> Agent | 1 | SessionCommand |
Start/stop a tracking session for a satellite pass |
talos/{org_slug}/gs/{station_id}/cmd/rot |
Director -> Agent | 0 | RotatorCommand |
Rotator position command (azimuth, elevation in degrees) |
talos/{org_slug}/gs/{station_id}/cmd/rig |
Director -> Agent | 0 | RigCommand |
Radio tuning command (frequency in Hz, modulation mode) |
talos/{org_slug}/gs/{station_id}/schedule |
Director -> Agent | 1 | PassSchedule |
Upcoming pass schedule for this station |
Telemetry Topics¶
| Topic | Direction | QoS | Payload Schema | Description |
|---|---|---|---|---|
talos/{org_slug}/gs/{station_id}/telemetry/rot |
Agent -> Director | 0 | RotatorTelemetry |
Actual rotator position readback (az, el) |
Visualization Topics¶
| Topic | Direction | QoS | Payload Schema | Description |
|---|---|---|---|---|
talos/{org_slug}/mission/viz |
Director -> Dashboard | 0 | MissionViz |
Satellite position, footprint, ground track for live display (multi-campaign) |
talos/{org_slug}/mission/select |
Dashboard -> Director | 1 | TransmitterSelect |
Manual transmitter selection (frequency/mode change) |
Subscription Wildcards¶
| Pattern | Subscriber | Purpose |
|---|---|---|
talos/{org_slug}/gs/{station_id}/cmd/# |
Agent | All commands for a specific station |
talos/{org_slug}/gs/+/info |
Director | Registration from any station in the org |
talos/{org_slug}/gs/+/telemetry/rot |
Dashboard | Telemetry from any station in the org |
talos/{org_slug}/gs/+/schedule |
Dashboard | Schedule updates from any station in the org |
Legacy Topics (Deprecated)¶
Non-org-scoped topics (e.g., talos/gs/{station_id}/cmd/rot) are still accepted for backward compatibility but will be removed in a future release. All new deployments should use org-scoped topics.
Database Schema¶
The PostgreSQL schema is defined via SQLModel ORM in core/database.py and managed by Alembic migrations (see migrations/). All tables use auto-incrementing integer primary keys unless otherwise noted.
Entity Relationship¶
+--------------+ +------------+
| Organization | | User |
+--------------+ +------------+
| id (PK) | | id (PK) |
| name | | email |
| slug (unique)| | created_at |
| created_at | +-----+------+
+------+-------+ |
| |
| +------------+ |
+--->| Membership |<---+
| +------------+
| | id (PK) |
| | user_id (FK)
| | org_id (FK)|
| | role | (owner / operator / viewer)
| | created_at |
| +------------+
|
+---> Station (org_id FK)
|
+---> Campaign
| id (PK)
| org_id (FK)
| name, norad_id, satnogs_id
| priority, status, transmitter_uuid
|
+---> Assignment
| id (PK)
| campaign_id (FK)
| station_id (FK)
| status (active/paused/completed)
+----------------+ +----------------+
| SatelliteCache | | Transmitter |
+----------------+ +----------------+
| sat_id (PK) |<------| uuid (PK) |
| norad_id | 1:N | sat_id (FK) |
| name | | description |
| tle_json | | downlink_low |
| updated_at | | mode, alive |
+----------------+ +----------------+
Tables¶
Organization
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | Integer | PK, auto-increment | Organization identifier |
| name | String | Not null | Organization display name |
| slug | String | Unique, not null | URL-safe identifier (used in API paths and MQTT topics) |
| created_at | DateTime | Default: now | Creation timestamp |
A default organization is auto-created for new users who do not belong to any organization.
Membership
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | Integer | PK, auto-increment | Membership identifier |
| user_id | Integer | FK -> User, not null | Member user |
| org_id | Integer | FK -> Organization, not null | Parent organization |
| role | String | Not null, default: operator | Role: owner, operator, or viewer |
| created_at | DateTime | Default: now | Membership creation timestamp |
Unique constraint on (user_id, org_id) prevents duplicate memberships.
User
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | Integer | PK, auto-increment | User identifier |
| String | Unique, not null | User email address | |
| created_at | DateTime | Default: now | Account creation timestamp |
Station
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | Integer | PK, auto-increment | Internal station identifier |
| station_id | String | Unique, not null | MQTT identifier (e.g., gs_athens_01) |
| org_id | Integer | FK -> Organization | Owning organization |
| name | String | Not null | Human-readable station name |
| lat | Float | Not null | Latitude (decimal degrees) |
| lng | Float | Not null | Longitude (decimal degrees) |
| alt | Float | Not null | Altitude (meters above sea level) |
| api_key | String | Not null | Station authentication key |
| network_id | Integer | SatNOGS Network station ID (for metadata sync) | |
| created_at | DateTime | Default: now | Provisioning timestamp |
Campaign
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | Integer | PK, auto-increment | Campaign identifier |
| org_id | Integer | FK -> Organization, not null | Owning organization |
| name | String | Not null | Campaign display name |
| satnogs_id | String | SatNOGS satellite identifier | |
| norad_id | Integer | Not null | NORAD catalog number |
| priority | Integer | Default: 5 | Priority level (1-10, higher = more important) |
| status | String | Default: draft | Lifecycle status: draft, scheduled, active, completed, cancelled |
| transmitter_uuid | String | Selected transmitter UUID (null = auto-select) | |
| created_at | DateTime | Default: now | Campaign creation timestamp |
| updated_at | DateTime | Default: now | Last modification timestamp |
Assignment
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | Integer | PK, auto-increment | Assignment identifier |
| campaign_id | Integer | FK -> Campaign, not null | Parent campaign |
| station_id | Integer | FK -> Station, not null | Assigned ground station |
| status | String | Default: active | Status: active, paused, completed |
| created_at | DateTime | Default: now | Assignment creation timestamp |
Unique constraint on (station_id, campaign_id) prevents duplicate station-campaign assignments.
Mission (Deprecated)
The Mission model is retained for backward compatibility but is replaced by Campaign. New code should use Campaign exclusively.
| Column | Type | Constraints | Description |
|---|---|---|---|
| id | Integer | PK, auto-increment | Mission identifier |
| sat_id | String | Not null | SatNOGS satellite identifier |
| norad_id | Integer | Not null | NORAD catalog number |
| name | String | Not null | Satellite display name |
| is_active | Boolean | Default: false | Currently active mission flag (deprecated) |
| created_at | DateTime | Default: now | Mission creation timestamp |
SatelliteCache
| Column | Type | Constraints | Description |
|---|---|---|---|
| sat_id | String | PK | SatNOGS satellite identifier |
| norad_id | Integer | Not null | NORAD catalog number |
| name | String | Not null | Satellite name |
| tle_json | JSON | Cached TLE orbital elements | |
| updated_at | DateTime | Default: now | Last sync from SatNOGS DB |
SatelliteCache is shared across all organizations (orbital data is not org-specific).
Transmitter
| Column | Type | Constraints | Description |
|---|---|---|---|
| uuid | String | PK | SatNOGS transmitter UUID |
| sat_id | String | FK -> SatelliteCache | Parent satellite identifier |
| description | String | Transmitter description | |
| downlink_low | Integer | Downlink frequency in Hz | |
| mode | String | Modulation mode (e.g., FM, CW, AFSK) | |
| alive | Boolean | Default: true | Active status from SatNOGS |
RBAC Model¶
| Role | Permissions |
|---|---|
| owner | Full access: manage members, delete organization, all operator permissions |
| operator | Manage stations, create and activate campaigns, manage assignments |
| viewer | Read-only access to dashboard, campaigns, and station status |
Security Architecture¶
Security hardening has been applied as part of the monorepo consolidation (Phase 1). The following describes the current and target security posture.
Authentication¶
- Session cookies signed with
itsdangerous(URLSafeTimedSerializer), loaded from environment-sourcedSECRET_KEY - Magic link login with expiring tokens (10-minute TTL via PyJWT)
- HttpOnly, Secure, SameSite=Lax cookie attributes in production
- No hardcoded secrets in source code; all credentials via environment variables and
.envfile (gitignored)
MQTT Security¶
- Mosquitto configured with username/password authentication (
MQTT_USER/MQTT_PASS) - Anonymous access disabled in broker configuration
- Target state: per-client MQTT credentials (Director, each Agent, Core API)
- Target state: MQTT TLS on port 8883 with topic ACLs restricting Agent access to their own station prefix
Network Security¶
- PostgreSQL port not exposed to host (internal Docker network only)
SECRET_KEY,MQTT_PASS, and database credentials stored in.envfile, never committed- Target state: separate Docker networks for frontend (API + broker) and backend (database + director)
- Target state: Agent validates rotator addresses against local allowlist
Authorization¶
- Role-based access control (RBAC) with three roles: owner, operator, viewer
- All API endpoints are org-scoped (
/api/org/{slug}/...) and require authentication - Operators can manage stations and campaigns within their organization
- Viewers have read-only dashboard access
- Owners can manage organization membership and delete the organization
- Station ownership is scoped to the organization, not individual users
Deployment Topology¶
TALOS deploys as a Docker Compose stack defined in ops/docker-compose.yml. Database schema changes are managed by Alembic; migrations run automatically on container startup or can be applied manually with alembic upgrade head.
+-------------------------------------------------------------------+
| Docker Host |
| |
| +------------------+ +------------------+ |
| | PostgreSQL | | Mosquitto | |
| | (db) | | (broker) | |
| | Port: 5432 | | Ports: 1883, | |
| | (internal) | | 9001 | |
| +--------+---------+ +--------+---------+ |
| | | |
| | talos_net (bridge network) |
| | | |
| +--------v---------+ +--------v---------+ |
| | Core API | | Director | |
| | (core) | | (director) | |
| | Port: 8000 | | (no ports) | |
| +------------------+ +------------------+ |
| |
+-------------------------------------------------------------------+
| |
| HTTP :8000 | MQTT :1883
| |
+------v------+ +-------v--------+-------+
| Browser | | Agent 1 | Agent 2 | ...
| (Dashboard) | | (Pi @ Site A) (Pi @ Site B)
+-------------+ +----------------+-------+
Docker Compose Services¶
| Service | Image | Exposed Ports | Purpose |
|---|---|---|---|
db |
postgres:15-alpine | 5432 (internal only) | PostgreSQL database |
broker |
eclipse-mosquitto:2 | 1883 (MQTT), 9001 (WebSocket) | MQTT message bus |
core |
Built from core/Dockerfile |
8000 (HTTP) | FastAPI web API and dashboard |
director |
Built from core/Dockerfile |
None | Director: multi-campaign physics engine |
Environment Variables¶
| Variable | Used By | Description |
|---|---|---|
DATABASE_URL |
core, director | PostgreSQL connection string |
BROKER_HOST |
core, director | MQTT broker hostname |
SECRET_KEY |
core | Session cookie signing key |
MQTT_USER |
core, director, agent | MQTT authentication username |
MQTT_PASS |
core, director, agent | MQTT authentication password |
POSTGRES_USER |
db | PostgreSQL superuser name |
POSTGRES_PASSWORD |
db | PostgreSQL superuser password |
POSTGRES_DB |
db | Database name |
Scalability¶
| Scale | Status | Key Bottleneck | Mitigation Path |
|---|---|---|---|
| 10 stations | Works well | Silent failures are the main risk | Add logging and health checks (Phase 2) |
| 100 stations | Performance degrades | Physics loop exceeds 0.5s time budget; pass prediction stalls the realtime loop | Vectorize SGP4 with dSGP4, move pass prediction to background thread, cache ground track computation |
| 1000 stations | Not feasible | Single-threaded Python loop, MQTT fan-out volume, memory pressure | Shard Director by region, evaluate NATS for messaging backbone, batch propagation with GPU acceleration |
Key Scaling Strategies¶
- Vectorized propagation: Replace per-satellite Skyfield calls with dSGP4 (ESA) for batch/GPU-accelerated SGP4
- Background pass prediction: Decouple pass prediction from the realtime 2 Hz loop to avoid blocking
- Ground track caching: Eliminate redundant SGP4 calls for visualization (currently ~48 wasted calls per tick)
- Regional sharding: Split the Director by geographic region so each instance handles a subset of stations
- MQTT 5.0: Shared subscriptions for horizontal scaling of consumers
Hardware-in-the-Loop Testing¶
TALOS supports end-to-end testing with real ground station hardware. A
Raspberry Pi provisioned with a USRP B200mini SDR and Yaesu G-5500
rotator runs as a dedicated GitLab CI runner, executing hardware tests
on every push. The test-hil CI job validates the complete data path:
MQTT broker -> Agent -> rotctld/rigctld -> physical hardware -> telemetry readback
See HIL Testing for provisioning and configuration details.