satlinkd serves a REST API and a set of WebSocket streams on port 8080. Everything the CLI
and the web console do goes through it, so anything they can do, a script can do.
Base URL and versioning#
http://<board>:8080/api/v1/... REST
ws://<board>:8080/ws/... WebSocket streamsEvery response carries an x-api-version: v1 header. The OpenAPI 3 document is served at
GET /api/v1/openapi.json, with an interactive Swagger UI at /docs. The
API Reference section of this site is generated from the same document.
curl -s http://192.168.2.1:8080/api/v1/health
curl -s http://192.168.2.1:8080/api/v1/version
curl -s http://192.168.2.1:8080/api/v1/system/capabilities | jq '.symbol_rates'Errors#
Errors are JSON with a machine-readable code and a human message:
{"code": "conflict", "message": "a scenario is already running: ..."}| HTTP | code | Typical cause |
|---|---|---|
| 400 | bad_request | Unparseable YAML, a bad duration, a framing that cannot hold alignment |
| 404 | not_found | Unknown profile, scenario or run; no datapath in this daemon |
| 409 | conflict | A scenario is already running; the chain cannot be conditioned; the datapath is held by someone else; a scenario exists and overwrite was not given |
| 422 | validation_failed | Semantic validation failed |
| 501 | not_implemented | The daemon lacks the resource (for example no scenarios_dir) |
| 500 | internal | Anything else; the message says what |
| 504 | — | A capture or loopback did not sustain the requested blocks in time |
Refusals are written to be actionable: a 409 from a transmit or a scenario launch names the first unmet prerequisite and the command that clears it.
Authentication and CORS#
Authentication is off by default. When the daemon is built with its auth feature and
[api] auth_required = true, every mutating request needs a JWT (HS256, signed with
[api] auth_secret) as a bearer token; satlinkctl sends it from --token or SATLINK_TOKEN.
CORS origins are set by [api] cors_origins. The shipped configuration allows every origin
(["*"]), so the web console can run from any host.
Request bodies are limited to [api] max_body_bytes (20 MiB by default). Payloads sent as hex
double in size; for large transfers use the /ws/tx stream.
Long-running work#
Some requests start work that outlives them:
| Request | Returns | Follow with |
|---|---|---|
POST /api/v1/scenarios/{name}/run | 202, the run id | GET /api/v1/scenario-runs/{run_id}, ws://…/ws/scenario-runs/{run_id}, then GET /api/v1/reports/{run_id} |
POST /api/v1/campaigns/{name}/run | the execution id | GET /api/v1/campaign-runs/{id}, ws://…/ws/campaign-runs/{id} |
| Any of the above | — | GET /api/v1/jobs, GET /api/v1/jobs/{job_id} |
Only one scenario runs at a time. A second launch is refused with 409 naming the run in progress; it is never queued.
WebSocket streams#
| Stream | Content |
|---|---|
/ws/metrics | Telemetry snapshots (metrics_bulk messages) at the telemetry cadence |
/ws/events | Runtime events (profile_applied, lock lost, …) |
/ws/pl/registers | Every PL register write, as it happens (observability only) |
/ws/monitor/fft?channel=rx&interval_ms=200 | The spectrum, pushed |
/ws/scenario-runs/{run_id} | A scenario run in flight: events, samples and assertion verdicts as they happen |
/ws/campaign-runs/{id} | A campaign execution as it advances |
/ws/tx | A persistent feed for pushing packets into the modem (see Datapath and Transmission) |
/ws/frames?framing=slot&slot_len=256&payload=true | The receive stream in real time (see Datapath and Transmission) |
WebSocket streams are not described in the OpenAPI document.
Routes served but absent from the OpenAPI document#
The following routes are served by the daemon and used by the CLI and the web console, but are not yet merged into the published OpenAPI document, so they do not appear in the API Reference:
| Route | Purpose | Documented in |
|---|---|---|
GET /api/v1/radio/rf | AD9361 rate, device loopback, DAC source, tuning readiness | Chain Conditioning |
POST /api/v1/radio/rf/tune | Set the AD9361 rate and tune the digital interface at that rate ({"rate_sps": 8000000}) | Chain Conditioning |
POST /api/v1/radio/rf/mode/rf | Route through the AD9361's internal digital loopback | Chain Conditioning |
POST /api/v1/radio/rf/mode/pl | Back to the PL loopback | Chain Conditioning |
POST /api/v1/radio/nco-offset | Set the RX/TX NCO offsets ({"rx_hz": …, "tx_hz": …}) | RF Fundamentals |
GET /api/v1/pl/regs, GET/POST /api/v1/pl/regs/{name} | List, read and write catalogued PL registers by name | PL Register Map |
PUT /api/v1/monitor/fft/{channel} | Switch spectrum analysis on or off for rx or tx | Telemetry and Metrics |
POST /api/v1/scenarios | Create or replace a scenario from YAML | Scenarios |
POST /api/v1/radio/rf/mode/air, /radio/rf/calibrate and /radio/rf/kill are in the document.
Talking to the API from a script#
import requests
BOARD = "http://192.168.2.1:8080/api/v1"
print(requests.get(f"{BOARD}/version").json())
r = requests.post(f"{BOARD}/chain/condition",
json={"target": "pl", "profile": "pl_loopback"})
r.raise_for_status()
run = requests.post(f"{BOARD}/scenarios/hc_qpsk/run").json()
print(run)The Python client in examples/src/satlink_examples/board.py wraps the REST API and the ZeroMQ
bridge; see Example Clients.