Python API

Everything in gr-satellites is importable from Python as the satellites package, so you can build decoders in your own GNU Radio scripts, look up satellites programmatically, or parse telemetry offline. This page documents the main entry points: the gr_satellites_flowgraph decoder, the component classes, the options mechanism shared by all components, the SatYAML lookup functions and the telemetry definitions.

Package Layout

Module Contents
satellites Low-level blocks (C++ bindings and Python blocks), e.g. satellites.pdu_to_kiss, satellites.hdlc_deframer.
satellites.core gr_satellites_flowgraph, the complete decoder.
satellites.components.demodulators bpsk_demodulator, fsk_demodulator, afsk_demodulator.
satellites.components.deframers All deframer components.
satellites.components.transports kiss_transport, tm_kiss_transport, nanolink_transport.
satellites.components.datasinks telemetry_parser, telemetry_submit, file_receiver, hexdump_sink, kiss_file_sink, kiss_server_sink, codec2_udp_sink.
satellites.components.datasources kiss_file_source.
satellites.satyaml yamlfiles, the SatYAML loader.
satellites.telemetry Telemetry definitions (construct structures).
satellites.filereceiver File and image receiver classes.
satellites.utils.config open_config() for config.ini.

Importing satellites loads the compiled bindings, so it needs a working installation (see Installation).

gr_satellites_flowgraph

satellites.core.gr_satellites_flowgraph(
    file=None, name=None, norad=None,
    samp_rate=None, iq=False, grc_block=False,
    options=None, config=None, pdu_in=False, dump_path=None)

A gr.hier_block2 that builds the decoder for one satellite.

Argument Type Meaning
file, name, norad str, str, int The satellite. Exactly one must be given, otherwise ValueError('exactly one of file, name and norad needs to be specified'). An unknown name or NORAD raises ValueError('satellite not found').
samp_rate float Input sample rate. Required unless pdu_in (ValueError('samp_rate not specified')).
iq bool Complex input when true, float otherwise.
grc_block bool True: build only demodulators and deframers and output frames on the out message port. False: also build data sinks, transports and output options, as the command line tool does.
options str, argparse namespace or None Command line options. A string is split with shlex and parsed against the satellite's demodulation, deframing and data sink options.
config ConfigParser Configuration (from satellites.utils.config.open_config()). Needed when grc_block=False.
pdu_in bool Frames instead of samples as input, on an in message port (used by --kiss_in). Only meaningful with grc_block=False.
dump_path str Directory for the demodulators' internal signals.

Ports:

Mode Input Output
grc_block=True 1 stream (float or complex) message port out
grc_block=False 1 stream, or message port in with pdu_in=True none (data sinks are internal)

Every output PDU's metadata is a dictionary with the key transmitter set to the transmitter name from the SatYAML file.

Example: decode a WAV file and handle frames in Python

import pmt
from gnuradio import gr, blocks
import satellites.core


class frame_printer(gr.basic_block):
    """Prints each decoded frame with its transmitter name."""
    def __init__(self):
        gr.basic_block.__init__(self, name='frame_printer',
                                in_sig=None, out_sig=None)
        self.message_port_register_in(pmt.intern('in'))
        self.set_msg_handler(pmt.intern('in'), self.handle_msg)

    def handle_msg(self, msg):
        meta = pmt.car(msg)
        frame = bytes(pmt.u8vector_elements(pmt.cdr(msg)))
        tx = pmt.dict_ref(meta, pmt.intern('transmitter'), pmt.PMT_NIL)
        name = pmt.symbol_to_string(tx) if pmt.is_symbol(tx) else '?'
        print(name, len(frame), frame.hex())


class top_block(gr.top_block):
    def __init__(self, path):
        gr.top_block.__init__(self, 'decoder')
        self.src = blocks.wavfile_source(path, False)
        self.decoder = satellites.core.gr_satellites_flowgraph(
            name='FUNcube-1', samp_rate=self.src.sample_rate(),
            grc_block=True, iq=False, options='--clk_bw 0.08')
        self.printer = frame_printer()
        self.connect((self.src, 0), self.decoder)
        self.msg_connect((self.decoder, 'out'), (self.printer, 'in'))


tb = top_block('ao73.wav')
tb.start()
tb.wait()

Using grc_block=False from Python

The full mode expects what the command line tool provides. In the current code it needs:

  • config: a ConfigParser with at least the [Groundstation] section (open_config() gives one), unless GR_SATELLITES_SUBMIT_TLM is set;
  • options: an argparse namespace that also contains the output options (kiss_out, kiss_append, kiss_server, kiss_server_address, zmq_pub, hexdump). A string is not enough, because string options are parsed only against the satellite's component options, and the flowgraph then fails with an AttributeError such as 'Namespace' object has no attribute 'kiss_out'. With options=None, satellites that have transports fail on self.options.hexdump.

