All docsStellar LinkRF bench · by Stellar Systems v0.1.0

Control & Automation

Datapath and Transmission

Claiming the modem, pushing packets, stream and slot framing, flushing, the RX feed, and capture and loopback measurements.

The datapath is the DMA path between satlinkd and the modem: one IIO device for transmit (satlink-modem-tx) and one for receive (satlink-modem-rx). This page covers how bytes get in and out.

Claiming the modem#

The daemon must acquire the datapath (open both IIO devices) before it can transmit or receive. On the board, [datapath] autostart = false: the daemon claims nothing until something asks it to, so bring-up tools that open the IIO devices directly are not locked out.

Shell
satlinkctl datapath status       # who holds the modem, and is the TX egress open
satlinkctl datapath enable       # claim it, then read back whether the claim took
satlinkctl datapath disable      # release it
RoutePurpose
GET /api/v1/datapathCurrent state
POST /api/v1/datapath/enableAcquire
POST /api/v1/datapath/disableRelease

chain up acquires the datapath as its last step. The measurement commands (capture, loopback) claim it themselves and want it exclusively: release it first, or they return 409.

Blocks and latency#

Data moves in DMA blocks of 4096 useful bytes (16 384 bytes on the wire, see Bench Architecture). The block assembler only submits full blocks, so:

  • a packet does not go on the air until its block is full;
  • at a low packet rate you must flush, or the packets wait indefinitely;
  • an RX block is returned only when it is entirely full, and the receive side always comes back a little short (the first frame of a freshly armed chain is lost to acquisition). Flush two blocks whenever you want to see the result come back.
Shell
satlinkctl tx flush --blocks 2          # POST /api/v1/datapath/flush {"blocks": 2}

The flush reply gives the useful bytes handed to the DMA (padding included; zero when nothing was pending) and tx_chain_running, which says whether the egress is open. There is deliberately no automatic flush timer: padding goes on the air, and spending it is the caller's decision.

Framing: stream or slot#

The modem's framer cuts the byte stream at FRAME_LEN without looking at packet boundaries. How you lay packets out decides whether their boundaries survive:

ModeBoundariesOverheadUse for
streamNone: the receiver gets the bytes re-cut at FRAME_LENZeroBulk payloads that carry their own delimiting
slotOne packet per slot, recoverableThe fill of each slotA packet feed (CSP, CCSDS)

A slot is [length: 2 bytes, big-endian][packet][padding…], exactly slot_len bytes. The length prefix is needed because CSP has no length field: without it the padding would be indistinguishable from payload.

Two constraints on slot_len, both enforced by refusal:

  • it must equal the PL's FRAME_LEN (the profile's framing.mtu_bytes without coding);
  • it must divide 4096: 1024, 512, 256, 128, 64, 16 work; 223 does not, so slot framing is unavailable under Reed-Solomon.

Pushing packets#

One-off: POST /api/v1/datapath/transmit#

JSON
{
  "packets": [
    {"protocol": "csp",
     "header": {"priority": "normal", "src": 10, "dst": 1, "dport": 7, "sport": 31,
                "flags": {"crc32": false, "xtea": false, "rdp": false, "hmac": false}},
     "payload_hex": "01020304"},
    {"protocol": "raw", "payload_hex": "deadbeef"}
  ],
  "framing": {"mode": "slot", "slot_len": 256}
}
ProtocolEffect
rawThe bytes are passed through untouched
cspSatLink builds a CSP packet (4-byte header) from header and the payload
ccsdsSatLink builds a CCSDS space packet (6-byte primary header)

Packet shapes (the same JSON is accepted by /ws/tx and returned by the frames store):

JSON
{"protocol": "raw", "payload_hex": "deadbeef"}

{"protocol": "csp",
 "header": {"priority": "normal", "src": 10, "dst": 1, "dport": 7, "sport": 31,
            "flags": {"crc32": false, "xtea": false, "rdp": false, "hmac": false}},
 "payload_hex": "01020304"}

{"protocol": "ccsds",
 "header": {"version": 0, "kind": "tc", "secondary_header": false, "apid": 42,
            "seq_flags": "standalone", "seq_count": 0, "data_length": 3},
 "payload_hex": "01020304"}

