Astro~/0.4
** Protocols / Time, payload, database / TDM * PAGE 29 / 35
** CCSDS 503.0-B-2 * Tracking Data Message
** pkg/tdm

Tracking Data Message

CCSDS 503.0-B-2, what a ground station measured while watching a spacecraft.

CCSDS 503.0-B-2 | Blue Book | pkg/tdm

Overview

A TDM carries what a ground station measured while it was watching something: range, Doppler, angles, signal levels, and the weather and clock corrections that go with them. It is what one agency sends another after tracking a spacecraft on their behalf, and it is normally one pass per file.

Where an ODM says where a spacecraft is, a TDM says what a station saw. The first is a conclusion; the second is evidence.

The shape is a header and then one or more segments. A segment is a metadata section describing how the measurements were taken, followed by a data section of Tracking Data Records.

Scope

Implemented. Reading and writing, in key-value notation. Every metadata keyword table 3-3 allows and every data keyword table 3-5 allows.

Also implemented: the XML form of section 5. EncodeXML and DecodeXML sit beside Encode and Decode.

Deliberately absent: tracking mathematics. Nothing here differences a range, unwraps an ambiguous one, applies a media or clock correction, or converts an angle between frames. Those need the interface control document the standard keeps deferring to, and clause 3.1.7 puts even the exchange method outside its own scope.

A measurement means nothing without its metadata

This is the trap. A Tracking Data Record is a keyword, a timetag and a number:

RANGE = 2010-215T20:04:24.000   65249.6771931631

Nothing in that line says what the number is in. Clause 3.5.2.7 puts the units in the segment's RANGE_UNITS, which may be km, s or RU — and if the keyword is absent the default is km. The record above came from a segment declaring RU. Read as kilometres it is wrong by orders of magnitude, and the record itself would never tell you.

Three keywords work this way:

KeywordGoWithout it
RANGE_UNITSMetadata.RangeUnits
RANGE_MODULUSMetadata.RangeModulus
ANGLE_TYPEMetadata.AngleType

RangeUnits returns the default rather than an empty string, and Humanize says out loud when the value was defaulted rather than stated.

A segment boundary is a configuration change

Clause 3.3.1.4 requires a new segment whenever any metadata value changes. A switch from one-way to two-way tracking, a different band, a different station: each ends a segment and starts another.

So the segments are not a packaging convenience, and two segments in one file may disagree about the units their measurements are in. Flattening them into one list of observations loses the only thing that says how to read each number.

The metadata is a list, not a struct

Metadata holds an ordered list of keyword-value pairs rather than forty named fields. That is what table 3-3 is: only TIME_SYSTEM and PARTICIPANT_n are mandatory, and the other forty-odd are optional station configuration whose meaning lives in an ICD.

A struct would be forty pointers, and a caller meeting an unfamiliar keyword would have no way to see it. Get reaches anything; the accessors cover what changes how a number must be read.

The XML form lines up neatly, with one rule kept

A Tracking Data Record becomes an <observation> carrying its epoch and its measurement:

RANGE = 2010-215T20:04:24.000 65249.6771931631
<observation>
  <EPOCH>2010-215T20:04:24.000</EPOCH>
  <RANGE>65249.6771931631</RANGE>
</observation>

Clause 3.4.3 pairs a timetag with exactly one observable, so an <observation> carrying two measurements is refused: the second has no timetag of its own.

What does not change is the important part. The units a measurement is in still come from the segment's RANGE_UNITS, in either form. The XML form does not put them on the record any more than the key-value form does.

The TDM names schema issue 2.0ndmxml-2.0.0-master-2.0.xsd — where the ODM gives 3.0 and the ADM 4.0.

Two keyword families overlap by prefix

TRANSMIT_FREQ_n and TRANSMIT_FREQ_RATE_n are both in table 3-5. Matching the shorter prefix first refuses TRANSMIT_FREQ_RATE_1, on the grounds that RATE_1 is not an index between 1 and 5.

RECEIVE_FREQ is the other oddity: table 3-5 lists it both bare and indexed, so RECEIVE_FREQ and RECEIVE_FREQ_1 are both legal. TRANSMIT_FREQ bare is not.

Using the package

message, err := tdm.Decode(data)

for _, segment := range message.Segments {
    // These come from the segment, never from a record.
    units := segment.Metadata.RangeUnits()
    modulus, ambiguous := segment.Metadata.RangeModulus()

    for _, obs := range segment.Observations {
        if obs.Keyword == "RANGE" && ambiguous {
            // Clause 3.5.2.7: not the range until the modulus is applied.
            _ = modulus
        }
        fmt.Println(obs.Keyword, obs.Epoch, obs.Value, units)
    }
}

Errors

ErrorMeans
ErrNoSegmentA message with no segment (clause 3.1.3).
ErrMissingTimeSystemA metadata section without TIME_SYSTEM, the one keyword table 3-3 makes mandatory.
ErrMissingParticipantNo PARTICIPANT_n; table 3-3 requires at least one.
ErrParticipantIndexAn index outside 1 to 5.
ErrMissingDataSectionA metadata section with no data section after it (clause 3.3.1.3).
ErrNoRecordsA data section with no records.
ErrMalformedRecordA record whose value is not a timetag and one measurement (clause 3.4.3).
ErrUnknownKeywordA keyword neither table 3-3 nor table 3-5 lists.
ErrUnterminatedBlockA META_START or DATA_START that never closes.

Reference