fast-ccsds-parser
Fast, dependency-free parsing of CCSDS Space Packets (CCSDS 133.0-B-2)
for the modern Python data stack. Turn raw telemetry dumps into dicts and
structured objects in one pass: primary headers, packet boundaries, and
payloads, with a pure standard-library core that installs anywhere
(pip install and go).
Built for small-satellite teams, university labs, and data engineers who just want their packets parsed, without pulling a decoding framework.
Install
pip install fast-ccsds-parser
The core has no dependencies. For columnar export, add an extra
(arrow, polars, pandas, or all):
pip install "fast-ccsds-parser[pandas]"
or from source:
pip install git+https://github.com/daikiito-dk/fast-ccsds-parser.git
Quickstart
from fast_ccsds_parser import parse_primary_header, iter_file
# One header, as a plain dict:
header = parse_primary_header(raw_bytes)
# {'version': 0, 'type': 0, 'secondary_header_flag': 1, 'apid': 42,
# 'sequence_flags': 3, 'sequence_count': 1337, 'packet_length': 255}
# A whole ground-station dump, streamed packet by packet (constant memory):
for pkt in iter_file("dump_20261010.pkts"):
print(pkt.header.apid, pkt.header.sequence_count, len(pkt.payload))
# In-memory buffers work the same way:
packets = [p.to_dict() for p in iter_packets(buffer)]
Need a whole dump as a DataFrame? Scan it column by column instead of
building one dict per packet (needs fast-ccsds-parser[pandas]):
from fast_ccsds_parser import scan_file, write_parquet
df = scan_file("dump_20261010.pkts").to_pandas()
write_parquet("dump_20261010.pkts", "dump_20261010.parquet")
What it parses
The 6-byte CCSDS primary header (all fields big-endian):
bit: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
0-1 | version | T | S | APID |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
2-3 | seq flags | sequence count / packet name |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
4-5 | packet length |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| Field | Bits | Meaning |
|---|---|---|
version |
3 | 0 for space packets |
type |
1 | 0 = telemetry, 1 = telecommand |
secondary_header_flag |
1 | 1 = a secondary header follows the primary header |
apid |
11 | application process identifier (all ones = idle) |
sequence_flags |
2 | segmentation status (unsegmented = 0b11) |
sequence_count |
14 | packet sequence count / packet name |
packet_length |
16 | data field length minus one (see below) |
API
| Entry point | Purpose |
|---|---|
parse_primary_header(data, *, offset=0, strict=True) |
Header fields as a dict |
PrimaryHeader.from_bytes(data, *, offset=0, strict=True) |
Header as a NamedTuple (.apid, .total_length, .is_idle, .to_dict(), ...) |
parse_packet(data, *, strict=True) |
Exactly one packet, as a SpacePacket |
iter_packets(data, *, strict=True, skip_idle=False) |
Generator over a buffer of concatenated packets |
parse_packets(data, ...) |
Same, as a list |
iter_file(path, ...) / parse_file(path, ...) |
Stream packets from a dump file with O(1) memory |
scan_bytes(data, *, payload=True, ...) |
Parse a buffer into a columnar PacketTable |
scan_file(path, *, payload=True, ...) |
Parse a whole file into one PacketTable (memory-mapped) |
iter_tables(path, *, chunk_packets=1_000_000, ...) |
Yield one PacketTable per chunk, for files larger than memory |
write_parquet(source, destination, ...) |
Convert a packet file to Parquet chunk by chunk |
SpacePacket carries header (a PrimaryHeader), payload (the data
field bytes), raw (the complete packet bytes), and a to_dict() that
flattens everything into a single dict.
Columnar output
PacketTable stores header fields in typed array.array columns and all
payloads in one contiguous buffer, so memory stays close to the input size.
Columns: offset, version, type, secondary_header_flag, apid,
sequence_flags, sequence_count, packet_length, and payload.
table = scan_file("dump_20261010.pkts") # or scan_bytes(buffer)
table.apid # array.array, one entry per packet
table.payload(0) # payload bytes of packet 0
arrow = table.to_arrow() # pyarrow.Table (header columns are zero-copy)
df = table.to_polars() # polars.DataFrame
df = table.to_pandas() # pandas.DataFrame (payload stays Arrow-backed)
Export needs the matching extra: to_arrow() and write_parquet() need
pyarrow (fast-ccsds-parser[arrow]), to_polars() and to_pandas()
add polars / pandas (the polars and pandas extras). Scanning
itself works with the standard library alone; pass payload=False to keep
only the header columns and use much less memory.
Semantics to know
packet_lengthis zero-based. The data field ispacket_length + 1bytes, so a total packet is6 + packet_length + 1bytes. This is the number-one source of off-by-one bugs when handling CCSDS data, and the parser handles it for you.payload= the full data field. Per CCSDS 133.0-B-2 the data field is an optional secondary header followed by user data; the payload bytes are returned as-is. Checkheader.has_secondary_headerto know whether a mission-specific secondary header is in front. Decoding those layouts is out of scope for the structural parser (see the roadmap).- Idle packets (APID =
0x7FF) are yielded like any other packet by default; passskip_idle=Trueto filter them out. - Strict mode (default) raises
InvalidPacketErrorwhen the packet version is not 0, which usually means your byte stream is misaligned. Passstrict=Falseto parse anyway. - All errors derive from
FastCCSDSError(aValueError).
Related projects
Several mature Python libraries already handle CCSDS space packets, and all of them can split packets and read primary headers without a mission packet definition. Pick the one that fits your job:
| (as of 2026-10) | fast-ccsds-parser 0.2 | ccsdspy 2.0 | space-packet-parser 6.2 | spacepackets 0.32 |
|---|---|---|---|---|
| Main focus | Structural parsing (headers, packet boundaries, payloads) | Field decoding from packet definitions | Field decoding from XTCE definitions | Building and parsing packets, incl. ECSS PUS and CFDP |
| Runtime dependencies | None | numpy, bitstruct, pyyaml, appdirs | click, lxml, rich | fastcrc, deprecation |
| Header-only parsing without definitions | Yes | Yes (ccsdspy.utils.read_primary_headers, split_by_apid) |
Yes (ccsds_generator) |
Yes (parse_space_packets) |
| Tabular output | Arrow / Polars / pandas, Parquet export (optional extras) | NumPy arrays (pandas via SPaC-Kit) | xarray (optional extra) | - |
| Live input | Planned: asyncio TCP / WebSocket | - | Sockets and UDP (synchronous) | Incremental parsing from a deque |
| Segmented packet reassembly | - | - | Yes | - |
If you need to decode packet contents against a mission packet database, use ccsdspy or space-packet-parser; both are mature and used on flight missions. If you build or parse ECSS PUS / CFDP traffic, use spacepackets. fast-ccsds-parser focuses on getting large packet dumps and live streams into data-analysis tools quickly and with little memory.
Performance
benchmarks/bench.py measures packet throughput and
peak memory, running each case in a fresh process. Results for 1,000,000
synthetic 32-byte packets (32 MB), Python 3.12, Apple M4, pandas 3.0.6,
polars 2.0.0, pyarrow 26.0.0:
| Case | Time | Peak RSS |
|---|---|---|
fast-ccsds-parser: scan_file (headers only) |
0.15 s | 80 MB |
fast-ccsds-parser: scan_file (with payloads) |
0.29 s | 122 MB |
fast-ccsds-parser: scan_file().to_pandas() |
0.45 s | 188 MB |
fast-ccsds-parser: scan_file().to_polars() |
0.62 s | 232 MB |
fast-ccsds-parser: write_parquet() |
0.61 s | 232 MB |
| fast-ccsds-parser: header walk | 0.47 s | 53 MB |
fast-ccsds-parser: iter_file (streaming) |
0.88 s | 23 MB |
| fast-ccsds-parser: pandas via per-packet dicts | 2.25 s | 677 MB |
ccsdspy: read_primary_headers |
21.28 s | 123 MB |
ccsdspy: iter_packet_bytes (raw bytes only) |
0.41 s | 63 MB |
space_packet_parser: ccsds_generator (raw bytes only) |
0.36 s | 72 MB |
(ccsdspy 2.0.1 and space_packet_parser 6.2.0 rows were measured in the same setup.)
What this shows:
- The columnar scan is the fastest way to a DataFrame:
to_pandas()is about 5x faster than building from per-packet dicts and uses about a quarter of the memory (188 MB vs 677 MB for 32 MB of input). - Streaming with
iter_fileuses the least memory of all cases. - Iterating raw packet bytes is about 2x faster in ccsdspy and
space_packet_parser, because
iter_filealso decodes every header and copies each payload. This is a known optimization target.
Run it yourself:
pip install -e ".[all]" ccsdspy space_packet_parser
python benchmarks/bench.py # synthetic data
python benchmarks/bench.py path/to/dump.bin # your own dump
Roadmap
- Phase 2a (done in 0.2.0): columnar output (
scan_file(),to_arrow(),to_polars(),to_pandas(),write_parquet()) that keeps memory close to the input size, with Arrow / Polars / pandas as optional extras. - Phase 2b: decoding CCSDS time codes (CUC / CDS) from secondary headers into timestamps.
- Phase 2c: asyncio parsing of live TCP socket / WebSocket streams (WebSocket as an optional extra).
- Phase 3: move the hot loops that benchmarks show are slow to Rust (PyO3), keeping the Python API unchanged.
Development
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest # offline test suite
ruff check . # lint
Tests marked network additionally parse public, real-world packet dumps
downloaded on the fly; they skip automatically when offline (CI runs them).
Releasing (PyPI)
Publishing uses trusted publishing (no API tokens). One-time setup is
described in .github/workflows/publish-testpypi.yml
(the file name is historical; it publishes to PyPI). Bump __version__ in
src/fast_ccsds_parser/__init__.py, add a CHANGELOG entry, then:
git tag vX.Y.Z
git push origin vX.Y.Z
The workflow builds sdist + wheel, runs twine check, and uploads to
PyPI.
License
Metadata
Release files for fast-ccsds-parser 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| fast_ccsds_parser-0.2.0.tar.gz | 40.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fast_ccsds_parser-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 58.9 kB
Release files / fast_ccsds_parser-0.2.0.tar.gz
| Download URL | fast_ccsds_parser-0.2.0.tar.gz |
|---|---|
| Size | 40.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4275729ac4ba32605ad42c034e0da152c33e8de352cc39e03cd9d5427ace5caf
|
|
BLAKE2b-256 checksum How to use checksums |
2936f5ee8cb5593ebd0bc2efaa7dc0bde7ed5a1991951dacfdaf61f7d69203a9
|
| 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 Oct 10, 2026.
Transparency logRelease files / fast_ccsds_parser-0.2.0-py3-none-any.whl
| Download URL | fast_ccsds_parser-0.2.0-py3-none-any.whl |
|---|---|
| Size | 18.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
aa44404ad7c532ec9104a29f3203c003fd57d2c88c7dfe7510fa2ed092588804
|
|
BLAKE2b-256 checksum How to use checksums |
dee2373b2d28db196ccb80b0fe65ff4b5b358789a07d3827c00faa23364de06b
|
| 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 Oct 10, 2026.
Transparency log