Skip to main content

Variable-length packets

A CCSDS packet does not always have the same length every time it is sent. A field inside the packet may carry a different number of elements from one packet to the next — for example a buffer of sensor samples whose count depends on the spacecraft's mode. CCSDSPy reads these streams with the VariableLength class.

Where FixedLength requires every packet of a given APID to have the same byte layout, VariableLength lets exactly one field — or a field sized by another field — grow or shrink per packet. CCSDSPy uses the packet's primary-header length value to work out the true size of each packet as it decodes.

note

Decoding variable-length packets is more involved than decoding fixed-length ones, so it is slower. If every packet of a given type really is the same length, prefer FixedLength. Reach for VariableLength only when the layout genuinely varies packet-to-packet.

The two kinds of variable field

A variable field is always declared as a PacketArray. There are two ways to size it:

array_shape valueBehaviour
"expand"The array grows to fill the remaining space in the packet.
"other_field"The array's element count is taken from the value of the field named other_field.

Both forms require the VariableLength class. A PacketArray with a fixed tuple array_shape (such as (32, 32)) can be used in either class.

Expanding fields

An expanding field consumes whatever bytes are left in the packet after all the other fields have been accounted for. Its length is computed from the packet-length value in the CCSDS primary header.

import ccsdspy
from ccsdspy import PacketField, PacketArray

pkt = ccsdspy.VariableLength([
PacketField(name="SHCOARSE", data_type="uint", bit_length=32),
PacketField(name="SHFINE", data_type="uint", bit_length=20),
PacketField(name="OPMODE", data_type="uint", bit_length=3),
PacketArray(
name="SCIENCE_DATA",
data_type="uint",
bit_length=16,
array_shape="expand",
),
])

result = pkt.load("MyCCSDS.tlm")

A few rules apply to expanding fields:

  • At most one field in a definition may use array_shape="expand".
  • You may not specify a bit_offset on any field in a variable-length definition. Because one field changes size, CCSDSPy must compute every offset itself, so list the fields strictly in packet order.
tip

Because the expanding field absorbs all remaining bytes, any fields you want decoded individually must come before it in the definition. Anything after an expanding field has no well-defined position.

Fields sized by another field

The second form links an array's element count to a value carried earlier in the same packet. Set array_shape to the name of that counter field. CCSDSPy reads the counter first, then reads that many elements for the array.

import ccsdspy
from ccsdspy import PacketField, PacketArray

pkt = ccsdspy.VariableLength([
PacketField(name="SHCOARSE", data_type="uint", bit_length=32),
PacketField(name="data1_len", data_type="uint", bit_length=8),
PacketArray(
name="data1",
data_type="uint",
bit_length=16,
array_shape="data1_len",
),
])

result = pkt.load("MyCCSDS.tlm")

Unlike the expanding form, you may have several reference-sized arrays in one packet. The only constraint is ordering: the counter field must appear before the PacketArray that refers to it, so that its value is known by the time the array is read.

Multi-dimensional arrays and array_order

PacketArray can also describe fixed multi-dimensional arrays via a tuple array_shape. When a field has more than one dimension, array_order controls how the flat sequence of values in the packet is mapped onto that shape. It follows NumPy's convention: "C" for row-major (C-style) order and "F" for column-major (Fortran-style) order.

PacketArray(
name="SENSOR_GRID",
data_type="uint",
bit_length=16,
array_shape=(32, 32),
array_order="C",
)
info

array_order matters only for multi-dimensional fixed-shape arrays. It does not apply to the one-dimensional variable-length forms ("expand" or a referenced field name), which are always read in the order the elements appear in the packet.

Loading and the primary header

VariableLength.load() takes the same arguments as the fixed-length loader:

result = pkt.load("MyCCSDS.tlm", include_primary_header=False)
ParameterMeaning
filePath to a file on disk, or a file-like object.
include_primary_headerIf True, the decoded CCSDS primary-header fields are added to the output.
reset_file_objIf True, restore a file-buffer object to its original position after reading.

load() returns a dictionary mapping each field name to a NumPy array, in the order the fields were defined. Setting include_primary_header=True adds the standard header fields CCSDS_VERSION_NUMBER, CCSDS_PACKET_TYPE, CCSDS_SECONDARY_FLAG, CCSDS_SEQUENCE_FLAG, CCSDS_APID, CCSDS_SEQUENCE_COUNT, and CCSDS_PACKET_LENGTH to that dictionary.

note

You should never define the primary-header fields manually in a packet definition — CCSDSPy handles the 6-byte CCSDS primary header for you and exposes it through include_primary_header.

Quick reference

GoalHow
One field fills the rest of the packetPacketArray(..., array_shape="expand")
Array length comes from an earlier fieldPacketArray(..., array_shape="other_field")
Fixed N-D array, choose memory order`PacketArray(..., array_shape=(rows, cols), array_order="C"
Add header fields to the outputpkt.load(file, include_primary_header=True)

Sources