A profile is a YAML file describing a complete radio configuration: frequency, gains, modulation, symbol rate, framing and coding. Applying it reconfigures the modem and the AD9361 without touching the bitstream.
A complete example#
This is dsl/profiles/rf_loopback.yaml without its comments: QPSK, uncoded, over the real RF
path with an external cable loopback.
version: "satlink.profile/v1"
metadata:
name: "rf_loopback"
display_name: "RF loopback (bench, external cable)"
description: "QPSK uncoded over the real RF path — TX1 -> pad -> splitter -> RX1"
vendor: "Stellar Systems"
family: "bring-up"
revision: "1.0"
author: "F. du Cray"
tags: ["bring-up", "loopback", "rf", "test"]
radio:
rf_port: "RF1"
duplex: "full"
center_frequency_hz: 437325000
tx_enabled: true
rx_enabled: true
loopback: false
tx_power_dbm: -20
rx_gain_db: 20
phy:
modulation: "QPSK"
symbol_rate_baud: 39062.5
samples_per_symbol: 4
encoding: "nrz"
framing:
preamble:
type: "fill"
pre_symbols: 512
post_symbols: 2048
sync_word:
value_hex: "1ACFFC1D"
mtu_bytes: 16
scrambler:
enabled: false
fec:
type: "none"
interleaver:
enabled: false
packet:
protocol: "raw"
mtu_bytes: 16Field reference#
The Effect column says what each field actually does. Fields marked descriptive are parsed, validated for type, stored and displayed, and reach no register: changing them changes nothing on the air.
Top level#
| Field | Required | Effect |
|---|---|---|
version | yes | Must start with satlink.profile/ (satlink.profile/v1) |
metadata | yes | See below |
radio | yes | RF front end |
phy | yes | Modulation and symbol rate |
framing | yes | Frames, marker, scrambler, coding, preamble, transfer frames |
packet | no | Descriptive: packets are built per request by the PS, not configured here |
bus | no | Descriptive: the host link is the daemon's configuration |
timing | no | Descriptive: nothing enforces turnaround or timeouts |
constraints | no | Descriptive: no rate limiter or duty-cycle enforcement exists |
observability | no | Descriptive: the daemon publishes a fixed metric set |
metadata#
| Field | Required | Effect |
|---|---|---|
name | yes | The identifier used by apply, scenarios (profile_ref) and campaigns. Profiles are keyed by this name, not by file name |
display_name, description, author, tags | no | Shown in listings and the web console |
vendor, family, revision | no | Descriptive |
radio#
| Field | Type | Effect |
|---|---|---|
center_frequency_hz | integer, > 0 | Tunes both AD9361 LOs below this centre and sets both NCOs to bring the signal back onto it (see RF Fundamentals). Also written to a descriptive chan_cfg register |
tx_enabled | bool, default false | Opens (true) or closes the TX egress (tx.egress.CTRL). With the egress closed the modem discards every block it is given |
rx_enabled | bool, default true | Enables the RX ingress |
loopback | bool, default false | Writes the PL loopback mux (GLOBAL_CTRL[0]) on every apply. true routes TX through the channel emulator into RX; the AD9361 is out of the path |
rx_gain_db | number | Sets the AD9361 RX gain in dB and switches its AGC to manual |
tx_power_dbm | number | Applied as the AD9361 TX attenuation when ≤ 0 (0 = full drive, down to −89.75). A positive value is not applied and the apply says so: the part cannot produce a positive output power on its own |
rf_port | string | Descriptive. The modem always uses TX1/RX1 |
duplex | half / full | Descriptive. Chain conditioning puts the ENSM in FDD for on-air work |
phy#
| Field | Type | Effect |
|---|---|---|
modulation | BPSK, QPSK, GMSK (OQPSK, MSK, π/4-DQPSK, FSK, GFSK are refused) | Selects and enables one modulator and the matching demodulator, sets the deframer's bits per symbol |
symbol_rate_baud | number, one of the ladder | Sets the DUC interpolation and DDC decimation, sizes the AD9361 analog filters, the LO offset and the carrier loop's sweep bounds |
samples_per_symbol | integer | Must be 4 or absent |
bt | number | GMSK only; required for GMSK and must be 0.5 |
freq_deviation_hz | integer | Descriptive (FSK is not implemented) |
encoding | string | Descriptive; line coding is not implemented |
framing#
| Field | Type | Effect |
|---|---|---|
mtu_bytes | integer, default 256 | Framer FRAME_LEN; the deframer's length is derived from it and the code (see Modulation & Coding) |
sync_word.value_hex | 32-bit hex | The ASM, written to the marker inserter and the deframer. Absent means no marker is inserted and the receiver cannot frame |
scrambler.enabled | bool | Enables the CCSDS randomiser at both ends |
scrambler.seed | integer | 8-bit seed, default 0xFF |
scrambler.polynomial | string | Descriptive; the polynomial is fixed |
fec.type | none, conv, rs, rs+conv | Configures fec_tx and fec_rx. ldpc, turbo, polar are accepted by the schema but not implemented: the chain is configured uncoded |
fec.rate | string | Required when type is not none; informative |
preamble | object | Preamble and inter-frame fill: type (fill or pattern), value_hex, pre_symbols, post_symbols. Absent disables the fill (see Modulation & Coding) |
interleaver | object | Descriptive; there is no interleaver |
transfer_frame | object | CCSDS transfer frames composed by the link service: type (tm, aos, uslp), scid, vcid, fecf (see Modulation & Coding) |
What an apply does#
POST /api/v1/profiles/{name}/apply (satlinkctl profile apply <name>):
- Validate the profile (rules below). A refusal names the field and the reason.
- Translate it into a list of PL register writes (about 55): loopback mux, channel reset to pass-through, DDC, AGC, matched filter, timing and carrier recovery, slicer, demodulators, deframer, FEC, framer, scrambler, marker inserter, fill, modulators, DUC, TX egress, RSSI.
- Stage and commit them through the profile sequencer, which waits for a safe point.
- Tune the AD9361: analog filter bandwidths, LOs and NCO offsets, RX gain and TX
attenuation. Failures here do not fail the apply (the modem is configured); they are reported
in the result's
tuningsentence. - Record a profile revision in the store.
An illustrative result:
{
"profile_name": "rf_loopback",
"cfg_epoch": 12,
"tuning": "RX gain 20 dB (AGC → manual), TX attenuation -20 dB; analog BW 200 kHz; LOs tuned to 437292041 Hz with a +32959 Hz NCO offset — centre 437325000 Hz (+0 Hz from the profile's 437325000), leakage 32959 Hz off the band"
}Some consequences worth knowing:
- An apply resets the channel emulator to pass-through and re-opens the TX egress when
tx_enabledis true, including after the RF kill switch. - An apply performed while traffic flows can time out in WAIT_SAFE. After a successful loopback measurement the datapath may stay busy and later applies time out until a reboot: apply before you measure, never after.
- A later manual change (frequency, gain, register) marks the profile dirty:
GET /api/v1/profiles/activereturns{"name": "...", "dirty": true, "dirty_reason": "..."}. - A scenario applies its own profile at the start of every run (see Scenarios).
Validation rules#
| Rule | Message names |
|---|---|
version starts with satlink.profile/ | version |
center_frequency_hz > 0 | radio.center_frequency_hz |
symbol_rate_baud is an available ladder rate | the nearest available rate |
samples_per_symbol is 4 or absent | phy.samples_per_symbol |
GMSK has bt = 0.5 | phy.bt |
| Modulation is BPSK, QPSK or GMSK | why the others are not implemented |
sync_word.value_hex parses as 32-bit hex | framing.sync_word.value_hex |
fec.rate present when fec.type is not none | framing.fec.rate |
rs or rs+conv requires mtu_bytes = 223 | mtu_bytes |
Preamble pattern ≥ 8 bits from every sync-word image, mean ≤ ¼ full scale, post_symbols ≠ 0, lengths ≤ 65535 | framing.preamble |
Transfer frame: mtu_bytes set and divides 4096, SCID/VCID fit the frame type, no RS | framing.transfer_frame, framing.mtu_bytes |
Managing profiles#
satlinkctl profile list
satlinkctl profile get rf_loopback # the profile as JSON
satlinkctl profile apply rf_loopback
satlinkctl profile activeThe daemon loads every *.yaml directly inside [daemon] profiles_dir
(/opt/satlink/dsl/profiles on the board) at startup; files in subdirectories are not loaded,
and a file that fails to parse is skipped with a warning in the log.
Pushing a profile without a firmware release#
PUT /api/v1/profiles/{name} registers a profile sent as YAML (or JSON) in the request body:
curl -X PUT --data-binary @my_profile.yaml \
-H 'content-type: text/plain' \
http://192.168.2.1:8080/api/v1/profiles/my_profile{"name": "my_profile", "replaced": false, "persisted": false,
"note": "registered in memory only — it is gone at the next daemon restart, and nothing was written to profiles_dir"}The profile is held in memory only and disappears at the next daemon restart. Keep your file
as the source of truth. To ship a profile permanently, add it to dsl/profiles/ and build a
release.
GET /api/v1/profiles/{name}/revisions lists when a profile was applied (newest first), from the
run store.
Shipped profiles#
| Profile | Path | Modulation | Rate (baud) | Coding | Notes |
|---|---|---|---|---|---|
pl_loopback | PL loopback | QPSK | 19 531.25 | none | The bench's known-good baseline; 16-byte frames, fill enabled |
rf_loopback | air | QPSK | 39 062.5 | none | Same as pl_loopback over the external cable loopback |
raw_tx | air | BPSK | 19 531.25 | none | No marker, no scrambler: pushed bytes are what goes out. Transmit-only, for spectrum work |
ccsds_tm_loopback | PL loopback | QPSK | 19 531.25 | none | 1024-byte frames with CCSDS TM transfer frames and FECF |
hc_qpsk and hc_* | PL loopback | QPSK/BPSK/GMSK | various | none/conv/rs/rs+conv | Health-check arms, each differing from hc_qpsk in one thing |
hc_air_qpsk, hc_air_bpsk, hc_air_gmsk | air | QPSK/BPSK/GMSK | 19 531.25 | none | On-air health-check arms |
hc_dev_qpsk | device loopback | QPSK | 19 531.25 | none | AD9361 digital loopback arm |
spaceinventor_uhf_csp | air | GMSK | 19 531.25 | none | A mission-style profile (CSP over UHF) |
spaceinventor_uhf_csp_loopback | PL loopback | QPSK | 19 531.25 | none | Its bench counterpart |
endurosat_uhf | air | GMSK | 19 531.25 | rs | Declares tx_power_dbm: 27, which is refused at apply |
Mission profiles cannot match their spacecraft's real 9600-baud links: the modem's lowest rate is 19 531.25 baud. The profile files say so in their comments.
Writing your own profile#
- Start from
pl_loopback.yaml(PL loopback) orrf_loopback.yaml(air), which have a measured baseline behind them. - Change one thing at a time and prove it with the PL loopback before going on air.
- For air profiles: enable the scrambler, set
radio.loopback: false, setrx_gain_dbfor your cabling (see RF Fundamentals) andtx_power_dbmas a negative attenuation. - Keep
mtu_bytesa divisor of 4096 unless you use Reed-Solomon (which forces 223). - Push it with
PUT /api/v1/profiles/{name}, apply it, and read thetuningsentence andsatlinkctl chain status.