Fixed-length packets
A fixed-length packet is one whose body always occupies the same number of bits, regardless of its contents. This is the most common layout in CCSDS telemetry and the simplest to describe with CCSDSPy. You declare the packet layout once as an ordered list of fields, then apply that layout to a binary file or stream containing many packets.
CCSDSPy reads tightly packed bits in the CCSDS Space Packet Protocol format used by many NASA and ESA missions, so fields do not need to align to byte boundaries — a 3-bit field followed by a 20-bit field is perfectly valid.
The 48-bit CCSDS primary header is handled for you. You only describe the
packet body (the application data); CCSDSPy parses the header automatically
and can optionally return its fields to you (see include_primary_header).
Defining the layout with FixedLength
ccsdspy.FixedLength takes a list of field definitions. Each field is a
PacketField (a single scalar value) or a PacketArray (multiple values of
the same size — covered on its own page).
import ccsdspy
from ccsdspy import PacketField, PacketArray
pkt = ccsdspy.FixedLength([
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),
PacketField(name='SPACER', data_type='fill', bit_length=1),
PacketField(name='VOLTAGE', data_type='int', bit_length=8),
])
Fields are read in the order you list them. CCSDSPy computes each field's bit offset from the running total, starting immediately after the primary header.
PacketField parameters
PacketField(name, data_type, bit_length, bit_offset=None, byte_order="big", description=None)
| Parameter | Type / allowed values | Required | Description |
|---|---|---|---|
name | str | yes | Identifier used as the key for this field's values after parsing. |
data_type | 'uint', 'int', 'float', 'str', 'fill' | yes | Data type of the field (see below). |
bit_length | int | yes | Number of bits the field occupies. |
bit_offset | int | no | Bit offset into the packet, including the 48-bit primary header. If omitted, it is computed automatically from field order. |
byte_order | 'big', 'little', or a digit string like "3412" | no | Byte order. Defaults to 'big'. |
description | str | no | Free-text metadata; not used during parsing. |
You almost never need to set bit_offset. Because fields are defined in order
within a list, CCSDSPy calculates offsets for you. Set it explicitly only when
you want to skip ahead or overlay fields at a known position.
Supported data_type values
The constructor accepts exactly five data types. Anything else raises a
ValueError.
data_type | Meaning | Notes |
|---|---|---|
'uint' | Unsigned integer | Any bit_length; not restricted to byte boundaries. |
'int' | Signed integer (two's complement) | Any bit_length; sign is interpreted from the field's own bit width. |
'float' | IEEE 754 floating point | Use bit_length=32 or bit_length=64. CCSDSPy maps floats onto NumPy's float32 / float64. |
'str' | String (raw bytes) | Returned as fixed-width byte strings. |
'fill' | Placeholder / padding | Reserved space between meaningful fields; the value is ignored. |
The 'fill' type lets you reserve bits you do not care about (spare bits,
reserved fields, or alignment padding) without polluting your parsed output
with meaningless values. In the example above, the 1-bit SPACER keeps the
following VOLTAGE field correctly positioned.
Bit-level fields
Because CCSDS packs values without padding, bit_length can be any width that
the data type allows — 3, 12, 20, and so on. Integers (uint / int)
are the most flexible here: a 12-bit uint and a 3-bit uint are both valid.
Floats are the exception: keep them at the standard 32- or 64-bit IEEE
widths.
When a field's bit length is not a whole number of bytes, CCSDSPy still parses
it correctly at the bit level, then returns each value in a NumPy array whose
element size is rounded up to the next whole byte (for example, a 20-bit
uint is returned in a 4-byte uint32 array).
Byte order
byte_order applies to integer fields and floats. In addition to the usual
'big' and 'little', CCSDSPy accepts an ad-hoc ordering written as a digit
string such as "3412", which lets you describe non-standard byte
arrangements seen in some instruments.
PacketField(name='COUNTER', data_type='uint', bit_length=32, byte_order='little')
Reading a file
Call .load() with a path (or file-like object). It returns a dictionary that
maps each field name to a NumPy array, with one entry per packet found in the
input.
result = pkt.load('MyCCSDS.tlm')
# result is a dict: field name -> numpy array (one value per packet)
result['VOLTAGE'] # e.g. array([...], dtype=int8)
result['SHCOARSE'] # e.g. array([...], dtype=uint32)
Pass include_primary_header=True to also receive the decoded CCSDS primary
header fields (such as CCSDS_VERSION_NUMBER, CCSDS_SEQUENCE_COUNT, and
CCSDS_PACKET_LENGTH) as additional keys in the result:
result = pkt.load('MyCCSDS.tlm', include_primary_header=True)
result['CCSDS_SEQUENCE_COUNT']
The field names (SHCOARSE, OPMODE, VOLTAGE, …) and the file name
MyCCSDS.tlm above are illustrative placeholders adapted from the CCSDSPy
documentation. Replace them with the field layout and file paths from your own
mission's interface control document (ICD).
Writing a file
The same definition works in the other direction. .to_file() encodes a
dictionary of NumPy arrays back into a binary file of CCSDS packets, generating
the primary header for each one — useful for building test vectors and
simulated telemetry from the very definition you decode with.
import numpy as np
pkt.to_file(
'generated.tlm',
pkt_type=0, # 0 = telemetry / reporting, 1 = commanding
apid=1426,
sec_header_flag=0, # 1 if a secondary header is present
seq_flag=3, # 3 = unsegmented (first and only segment)
seq_count=0, # starting sequence number
data={
'SHCOARSE': np.array([100, 101, 102], dtype='uint32'),
'OPMODE': np.array([1, 1, 2], dtype='uint8'),
'VOLTAGE': np.array([-3, -2, -1], dtype='int8'),
},
)
Every array in data must have the same length — that length is the number of
packets written — and the keys must match the field names in the definition.
Mismatched lengths raise a ValueError.
You do not supply values for 'fill' fields. They are padding, so the
encoder emits the right number of zero bits for them and moves on, keeping the
offsets of the real fields aligned exactly as the decoder expects.
fill fieldsUntil PR #157, the encoder built
an invalid bitstruct format code for fill fields (uint512 rather than
u512), so any call to to_file() on a definition containing a fill
field failed outright with bitstruct.Error: bad format. Decoding was never
affected. If you are on an older release and hit that error, either upgrade to a
build that includes the fix or drop the fill fields from the definition you
encode with.
VariableLength.to_file() takes the same arguments minus seq_count.
When to use FixedLength
Use FixedLength when every packet that matches a definition has an identical
body size. If your packet contains a field whose length depends on another
field's value, or an array that grows to fill the packet, you need
ccsdspy.VariableLength instead.
Sources
- CCSDSPy documentation — Fixed Length Packets
- CCSDSPy documentation — Defining Packet Fields
- CCSDSPy documentation home
- CCSDSPy/ccsdspy on GitHub (README and
ccsdspy/packet_fields.py) ccsdspy/packet_types.py—FixedLength.to_file()andVariableLength.to_file()- Fix encoding of
fillfields — CCSDSPy/ccsdspy PR #157