minspp
Minimalistic implementation of the Space Packet specification from the CCSDS Space Packet Protocol standard.
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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file minspp-0.0.7.tar.gz.
File metadata
- Download URL: minspp-0.0.7.tar.gz
- Upload date:
- Size: 41.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b7400cd050122812ee63412872cbc70b910c8660ccf3031207499d99caa62a3e
|
|
| MD5 |
f4c3aead0aed57a2d33c2bd2a82d066d
|
|
| BLAKE2b-256 |
e82c3f7337fc7002983c5244174cda126ebd11ccf0c835b711e2893187d60422
|
Provenance
The following attestation bundles were made for minspp-0.0.7.tar.gz:
Publisher:
python-release.yml on nunorc/minspp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
minspp-0.0.7.tar.gz -
Subject digest:
b7400cd050122812ee63412872cbc70b910c8660ccf3031207499d99caa62a3e - Sigstore transparency entry: 2282916831
- Sigstore integration time:
-
Permalink:
nunorc/minspp@732009686ee8531f75a8c0aa59c607698d98c337 -
Branch / Tag:
refs/tags/0.0.7 - Owner: https://github.com/nunorc
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-release.yml@732009686ee8531f75a8c0aa59c607698d98c337 -
Trigger Event:
push
-
Statement type:
File details
Details for the file minspp-0.0.7-py3-none-any.whl.
File metadata
- Download URL: minspp-0.0.7-py3-none-any.whl
- Upload date:
- Size: 31.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f018f9446eed12732c67e42386bc6196f9d8979560c38abe5dfec05fdf632867
|
|
| MD5 |
759c6bab93bf57bc353c0518fff2d4e3
|
|
| BLAKE2b-256 |
4565a8ed038ddc37422b96fd236d1b9a89343f42f1e2b58255ae5a80af77ff77
|
Provenance
The following attestation bundles were made for minspp-0.0.7-py3-none-any.whl:
Publisher:
python-release.yml on nunorc/minspp
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
minspp-0.0.7-py3-none-any.whl -
Subject digest:
f018f9446eed12732c67e42386bc6196f9d8979560c38abe5dfec05fdf632867 - Sigstore transparency entry: 2282917085
- Sigstore integration time:
-
Permalink:
nunorc/minspp@732009686ee8531f75a8c0aa59c607698d98c337 -
Branch / Tag:
refs/tags/0.0.7 - Owner: https://github.com/nunorc
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-release.yml@732009686ee8531f75a8c0aa59c607698d98c337 -
Trigger Event:
push
-
Statement type: