All docsStellar LinkRF bench · by Stellar Systems v0.1.0

Control & Automation

Telemetry and Metrics

The telemetry sample and what each field measures, watched registers, the live streams, Prometheus metrics, events and logs, IQ captures and the spectrum.

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#

FieldUnitSource and meaning
evm% of full scaleQPSK or BPSK demodulator EVM (Q10.6). Lower is better. Not meaningful on GMSK
snr_db, mer_dbdBFrom 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
berratioPost-FEC: fec_fail / (fec_corr + fec_fail). Absent without coding
ber_asmratioPre-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_hzHzCarrier loop frequency: freq_q × symbol_rate / 2³², using the profile's symbol rate
phase_errrawThe 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_dbmdBm (uncalibrated)The PL's RSSI meter. Monotonic, but its absolute offset has not been calibrated (about 13 dB off)
part_rssi_dbdB below full scaleThe AD9361's own RSSI, referred to its input. Smaller is stronger. Absent when the part cannot be read
part_rx_gain_dbdBThe AD9361's RX gain, read back from the part
agc_gainlinearThe 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
lockboolThe timing loop's lock
carrier_lockboolThe carrier loop's lock, a different fact
tx_framescounttx.framer.FRAME_CNT: frames the modem actually framed. The traffic proof
tx_egress_samplescountSamples that left the TX chain. Frames climbing with this at zero means the egress is closed and blocks are discarded
rx_frames, rx_frame_errorscountDeframer frame count and soft-match (one wrong marker bit) count
rx_symbolscountSymbols 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_raterawThe channel emulator's active bank, i.e. what the datapath was really running under
watchedmapThe 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:

RegisterWhy
tx.framer.frame_cnt, rx_demod.deframer.frame_cnt, rx_demod.deframer.err_cntFrames sent, delimited, soft-matched
rx_demod.deframer.statusBits [4:3]: the constellation rotation the marker matched under
tx.scrambler.ctrl, rx_demod.deframer.scrambleThe scrambler at both ends
rx_demod.deframer.ctrl, rx_demod.deframer.sync_wordWhat the deframer is hunting for
rx_demod.carrier_recovery.status, rx_demod.timing_recovery.statusBoth lock flags, separately
rx_demod.demod_gmsk.statusThe GMSK demodulator's only observable
rx_demod.demod_qpsk.sig_pwr, .err_pwr, .win_cntThe components of the EVM
rx_demod.carrier_recovery.cfo, .phase_err, .loop_bwCarrier loop raw state
rx_demod.agc.status, rx_demod.slicer.sym_cntAGC 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_statusEgress, 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#

StreamContent
ws://<board>:8080/ws/metricsEvery 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/eventsRuntime events
ws://<board>:8080/ws/pl/registersEvery 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:

FamilyContent
satlink_evm, satlink_snr_db, satlink_cfo_hz, satlink_rssi_dbm, satlink_lock, satlink_carrier_phase_errLink 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_cfoPL counters and raw values
satlink_iio_bytes_total, satlink_datapath_tx_blocks_totalDatapath traffic
satlink_events_totalRuntime 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#

RouteContent
GET /api/v1/events, /events/latestRecent runtime events (satlinkctl events --n 20); DELETE clears them
GET /api/v1/logsStructured log query
GET /api/v1/pl/status, /pl/profile-sequencer, /pl/interruptsShell identity, sequencer state, interrupt status (POST /pl/interrupts/ack to acknowledge)
GET /api/v1/system/status, /system/clocks, /system/capabilitiesServices, 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#

RouteContent
GET /api/v1/monitor/rssiThe PL RSSI meter
GET /api/v1/monitor/powerThe PL power meter (input and output taps around the DUC)
GET /api/v1/monitor/fft?channel=rxOne 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=200Spectrum 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#

RoutePurpose
POST /api/v1/iq/capturesStart 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}/downloadRaw 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.

Stellar Link · v0.1.0

↑↓ to moveEnter to open