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: aConfigParserwith at least the[Groundstation]section (open_config()gives one), unlessGR_SATELLITES_SUBMIT_TLMis 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 anAttributeErrorsuch as'Namespace' object has no attribute 'kiss_out'. Withoptions=None, satellites that have transports fail onself.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.