All docsStellar LinkRF bench · by Stellar Systems v0.1.0

Tools & Utilities

Example Clients

The Python examples (a reusable board client, a chat over the link, a monitor, an RF level sweep) and the CCSDS terminal application.

The examples/ directory holds small programs that talk to a running board. They are meant to be read and copied.

Python examples#

Shell
cd examples
uv run satlink-chat --host 192.168.2.1
uv run satlink-monitor --host 192.168.2.1
uv run satlink-rf-sweep --host <board>

They need Python 3.11 or later; pyzmq and requests are the only dependencies (pyzmq ships libzmq in its wheel).

ModulePurpose
board.pyThe reusable client: the REST API on 8080 and the ZeroMQ bridge on 5555/5556/5557. The part to reuse
chat.pyType a line, it goes through the modem, it comes back
monitor.pyA terminal monitor of telemetry and received frames
rf_sweep.pySweep the transmit level and measure what the link actually delivers (unique payloads pushed and found again), not just whether it locks

The board client#

Python
from pathlib import Path
from satlink_examples import Board

board = Board("192.168.2.1")
print(board.version())
board.register_profile("chat_loopback", Path("profiles/chat_loopback.yaml"))
board.apply_profile("chat_loopback")

with board.frames() as rx:          # subscribe BEFORE sending
    board.send(b"hello from the ground segment")
    frame = rx.next(timeout=5.0)
    if frame:
        print(frame.seq, frame.framing, frame.text())
MethodPurpose
version()GET /api/v1/version
register_profile(name, path)Push a profile file with PUT /api/v1/profiles/{name} (in memory on the board)
apply_profile(name)Apply it
chain()The chain state
set_rf_gain(rx_gain_db=…, tx_gain_db=…)AD9361 gains
send(payload)PUSH one frame; the socket is opened once and kept
frames()Context manager subscribing to frame.rx; next() returns a Frame (seq, framing, payload, text())
telemetry(), telemetry_once()The telemetry topic

The chat#

chat.py pushes its own profile (profiles/chat_loopback.yaml, not in the firmware) and applies it, then sends every line you type and prints what comes back. The profile arms the PL loopback, so every line printed back is your own, after a full trip through the framer, modulator, channel emulator and demodulator. That is a real measurement of the link.

text
board 192.168.2.1 — release r20260915-0646e23cde23 — fpga 0xDEADBEEF
profile 'chat_loopback' pushed from chat_loopback.yaml — registered in memory only
profile 'chat_loopback' applied
chain ready for 'pl-loopback' — anything you type goes out

>> hello from the ground segment
<< [block seq 4] hello from the ground segment

Options: --host, --api-port, --tx-port, --rx-port, --profile <file>, --no-apply (use the chain as it is), --raw (print bytes instead of text).

To chat over the air instead, condition the chain for an RF profile first and skip the apply:

Shell
curl -X POST http://<board>:8080/api/v1/chain/condition \
     -H 'content-type: application/json' \
     -d '{"target":"air","profile":"rf_loopback"}'
uv run satlink-chat --host <board> --no-apply

Things that will otherwise mislead you:

  • The first line of the first run often comes back one character short: on a freshly applied profile the datapath is acquired by that very send, and the leading byte falls in the acquisition transient. Send it again.
  • What arrives is probably a whole DMA block: unless the daemon's link.frame_slot_bytes matches the profile's mtu_bytes, the payload is 4096 bytes holding your line and padding. Frame.text() strips the padding for display; a real client parses Frame.payload with the framing its profile declares.
  • The status carries two gains: rx_gain_db (PL digital scalar) and rx_rf_gain_db (the part's dB). Read the one you set.

A Rust terminal application, one layer up: it composes CCSDS transfer frames (TM 132.0-B, AOS 732.0-B, USLP 732.1-B, and TC 232.0-B inside a 231.0-B CLTU), pushes their octets through the bridge, locates them in the received stream, decodes them and compares.

Shell
cd examples/satlink-ccsds-cli
cargo run -- --host 192.168.2.1                 # the terminal UI
cargo run -- --offline                          # in-memory round trip, no board
cargo run -- --probe "hello" --probe-count 3    # headless, for a script
cargo test

Keys: enter sends, tab moves between message, SCID, VCID and APID, F2 cycles the frame type, F3 toggles the CRC-16 FECF, F5 re-reads the chain, esc quits. With no board reachable it falls back to offline mode and says so: an offline round trip proves the codec and nothing about a link.

It is a standalone crate, deliberately outside the ps/ workspace, so its terminal UI dependencies never enter the daemon's cross-build. It pushes its own profile, profiles/ccsds_tm_loopback.yaml (the firmware also ships one under the same name).

Stellar Link · v0.1.0

↑↓ to moveEnter to open