For custom applications it is usually simpler to use grc_block=True and add the data sink blocks you need.

Component Classes

The components can be combined by hand. A 9k6 G3RUH AX.25 receiver from real FM audio:

from gnuradio import gr, blocks
from satellites.components.demodulators import fsk_demodulator
from satellites.components.deframers import ax25_deframer
from satellites.components.datasinks import hexdump_sink


class ax25_rx(gr.top_block):
    def __init__(self, path):
        gr.top_block.__init__(self, 'ax25_rx')
        self.src = blocks.wavfile_source(path, False)
        rate = self.src.sample_rate()
        self.demod = fsk_demodulator(baudrate=9600, samp_rate=rate, iq=False)
        self.deframer = ax25_deframer(g3ruh_scrambler=True)
        self.sink = hexdump_sink()
        self.connect((self.src, 0), self.demod, self.deframer)
        self.msg_connect((self.deframer, 'out'), (self.sink, 'in'))

Constructor signatures of the main components:

Component Signature
bpsk_demodulator (baudrate, samp_rate, iq, f_offset=None, differential=False, manchester=False, dump_path=None, options=None)
fsk_demodulator (baudrate, samp_rate, iq, deviation=None, subaudio=False, dc_block=True, dump_path=None, options=None)
afsk_demodulator (baudrate, samp_rate, iq, af_carrier, deviation, dump_path=None, options=None, fm_deviation=None)
ax25_deframer (g3ruh_scrambler, options=None)
ax100_deframer (mode, scrambler='CCSDS', syncword_threshold=None, syncword=..., options=None) (mode is 'RS' or 'ASM')
ccsds_rs_deframer (frame_size=223, precoding=None, rs_en=True, rs_basis='dual', rs_interleaving=1, scrambler='CCSDS', syncword_threshold=None, options=None)
ccsds_concatenated_deframer same plus convolutional='CCSDS'
ao40_fec_deframer (syncword_threshold=None, short_frames=False, crc=False, options=None)
kiss_transport (control_byte=True, header_remove_bytes=0, options=None)
tm_kiss_transport (virtual_channels=[], short_tm=False, options=None)
telemetry_parser (definition, file=sys.stdout, options=None)
file_receiver (receiver, path=None, verbose=None, options=None, **kwargs)
kiss_file_sink (file, append=False, options=None)
kiss_server_sink (address, port, options=None)
telemetry_submit (server, norad=None, port=None, url=None, config=None, options=None)

file_receiver raises ValueError('Must indicate path in function arguments or options') (or the same for verbose) if neither the argument nor an options namespace provides it. Extra keyword arguments (display, fullscreen) go to the receiver class.

The options Mechanism

Most components inherit from satellites.utils.options_block.options_block, which gives every component a self.options namespace:

options passed Result
None or "" The component's own add_options() defaults.
A string, e.g. "--clk_bw 0.1" Split with shlex.split() and parsed with the component's add_options(). Unknown options make argparse exit.
An argparse namespace Used as is (this is how gr_satellites shares one namespace between all components).

An explicit constructor argument always wins over the options: for example syncword_threshold=4 ignores --syncword_threshold, and f_offset=1500 ignores --f_offset. Only arguments left at None fall back to self.options.

Each component class also has a class method add_options(parser) that registers its options on an argparse parser. That is how gr_satellites builds its satellite-specific help.

SatYAML Lookup

from satellites.satyaml import yamlfiles

sat = yamlfiles.open_satyaml(name='FUNcube-1')   # or norad=39444, or file='x.yml'
print(sat['name'], sat['norad'])
for tx_name, tx in sat['transmitters'].items():
    print(tx_name, tx['modulation'], tx['baudrate'], tx['framing'])

all_sats = yamlfiles.load_all_yaml()            # list of dicts
yamlfiles.check_yaml('my_sat.yml')              # raises YAMLError on problems

open_satyaml() returns the parsed YAML as plain Python dictionaries. yaml_files() returns the paths of the bundled files (an unsorted glob of *.yml next to the module).

Telemetry Definitions Offline

Telemetry definitions are construct objects, usable without GNU Radio flowgraphs:

from satellites.telemetry import au03

frame = bytes.fromhex('...')   # a frame saved from gr_satellites --hexdump
print(au03.parse(frame))

Some definitions are stateful objects (for example funcube, which reassembles Whole Orbit data across frames) and return a formatted string rather than a Container.

See Also