Every CSP header field is required: priority is critical, high, normal or low; addresses are 5 bits (0 to 31) and ports 6 bits (0 to 63). CSP payloads are limited to 256 bytes. In slot mode the encoded packet, header included, must fit in slot_len − 2 bytes. For CCSDS, kind is tm or tc and seq_flags is continuation, first, last or standalone.

The reply reports packets, useful_bytes, wire_bytes, boundaries_preserved, tx_chain_running and block_useful_bytes. It returns once the data is queued, not once it has been transmitted. When the chain cannot carry an emission the request is refused with 409, naming the first missing prerequisite and the command that clears it; "force": true transmits anyway (for bench work on an unconditioned chain).

From the CLI, which builds the request and always flushes:

Shell
satlinkctl tx send --protocol csp --src 10 --dst 1 --hex 01020304
satlinkctl tx send --file payload.bin --framing slot --slot-len 256

Sustained feed: ws://<board>:8080/ws/tx#

For continuous traffic, keep one WebSocket open rather than issuing one HTTP request per packet. The first message fixes the framing for the session:

JSON
{"framing": {"mode": "slot", "slot_len": 256}}
JSON
{"type": "ready", "framing": {"mode": "slot", "slot_len": 256},
 "boundaries_preserved": true, "block_useful_bytes": 4096}

Then push as often as you like:

JSON
{"packets": [{"protocol": "raw", "payload_hex": "01020304"}]}
JSON
{"type": "queued", "packets": 1, "useful_bytes": 256}

A malformed packet is answered {"type": "rejected", "reason": "…"} and the socket stays open.

The ZeroMQ PUSH socket of the link service is the other way to feed the modem continuously.

Receiving#

Real time: ws://<board>:8080/ws/frames#

text
/ws/frames?framing=slot&slot_len=256&payload=true
ParameterMeaning
framingslot to parse slots, stream (default) to receive whole blocks
slot_lenSlot pitch for slot
payloadInclude the payload as hex (off by default: it doubles the volume)

Messages:

typeWhen
packetOne recovered packet per slot (slot, len, payload)
fillA padding slot; never reported as a zero-length packet
unparsedA slot whose length prefix does not fit; reported, not dropped
blockStream mode: one DMA block, 4096 useful bytes
laggedThe subscriber fell behind by blocks blocks

Polling: GET /api/v1/frames#

Returns a page from a ring of recent RX blocks:

JSON
{"granularity": "dma_block", "clock": "boot_relative", "empty_reason": null, "frames": [ ... ]}

Each entry is one DMA block, not one protocol frame. Times are relative to boot. When the page is empty, empty_reason says whether the link carried nothing or nothing was reading the modem. There is no per-frame FEC or CRC verdict: fec_status and crc_ok stay "none"/null. DELETE /api/v1/frames clears the ring.

Capture and loopback measurements#

Three routes measure the link at the byte level. They claim the datapath themselves.

RouteCLIWhat it does
POST /api/v1/datapath/capturesatlinkctl capture -o rx.bin --blocks 4Returns raw RX DMA blocks, concatenated
POST /api/v1/datapath/loopbacksatlinkctl loopback --payload p.bin --blocks 4Feeds a payload (raw le:u8/32) while capturing, returns the capture
POST /api/v1/datapath/measure—Loopback plus comparison, returns per-block match counts

Parameters common to all three:

ParameterDefaultMeaning
blocks4Blocks to collect in a row. Getting them is the proof of a sustained regime; a short capture fails (504) rather than being truncated
warmup0Blocks to obtain and discard first. Every capture re-arms the chain, so the first block can carry an acquisition transient. Explicit and zero by default, so real head-of-capture errors are never discarded silently
timeout_ms30000Give up after this long
release / --keepreleaseRelease the IIO devices afterwards

satlinkctl loopback compares the capture with the payload and prints a verdict (BytePerfect, Degraded, …). satlinkctl compare capture.bin payload.bin does the same offline on the host. See Measurement Tools for how to read the result.

Stellar Link · v0.1.0

↑↓ to moveEnter to open