Writing a Telemetry Parser

Telemetry parsers turn raw frames into named, scaled values such as battery voltages, temperatures and counters. In gr-satellites they are always written in Python with the construct library. This page explains the contract a parser must meet, the helpers that gr-satellites provides, a complete worked example, how to register the parser, and how to test it.

The Contract

A telemetry definition is any object with a parse(data) method, exposed as an attribute of the satellites.telemetry package. The telemetry parser data sink uses it like this for every frame:

  1. Calls definition.parse(frame_bytes).
  2. If it raises construct.core.ConstructError (or a subclass, such as StreamError for a frame that is too short), prints Could not parse telemetry beacon <error> and moves on.
  3. If the result is falsy (None, empty), prints nothing.
  4. Otherwise prints str(result).

Most definitions are simply a construct Struct, whose parse result is a Container that prints as an indented tree. Definitions that need state or custom output (for example funcube, which reassembles Whole Orbit data across frames and returns formatted text) are classes with a parse() method.

Note: Only ConstructError is caught. Any other exception raised inside parse() (an IndexError, a ValueError from your own code) escapes the message handler. Convert such conditions into a ConstructError, or return None for frames you want to ignore.

Helpers Available

Helper Module Use
CSPHeader satellites.telemetry.csp CubeSat Space Protocol header (priority, source, destination, ports, flags). Used by GOMspace-based satellites.
Header, Address, Callsign satellites.telemetry.ax25 AX.25 header parsing. The ax25 definition parses header plus info bytes.
LinearAdapter(c, subcon) satellites.adapters Decodes raw / c.
AffineAdapter(c, a, subcon) satellites.adapters Decodes (raw − a) / c.
PolynomialAdapter(coeffs, subcon) satellites.adapters Decodes Σ coeffs[i] · raw^i (decode only).
UNIXTimestampAdapter(subcon) satellites.adapters Seconds since the epoch to a UTC datetime.
TableAdapter(table, subcon) satellites.adapters Index into a lookup table.

Generic definitions already exist for common cases: ax25 (AX.25 header plus payload), csp and cspv2 (CSP header plus payload).

Example: a Simple Beacon

Suppose MY-SAT sends 16-byte beacons inside AX.25 frames: a 4-byte UNIX timestamp, the battery voltage in millivolts (16 bits), a temperature in 0.5 °C steps (signed byte), a boot counter and 8 status bytes. This example follows the same pattern as the bundled au03.py:

# python/telemetry/my_sat.py
from construct import *

from ..adapters import LinearAdapter, UNIXTimestampAdapter
from .ax25 import Header

Timestamp = UNIXTimestampAdapter(Int32ub)

Beacon = Struct(
    'timestamp' / Timestamp,
    'battery_voltage' / LinearAdapter(1000, Int16ub),   # volts
    'temperature' / LinearAdapter(2, Int8sb),          # degrees C
    'boot_count' / Int16ub,
    'status' / Bytes(8),
    )

my_sat = Struct(
    'header' / Header,
    'beacon' / Beacon,
    )

The name of the module-level object (my_sat) is what the SatYAML file refers to:

data:
  &tlm Telemetry:
    telemetry: my_sat

Printed output for a frame looks similar to:

-> Packet from 9k6 FSK downlink
Container: 
    header = Container: 
        addresses = ListContainer: 
            ...
        control = 0x03
        pid = 0xF0
    beacon = Container: 
        timestamp = 2024-03-01 12:34:56+00:00
        battery_voltage = 7.912
        temperature = 21.5
        boot_count = 42
        status = b'\x00\x00\x00\x00\x00\x00\x00\x00' (total 8)

Tips:

  • Use Switch on a frame-type field when the satellite sends several beacon types, as many bundled parsers do.
  • Use Hex(...) for fields that are more readable in hexadecimal.
  • construct.setGlobalPrintFullStrings(True) is set by satellites.telemetry, so long byte strings are printed in full.

Registering the Parser

  1. Add an import to python/telemetry/__init__.py, keeping alphabetical order:

    from .my_sat import my_sat
  2. Add the file name to the GR_PYTHON_INSTALL(FILES ...) list in python/telemetry/CMakeLists.txt, so it is installed.

  3. Rebuild and reinstall (make, sudo make install).

Note: CONTRIBUTING.md refers to python/telemetry/__index__.py; the file is actually python/telemetry/__init__.py.

Testing the Parser

Parse a saved frame directly, without running a flowgraph:

from satellites.telemetry import my_sat

frame = bytes.fromhex('...')   # copy from gr_satellites --hexdump output
print(my_sat.parse(frame))

Or decode a KISS file you saved earlier, which runs the real data sink:

gr_satellites ./MY-SAT.yml --kiss_in my_sat.kss

For a regression test, follow the existing qa_*.py files (for example python/telemetry/qa_by02.py), which use gnuradio.gr_unittest and are registered with a GR_ADD_TEST(...) line in the directory's CMakeLists.txt, so that make test runs them.

See Also