Skip to main content

minspp

Minimalistic implementation of the Space Packet specification from the CCSDS Space Packet Protocol standard.

Repository | Documentation

This package was formerly published as minsp, which is no longer updated.

Installation

Install using pip:

$ pip install minspp

Install package from the git repository:

$ pip install git+https://github.com/nunorc/minspp@master

Getting Started

Import the SpacePacket class from the package:

>>> from minspp import SpacePacket

For example, to create a new space packet for APID 11 and an arbitrary data field:

>>> space_packet = SpacePacket(apid=11, data_field=b'hello')
>>> space_packet
SpacePacket(version=0, type=<PacketType.TM: 0>, secondary_header_flag=0, apid=11, sequence_flags=<SequenceFlags.UNSEGMENTED: 3>, sequence_count=0, data_length=4, secondary_header=b'', data_field=b'hello')

To get the bytes representation of the packet:

>>> byte_stream = space_packet.as_bytes()
>>> byte_stream
b'\x00\x0b\xc0\x00\x00\x04hello'

Packets can also be created from a byte stream:

>>> new_packet = SpacePacket.from_bytes(byte_stream)
>>> new_packet
SpacePacket(version=0, type=<PacketType.TM: 0>, secondary_header_flag=0, apid=11, sequence_flags=<SequenceFlags.UNSEGMENTED: 3>, sequence_count=0, data_length=4, secondary_header=b'', data_field=b'hello')
>>> new_packet.data_field
b'hello'

The packet data length field delimits the packet, so any octets past the end of the packet are ignored. To walk a buffer holding several back to back packets use iter_packets, which takes the same secondary header arguments as from_bytes:

>>> stream = b'\x00\x0b\xc0\x00\x00\x04hello\x00\x0c\xc0\x00\x00\x04world'
>>> [(p.apid, p.data_field) for p in SpacePacket.iter_packets(stream)]
[(11, b'hello'), (12, b'world')]

The packet data field can end with a packet error control field, the CRC-16-CCITT of every preceding octet of the packet, which most missions mandate for telecommands. It is opt-in on both sides, and the data length written to the primary header accounts for the two extra octets:

>>> byte_stream = space_packet.as_bytes(packet_error_control=True)
>>> byte_stream
b'\x00\x0b\xc0\x00\x00\x06hello\x81c'
>>> SpacePacket.from_bytes(byte_stream, packet_error_control=True).data_field
b'hello'

Decoding verifies the field and strips it, a mismatch raises a ValueError.

Secondary header can have a custom data definition, or to use PUS. Telemetry packets use the PUS-C (ECSS-E-ST-70-41C) TM secondary header:

>>> from minspp.pus import PUSTMHeader
>>> pus_header = PUSTMHeader()
>>> pus_header
PUSTMHeader(version=2, time_reference_status=0, service_type=1, service_subtype=1, message_type_counter=0, destination_id=0, has_time=False, cuc_time=b'')

And create a new packet with the PUS header:

>>> space_packet = SpacePacket(secondary_header=pus_header)
>>> space_packet
SpacePacket(version=0, type=<PacketType.TM: 0>, secondary_header_flag=1, apid=0, sequence_flags=<SequenceFlags.UNSEGMENTED: 3>, sequence_count=0, data_length=6, secondary_header=PUSTMHeader(version=2, time_reference_status=0, service_type=1, service_subtype=1, message_type_counter=0, destination_id=0, has_time=False, cuc_time=b''), data_field=b'')

For example a housekeeping parameter report (service 3, subtype 25) for destination 42:

>>> tm_header = PUSTMHeader(service_type=3, service_subtype=25, message_type_counter=7, destination_id=42)
>>> tm_header.as_bytes()
b' \x03\x19\x00\x07\x00*'

The width of the TM destination ID is mission defined, use destination_id_length to set it in bytes (defaults to 2, and 0 means the field is absent):

>>> PUSTMHeader(service_type=3, service_subtype=25, message_type_counter=7, destination_id=42, destination_id_length=1).as_bytes()
b' \x03\x19\x00\x07*'

The split of the CUC time between the coarse (seconds) and the fine (sub-seconds) field is mission defined too, use cuc_coarse_length to set the coarse part, the remaining octets of cuc_time_length hold the fine part:

>>> header = PUSTMHeader(has_time=True, cuc_time_length=7, cuc_coarse_length=5)
>>> header.fine_time_length()
2

So is the epoch the coarse time counts from, which defaults to the Unix epoch, use cuc_epoch for a mission that counts from another one (the CCSDS agency-standard epoch is 1958-01-01, and mission elapsed time counts from launch):

>>> from datetime import datetime, timezone
>>> from minspp.utils import cuc_as_datetime
>>> epoch = datetime(1958, 1, 1, tzinfo=timezone.utc)
>>> header = PUSTMHeader(has_time=True, cuc_epoch=epoch)
>>> cuc_as_datetime(header.cuc_time, epoch=epoch)   # the current time
datetime.datetime(2026, 7, 29, 20, 17, 2, 120716, tzinfo=datetime.timezone.utc)

The CUC field carries neither the epoch nor the coarse and fine lengths, they are declared out-of-band, so decoding must use the same values the sender used. There is no P-field (preamble) support, and all the arithmetic is done on UTC datetimes: a mission whose CUC counts a continuous time scale (TAI, GPS) is off by the leap seconds accumulated since its epoch, this package does not convert time scales.

Telecommand packets use a different secondary header layout, implemented by the PUSTCHeader class:

>>> from minspp.pus import PUSTCHeader
>>> PUSTCHeader()
PUSTCHeader(version=2, ack=0, service_type=1, service_subtype=1, source_id=0, has_time=False, cuc_time=b'')

The width of the TC source ID is mission defined, use source_id_length to set it in bytes (defaults to 1, and 0 means the field is absent):

>>> PUSTCHeader(service_type=8, service_subtype=1, source_id=0x0102, source_id_length=2).as_bytes()
b' \x08\x01\x01\x02'

Field values are checked when a packet or a header is packed, not when it is built. An out of range value raises a ValueError instead of being silently masked or surfacing as a struct.error, and the standard reserves service type and message subtype 0:

>>> PUSTCHeader(service_type=300).as_bytes()
Traceback (most recent call last):
  ...
ValueError: Invalid service type 300, must be between 1 and 255.

Decoding stays permissive, so a malformed packet can still be inspected. Use strict (or pus_strict on SpacePacket.from_bytes) to reject a secondary header that is not valid PUS-C, i.e. one whose version is not 2 or whose service type or subtype is the reserved 0:

>>> PUSTCHeader.from_bytes(b'\x10\x08\x01\x00').version   # a PUS-A header
1
>>> PUSTCHeader.from_bytes(b'\x10\x08\x01\x00', strict=True)
Traceback (most recent call last):
  ...
ValueError: Invalid PUS version 1, must be 2 for PUS-C.

Note that ECSS-E-ST-70-41C defines no time field for the TC secondary header, timestamps belong to telemetry. The has_time and cuc_time arguments of PUSTCHeader are a mission specific extension for missions that extend the header, they are off by default and leaving them off keeps the header standard conformant.

Similar approach for a MAL secondary header:

>>> from minspp.mo import MALHeader
>>> mal_header = MALHeader()
>>> mal_header
MALHeader(version=0, sdu_type=0, service_area=0, service=0, operation=0, area_version=0, is_error=0, qos_level=0, session=0, secondary_apid=0, secondary_apid_qualifier=0, transaction_id=0, source_id_flag=0, destination_id_flag=0, priority_flag=0, timestamp_flag=0, network_zone_flag=0, session_name_flag=0, domain_flag=0, authentication_id_flag=0, source_id=0, destination_id=0, segment_counter=0, priority=0, timestamp=None, network_zone='', session_name='', domain='', authentication_id='')

And to create a new packet with the MAL header:

>>> space_packet = SpacePacket(secondary_header=mal_header)
>>> space_packet
SpacePacket(version=0, type=<PacketType.TM: 0>, secondary_header_flag=1, apid=0, sequence_flags=<SequenceFlags.UNSEGMENTED: 3>, sequence_count=0, data_length=20, secondary_header=MALHeader(version=0, sdu_type=0, service_area=0, service=0, operation=0, area_version=0, is_error=0, qos_level=0, session=0, secondary_apid=0, secondary_apid_qualifier=0, transaction_id=0, source_id_flag=0, destination_id_flag=0, priority_flag=0, timestamp_flag=0, network_zone_flag=0, session_name_flag=0, domain_flag=0, authentication_id_flag=0, source_id=0, destination_id=0, segment_counter=0, priority=0, timestamp=None, network_zone='', session_name='', domain='', authentication_id=''), data_field=b'')

To create a space packet from a byte stream including a PUS header, use pus_tm=True for a TM header, or pus_tc=True for a TC one:

>>> byte_stream = SpacePacket(secondary_header=pus_header).as_bytes()
>>> SpacePacket.from_bytes(byte_stream, pus_tm=True)
SpacePacket(version=0, type=<PacketType.TM: 0>, secondary_header_flag=1, apid=0, sequence_flags=<SequenceFlags.UNSEGMENTED: 3>, sequence_count=0, data_length=6, secondary_header=PUSTMHeader(version=2, time_reference_status=0, service_type=1, service_subtype=1, message_type_counter=0, destination_id=0, has_time=False, cuc_time=b''), data_field=b'')

A TC header with a non default source ID width must be decoded with the same width, otherwise the data field is misaligned, use pus_source_id_length:

>>> tc_header = PUSTCHeader(service_type=8, service_subtype=1, source_id=0x0102, source_id_length=2)
>>> byte_stream = SpacePacket(secondary_header=tc_header, data_field=b'\xAB\x2A').as_bytes()
>>> SpacePacket.from_bytes(byte_stream, pus_tc=True, pus_source_id_length=2).data_field
b'\xab*'

The same holds for the other mission defined values, use pus_destination_id_length for a TM header, pus_cuc_coarse_length for the coarse and fine time split, and pus_cuc_epoch for the epoch:

>>> tm_header = PUSTMHeader(service_type=3, service_subtype=25, destination_id=42, destination_id_length=1)
>>> byte_stream = SpacePacket(secondary_header=tm_header, data_field=b'\x01\x02').as_bytes()
>>> SpacePacket.from_bytes(byte_stream, pus_tm=True, pus_destination_id_length=1).data_field
b'\x01\x02'

Or from a byte stream including a MAL header:

>>> byte_stream = SpacePacket(secondary_header=mal_header).as_bytes()
>>> SpacePacket.from_bytes(byte_stream, mal=True)
SpacePacket(version=0, type=<PacketType.TM: 0>, secondary_header_flag=1, apid=0, sequence_flags=<SequenceFlags.UNSEGMENTED: 3>, sequence_count=0, data_length=20, secondary_header=MALHeader(version=0, sdu_type=0, service_area=0, service=0, operation=0, area_version=0, is_error=0, qos_level=0, session=0, secondary_apid=0, secondary_apid_qualifier=0, transaction_id=0, source_id_flag=0, destination_id_flag=0, priority_flag=0, timestamp_flag=0, network_zone_flag=0, session_name_flag=0, domain_flag=0, authentication_id_flag=0, source_id=0, destination_id=0, segment_counter=0, priority=0, timestamp=None, network_zone='', session_name='', domain='', authentication_id=''), data_field=b'')

Use SpacePacketAssembler to recover the data from a list of fragmented packets, for example consider the following packets:

>>> from minspp import SpacePacket, SequenceFlags
>>> sp1 = SpacePacket(sequence_flags=SequenceFlags.FIRST, data_field=b"123")
>>> sp2 = SpacePacket(sequence_flags=SequenceFlags.CONTINUATION, data_field=b"456")
>>> sp3 = SpacePacket(sequence_flags=SequenceFlags.LAST, data_field=b"789")

To recover the fragmented payload by processing the individual packets:

>>> from minspp import SpacePacketAssembler
>>> spa = SpacePacketAssembler()
>>> spa.process_packet(sp1)
>>> spa.process_packet(sp2)
>>> spa.process_packet(sp3)
b'123456789'

Or directly using the from_packets method:

>>> SpacePacketAssembler.from_packets([sp1, sp2, sp3])
b'123456789'

Acknowledgements

  • Dominik Marszk for general support and MAL header baseline implementation.

Metadata

Release files for minspp 0.0.7

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for minspp 0.0.7
File Size Uploaded
minspp-0.0.7.tar.gz 41.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for minspp 0.0.7
File Interpreter ABI Platform
minspp-0.0.7-py3-none-any.whl Python 3 none any Details

Total release size: 72.6 kB

Release files / minspp-0.0.7.tar.gz

Download URL minspp-0.0.7.tar.gz
Size 41.6 kB
Tags Source
SHA-256 checksum
How to use checksums
b7400cd050122812ee63412872cbc70b910c8660ccf3031207499d99caa62a3e
BLAKE2b-256 checksum
How to use checksums
e82c3f7337fc7002983c5244174cda126ebd11ccf0c835b711e2893187d60422
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 29, 2026.

Transparency log

Release files / minspp-0.0.7-py3-none-any.whl

Download URL minspp-0.0.7-py3-none-any.whl
Size 31.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f018f9446eed12732c67e42386bc6196f9d8979560c38abe5dfec05fdf632867
BLAKE2b-256 checksum
How to use checksums
4565a8ed038ddc37422b96fd236d1b9a89343f42f1e2b58255ae5a80af77ff77
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 29, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.0.7 This release

2 release files

0.0.6

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page