The examples/ directory holds small programs that talk to a running board. They are meant
to be read and copied.
Python examples#
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).
| Module | Purpose |
|---|---|
board.py | The reusable client: the REST API on 8080 and the ZeroMQ bridge on 5555/5556/5557. The part to reuse |
chat.py | Type a line, it goes through the modem, it comes back |
monitor.py | A terminal monitor of telemetry and received frames |
rf_sweep.py | Sweep the transmit level and measure what the link actually delivers (unique payloads pushed and found again), not just whether it locks |
The board client#
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())| Method | Purpose |
|---|---|
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.
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 segmentOptions: --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:
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-applyThings 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_bytesmatches the profile'smtu_bytes, the payload is 4096 bytes holding your line and padding.Frame.text()strips the padding for display; a real client parsesFrame.payloadwith the framing its profile declares. - The status carries two gains:
rx_gain_db(PL digital scalar) andrx_rf_gain_db(the part's dB). Read the one you set.
satlink-ccsds-cli#
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.
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 testKeys: 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).