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.
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 value | Behaviour |
|---|---|
"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_offseton 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.
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",
)
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)
| Parameter | Meaning |
|---|---|
file | Path to a file on disk, or a file-like object. |
include_primary_header | If True, the decoded CCSDS primary-header fields are added to the output. |
reset_file_obj | If 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.
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
| Goal | How |
|---|---|
| One field fills the rest of the packet | PacketArray(..., array_shape="expand") |
| Array length comes from an earlier field | PacketArray(..., array_shape="other_field") |
| Fixed N-D array, choose memory order | `PacketArray(..., array_shape=(rows, cols), array_order="C" |
| Add header fields to the output | pkt.load(file, include_primary_header=True) |