satlinkd polls the modem and the AD9361 every [telemetry] poll_interval_ms (200 ms, 5 Hz,
on the board). Each poll produces one sample. Samples feed the web console, the
/ws/metrics stream, the link service's telemetry topic, the Prometheus exporter, and the
report of any scenario running at the time.
The telemetry sample#
| Field | Unit | Source and meaning |
|---|---|---|
evm | % of full scale | QPSK or BPSK demodulator EVM (Q10.6). Lower is better. Not meaningful on GMSK |
snr_db, mer_db | dB | From the QPSK demodulator's windowed signal and error powers over 1024 symbols: mer = 10·log10(S/E), snr = 10·log10((S−E)/E). Absent until a window completes, on BPSK/GMSK, or when the error power is zero |
ber | ratio | Post-FEC: fec_fail / (fec_corr + fec_fail). Absent without coding |
ber_asm | ratio | Pre-FEC estimate on the sync word: ERR_CNT / (32 × FRAME_CNT). Saturates near 3·10⁻², because a marker with two wrong bits is lost, not counted |
cfo_hz | Hz | Carrier loop frequency: freq_q × symbol_rate / 2³², using the profile's symbol rate |
phase_err | raw | The carrier loop's phase detector output, signed and unitless: comparable with itself over time ("hunting or settled"), nothing more. Zero is a real value |
rssi_dbm | dBm (uncalibrated) | The PL's RSSI meter. Monotonic, but its absolute offset has not been calibrated (about 13 dB off) |
part_rssi_db | dB below full scale | The AD9361's own RSSI, referred to its input. Smaller is stronger. Absent when the part cannot be read |
part_rx_gain_db | dB | The AD9361's RX gain, read back from the part |
agc_gain | linear | The PL AGC's gain (Q4.12, range 0 to just under 16). Near 16 means the receiver is starved, not that there is no signal |
lock | bool | The timing loop's lock |
carrier_lock | bool | The carrier loop's lock, a different fact |
tx_frames | count | tx.framer.FRAME_CNT: frames the modem actually framed. The traffic proof |
tx_egress_samples | count | Samples that left the TX chain. Frames climbing with this at zero means the egress is closed and blocks are discarded |
rx_frames, rx_frame_errors | count | Deframer frame count and soft-match (one wrong marker bit) count |
rx_symbols | count | Symbols the demodulator decided. It moves even when no frame syncs, separating "no signal" from "signal the deframer cannot delimit" |
chan_ctrl, chan_atten, chan_awgn_amp, chan_nco_freq, chan_burst_rate | raw | The channel emulator's active bank, i.e. what the datapath was really running under |
watched | map | The registers listed in [telemetry] watch_registers, by name, on the same tick |
source | — | pl when read from live registers, stub when every register read zero (no bitstream) |
Counters are raw and cumulative, cleared only by a control write or a reboot. Compute rates by differencing two samples; a counter that did not move is then visible, which a pre-computed delta would hide.
How to read the lock flags#
Readings taken while nothing is transmitted describe the silence, not your link: agc_gain near
16 and lock: false with no traffic is correct behaviour.
Watched registers#
[telemetry] watch_registers in satlinkd.toml lists extra PL registers, by catalogued name,
sampled on every tick. They appear under watched in each sample, reach the ZeroMQ telemetry
stream, and are assertable in scenarios as reg.<name>. The shipped configuration watches, among
others:
| Register | Why |
|---|---|
tx.framer.frame_cnt, rx_demod.deframer.frame_cnt, rx_demod.deframer.err_cnt | Frames sent, delimited, soft-matched |
rx_demod.deframer.status | Bits [4:3]: the constellation rotation the marker matched under |
tx.scrambler.ctrl, rx_demod.deframer.scramble | The scrambler at both ends |
rx_demod.deframer.ctrl, rx_demod.deframer.sync_word | What the deframer is hunting for |
rx_demod.carrier_recovery.status, rx_demod.timing_recovery.status | Both lock flags, separately |
rx_demod.demod_gmsk.status | The GMSK demodulator's only observable |
rx_demod.demod_qpsk.sig_pwr, .err_pwr, .win_cnt | The components of the EVM |
rx_demod.carrier_recovery.cfo, .phase_err, .loop_bw | Carrier loop raw state |
rx_demod.agc.status, rx_demod.slicer.sym_cnt | AGC saturation, receive-side traffic proof |
tx.egress.cnt, tx.egress.status, common.seq.seq_state, common.seq.seq_err_code, shell.irq.status, shell.axi_ctrl.rf_status | Egress, sequencer, interrupts, ADC capture tap |
Three extra keys come from the AD9361's DAC core rather than the PL when it is present:
ad9361.dac.clk_count, ad9361.dac.unf and ad9361.dac.if_busy.
An unknown name is reported as an error at startup and dropped. Keep the list short: each entry is one AXI read per tick.
Live streams#
| Stream | Content |
|---|---|
ws://<board>:8080/ws/metrics | Every sample, as metrics_bulk messages |
tcp://<board>:5557 (ZeroMQ SUB, topic telemetry) | Every sample, JSON, with seq and ts_ms (see ZeroMQ Link Service) |
ws://<board>:8080/ws/events | Runtime events |
ws://<board>:8080/ws/pl/registers | Every PL register write, as it happens |
Prometheus#
The daemon exports Prometheus metrics on [observability] metrics_bind (port 9090), and the
same families as JSON at GET /api/v1/metrics. Families include:
| Family | Content |
|---|---|
satlink_evm, satlink_snr_db, satlink_cfo_hz, satlink_rssi_dbm, satlink_lock, satlink_carrier_phase_err | Link metrics |
satlink_pl_agc_gain, satlink_pl_framer_frames, satlink_pl_deframer_frames, satlink_pl_deframer_errors, satlink_pl_slicer_symbols, satlink_pl_egress_samples, satlink_pl_ingress_samples, satlink_pl_ingress_overflow, satlink_pl_ingress_saturate, satlink_pl_evm_raw, satlink_pl_carrier_cfo | PL counters and raw values |
satlink_iio_bytes_total, satlink_datapath_tx_blocks_total | Datapath traffic |
satlink_events_total | Runtime events |
satlink_system_* | CPU, load, memory, uptime, temperature, voltage |
Nothing scrapes the board by default; the telemetry sample and scenario reports are the primary record.
Events and logs#
| Route | Content |
|---|---|
GET /api/v1/events, /events/latest | Recent runtime events (satlinkctl events --n 20); DELETE clears them |
GET /api/v1/logs | Structured log query |
GET /api/v1/pl/status, /pl/profile-sequencer, /pl/interrupts | Shell identity, sequencer state, interrupt status (POST /pl/interrupts/ack to acknowledge) |
GET /api/v1/system/status, /system/clocks, /system/capabilities | Services, clocks, capabilities |
On the board the daemon logs to /var/log/satlinkd.log (RAM disk). With
[pl_driver] trace_writes and trace_to_kmsg on (the shipped setting), every PL register write
is also logged by name to the kernel log, so it reaches dmesg and the serial console on the same
timeline as kernel messages.
RSSI, power and spectrum#
| Route | Content |
|---|---|
GET /api/v1/monitor/rssi | The PL RSSI meter |
GET /api/v1/monitor/power | The PL power meter (input and output taps around the DUC) |
GET /api/v1/monitor/fft?channel=rx | One spectrum frame |
PUT /api/v1/monitor/fft/{channel} | Switch spectrum analysis on or off for rx or tx |
ws://<board>:8080/ws/monitor/fft?channel=rx&interval_ms=200 | Spectrum frames, pushed |
The spectrum is computed by the PS from blocks captured through the ADC capture DMA. Analysis is
off by default per channel, because each frame costs a DMA block and a transform. When there
are no bins, the response says why (switched off, no capture path, source timed out). A
synthesised spectrum is only produced when [monitor] synthetic_fft = true, for UI work without a
board, and is then labelled synthetic: true.
IQ captures#
| Route | Purpose |
|---|---|
POST /api/v1/iq/captures | Start a capture: samples, optional sample_rate_hz, center_frequency_hz, tags, annotations, profile_ref |
GET /api/v1/iq/captures, /iq/captures/{id} | List and describe captures |
GET /api/v1/iq/captures/{id}/download | Raw IQ: little-endian int16 I and Q pairs |
DELETE /api/v1/iq/captures/{id} | Delete one |
Captures come from the same ADC capture tap as the spectrum. A synthetic fallback exists only
with [iq] allow_synthetic = true and is marked synthetic: true. Captures are stored on the
board and do not survive a reboot unless downloaded.