Telemetry — how it works
Telemetry turns the protobuf messages a spacecraft downlinks into named values you can read as latest readings and plot on charts. You declare the values you care about (fields) and how to pull them out of a decoded message (processors). That's the whole model.
The pieces
- Field definition — one named, typed value you want to track (e.g.
battery_voltage, afloat). Workspace-scoped, optionally scoped to a subsystem. - Processor — how a decoded message becomes field values. A processor names a protocol version and a message type, then maps selectors (paths into the decoded message) onto field definitions.
- Sample — one stored reading of a field over time; the time series behind charts.
- Latest value — the most recent sample per field, kept in memory for the console and latest-value cards.
Telemetry rides on the same protobuf-over-UDP pipeline as commands — see Protocols, commands & UDP for how packets arrive and get decoded.
The 5-minute path
To capture and graph a couple of fields from one message:
- On the Field Dictionary page, create a field per value you want — give it a name and a value type.
- On the Processors page, create a processor: choose the protocol version and the message type the value lives in.
- Add a mapping row per field: use the field tree to pick the value, which fills in the selector (the path into the message), and point it at your field definition.
- Send some telemetry. Latest values and charts appear on the satellite's telemetry view.
That's all that's required. Everything below is optional and only matters as your needs grow.
Selectors — pointing at a value
A selector is a bare path into the decoded message (the field tree writes these for you). It supports:
- Nested fields —
telemetry.load_average_1m. - List index —
telemetry.fan_rpm[0].value. - Match by content —
telemetry.temperatures[name="cpu"].valuefinds the list element whosenameequalscpuand reads itsvalue. The match value is compared as text.
A selector that doesn't resolve (missing field, wrong list element) simply contributes nothing — it doesn't error. This is how a processor for the wrong message in an envelope stays quiet.
Two common telemetry shapes
Real spacecraft telemetry tends to be a collection of readings, each tagged with an identity. The only question is where that identity lives — and both shapes work today by listing one mapping per reading:
- Identity in the field name (structural). Each channel is its own named
field, e.g. a power monitor reporting
vip_cnt_ch14.volt,vip_cnt_ch14.curr,vip_cnt_ch14.pwr. Map each path to a field (vip_cnt_ch14.pwr → ch14_power). - Identity in the data (content). Readings sit in a repeated list and
carry an id field, e.g.
SensorReading { sensor_id, value }. Use a content-match selector per id:top_readings[sensor_id="14"].value → ch14_value.
A more compact, table-driven way to handle many or changing channels at once (instead of one mapping each) is planned — for now, list the ones you care about.
Recognising the right message (envelope convention)
AutoSat decodes an incoming packet against the message types your processors name, and a processor contributes values only when its selectors resolve. This is reliable when your messages share a single envelope with a one-of payload, e.g.:
message DownlinkEnvelope {
oneof payload {
Heartbeat heartbeat = 1;
Telemetry telemetry = 2;
Eps eps = 3;
}
}
Because exactly one branch is set per packet, each processor only matches the message that was actually sent. Wrapping all telemetry in one envelope is the recommended pattern.
If a single packet produces values for more than one top-level message type, that's a sign two unrelated messages look alike on the wire — a warning is logged. Prefer the envelope above so the payload branch tells AutoSat which message it is.
Subsystems — where a reading is attributed
A field and a processor can be satellite-level or scoped to a subsystem. A subsystem-scoped processor attributes its values to the installed instance of that subsystem on the satellite.
Which instance a reading belongs to is something you configure, not something the protobuf tells us — the message gives the value, you decide the channel-to-subsystem mapping. Today a processor resolves to a subsystem's single installed instance; routing across multiple identical instances (by a hardware id in the packet) is planned.