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:
- Calls
definition.parse(frame_bytes). - If it raises
construct.core.ConstructError(or a subclass, such asStreamErrorfor a frame that is too short), printsCould not parse telemetry beacon <error>and moves on. - If the result is falsy (
None, empty), prints nothing. - 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
ConstructErroris caught. Any other exception raised insideparse()(anIndexError, aValueErrorfrom your own code) escapes the message handler. Convert such conditions into aConstructError, or returnNonefor 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
Switchon 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 bysatellites.telemetry, so long byte strings are printed in full.
Registering the Parser
-
Add an import to
python/telemetry/__init__.py, keeping alphabetical order:from .my_sat import my_sat -
Add the file name to the
GR_PYTHON_INSTALL(FILES ...)list inpython/telemetry/CMakeLists.txt, so it is installed. -
Rebuild and reinstall (
make,sudo make install).
Note:
CONTRIBUTING.mdrefers topython/telemetry/__index__.py; the file is actuallypython/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.