This is the initial go at the library.

This is the initial pass. It sets some things up and lets me get
some ideas out of my head.
This commit is contained in:
Myrddin Dundragon 2025-06-20 20:49:51 -04:00
parent 6eada9955f
commit 46c02a152f
6 changed files with 500 additions and 21 deletions

7
Cargo.lock generated Normal file
View File

@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "ccsds_spp"
version = "0.0.0"

View File

@ -1,3 +1,51 @@
# ccsds_spp
CCSDS Space Packet Protocol
A `no_std`, heapless Rust implementation of the
[CCSDS Space Packet Protocol][SPP], suitable for embedded and real-time
systems.
This library is intended for use in satellite software, space simulation,
and telemetry/telecommand tooling. It is designed to be robust, portable,
and usable in constrained environments. No allocator required.
---
## Features
* `no_std` compatible
* Heapless: works in memory-constrained environments
* Bit-accurate parsing and encoding
* `FromBits` / `IntoBits` traits for masked operations
* CCSDS-compliant `Version`, `PacketType`, `APID` support
* Designed for integration into embedded systems or FFI-safe libraries
---
## Example
```rust
use ccsds_space_packet::{PacketHeader, PacketType, Version, ApId};
let mut header = PacketHeader { data: [0u8; 6] };
header.set_version(Version::One);
header.set_packet_type(PacketType::Telemetry);
header.set_ap_id(ApId::from(42));
assert_eq!(header.get_version(), Version::One);
assert_eq!(header.get_packet_type(), PacketType::Telemetry);
assert_eq!(u16::from(header.get_ap_id()), 42);
```
## Design Notes
This is a work in progress.
Focus is on correctness and portability over performance for now.
Library avoids panics where possible and returns Result<T, &str> for error handling.
## References
[SPP]: https://ccsds.org/wp-content/uploads/gravity_forms/5-448e85c647331d9cbaf66c096458bdd5/2025/01//133x0b2e2.pdf "CCSDS Space Packet Protocol PDF"

46
src/bits.rs Normal file
View File

@ -0,0 +1,46 @@
pub trait FromBytes: Sized
{
type Error;
fn from_bytes(bytes: &[u8]) -> Result<Parsed<Self>, Self::Error>;
}
pub trait IntoBytes
{
type Error;
fn into_bytes(self, buffer: &mut [u8]) -> Result<usize, Self::Error>;
}
pub trait FromBits: Sized
{
type Error;
fn from_bits(bytes: &[u8], mask: &[u8]) -> Result<Self, Self::Error>;
}
pub trait IntoBits
{
type Error;
fn into_bits(self, bytes: &mut [u8], mask: &[u8]) -> Result<(), Self::Error>;
}
pub struct Parsed<T>
{
pub bytes_read: usize,
pub data: T
}
impl<T> Parsed<T>
{
pub fn new(bytes_read: usize, data: T) -> Self
{
Parsed
{
bytes_read,
data
}
}
}

389
src/header.rs Normal file
View File

@ -0,0 +1,389 @@
use crate::bits::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ApId
{
value: u16
}
impl ApId
{
pub const MASK_U16: u16 = 0b0000011111111111;
pub const MASK_ARR: [u8; 2] = [0b00000111, 0b11111111];
}
impl From<u16> for ApId
{
fn from(value: u16) -> Self
{
ApId
{
value
}
}
}
impl From<ApId> for u16
{
fn from(apid: ApId) -> u16
{
apid.value
}
}
impl FromBytes for ApId
{
type Error = &'static str;
fn from_bytes(bytes: &[u8]) -> Result<Parsed<Self>, Self::Error>
{
if bytes.len() < 2usize { return Err("ERROR"); }
let id: u16 = (((bytes[0] as u16) << 8) & ApId::MASK_ARR[0]) |
((bytes[1] as u16) & ApId::MASK_ARR[1]);
Ok(Parsed::new(2usize, id.into()))
}
}
impl IntoBytes for ApId
{
type Error = &'static str;
fn into_bytes(self, buffer: &mut [u8]) -> Result<usize, Self::Error>
{
if buffer.len() < 2usize { return Err("Error"); }
let id: u16 = self.from();
buffer[0] = ((id >> 8) as u8) & ApId::MASK_ARR[0];
buffer[1] = (id as u8) & ApId::MASK_ARR[0];
Ok(2usize)
}
}
impl FromBits for ApId
{
type Error = &'static str;
fn from_bits(bytes: &[u8], mask: &[u8]) -> Result<Self, Self::Error>
{
if bytes.len() < 2usize || mask.len() < 2usize { return Err("Error"); }
let id: u16 = (((bytes[0] & mask[0]) as u16) << 8) |
((bytes[1] & mask[1]) as u16);
Ok(id.into())
}
}
impl IntoBits for ApId
{
type Error = &'static str;
fn into_bits(self, bytes: &mut [u8], mask: &[u8]) -> Result<(), Self::Error>
{
if bytes.len() < 2usize || mask.len() < 2usize { return Err("Error"); }
let id: u16 = self.from();
bytes[0] = ((id >> 8) as u8) & mask[0];
bytes[1] = (id as u8) & mask[1];
Ok(())
}
}
pub enum Version
{
One
}
const VERSION_ONE: u8 = 0b00000000;
impl Version
{
pub const MASK_U8: u8 = 0b11100000;
pub const MASK_ARR: [u8; 1] = [Version::MASK_U8];
}
impl FromBytes for Version
{
type Error = &'static str;
fn from_bytes(bytes: &[u8]) -> Result<Parsed<Self>, Self::Error>
{
if bytes.len() < 1usize { return Err("ERROR"); }
match bytes[0] & Version::MASK_U8
{
VERSION_ONE => { Ok(Parsed::new(1usize, Version::One)) }
_ => { Err("Error: Unknown Version") }
}
}
}
impl IntoBytes for Version
{
type Error = &'static str;
fn into_bytes(self, buffer: &mut [u8]) -> Result<usize, Self::Error>
{
if buffer.len() < 1 { return Err("Error"); }
match self
{
Version::One => { buffer[0] = VERSION_ONE; }
}
Ok(1)
}
}
impl FromBits for Version
{
type Error = &'static str;
fn from_bits(bytes: &[u8], mask: &[u8]) -> Result<Self, Self::Error>
{
if bytes.len() < 1 || mask.len() < 1 { return Err("Error"); }
match bytes[0] & mask[0]
{
VERSION_ONE => { Ok(Version::One) }
_ => { Err("ERROR") }
}
}
}
impl IntoBits for Version
{
type Error = &'static str;
fn into_bits(self, bytes: &mut [u8], mask: &[u8]) -> Result<(), Self::Error>
{
if bytes.len() < 1 || mask.len() < 1 { return Err("Error"); }
match self
{
Version::One => { bytes[0] |= VERSION_ONE & mask[0]; }
}
Ok(())
}
}
pub enum PacketType
{
Telemetry,
Telecommand
}
const TYPE_TELEMETRY: u8 = 0b00000000;
const TYPE_TELECOMMAND: u8 = 0b00010000;
impl PacketType
{
pub const MASK_U8: u8 = 0b00010000;
pub const MASK_ARR: [u8; 1] = [PacketType::MASK_U8];
}
impl FromBytes for PacketType
{
type Error = &'static str;
fn from_bytes(bytes: &[u8]) -> Result<Parsed<Self>, Self::Error>
{
if bytes.len() < 1usize { return Err("ERROR"); }
match bytes[0] & PacketType::MASK_U8
{
TYPE_TELEMETRY => { Ok(Parsed::new(1usize, PacketType::Telemetry)) }
TYPE_TELECOMMAND => { Ok(Parsed::new(1usize, PacketType::Telecommand)) }
_ => { Err("Error: Unknown PacketType.") }
}
}
}
impl IntoBytes for PacketType
{
type Error = &'static str;
fn into_bytes(self, buffer: &mut [u8]) -> Result<usize, Self::Error>
{
match self
{
PacketType::Telemetry => { buffer[0] = TYPE_TELEMETRY; }
PacketType::Telecommand => { buffer[0] = TYPE_TELECOMMAND; }
}
Ok(1)
}
}
impl FromBits for PacketType
{
type Error = &'static str;
fn from_bits(bytes: &[u8], mask: &[u8]) -> Result<Self, Self::Error>
{
if bytes.len() < 1 || mask.len() < 1 { return Err("Error"); }
match bytes[0] & mask[0]
{
TYPE_TELEMETRY => { Ok(PacketType::Telemetry) }
TYPE_TELECOMMAND => { Ok(PacketType::Telecommand) }
_ => { Err("ERROR") }
}
}
}
impl IntoBits for PacketType
{
type Error = &'static str;
fn into_bits(self, bytes: &mut [u8], mask: &[u8]) -> Result<(), Self::Error>
{
if bytes.len() < 1 || mask.len() < 1 { return Err("Error"); }
match self
{
PacketType::Telemetry => { bytes[0] |= TYPE_TELEMETRY & mask[0]; }
PacketType::Telecommand => { bytes[0] |= TYPE_TELECOMMAND & mask[0]; }
}
Ok(())
}
}
const HEADER_SIZE: usize = 6usize;
pub struct PacketHeader
{
data: [u8; HEADER_SIZE]
}
impl PacketHeader
{
pub fn get_version(&self) -> Version
{
match Version::from_bits(&self.data[0..1], &Version::MASK_ARR)
{
Ok(version) =>
{
version
}
Err(error) =>
{
println!("Error: {}", error);
Version::One
}
}
}
pub fn get_packet_type(&self) -> PacketType
{
match PacketType::from_bits(&self.data[0..1], &PacketType::MASK_ARR)
{
Ok(packet_type) =>
{
packet_type
}
Err(error) =>
{
println!("Error: {}", error);
PacketType::Telemetry
}
}
}
pub fn get_ap_id(&self) -> ApId
{
match ApId::from_bits(&self.data[0..2], &ApId::MASK_ARR)
{
Ok(id) =>
{
id
}
Err(error) =>
{
println!("Error: {}", error);
0.into()
}
}
}
pub fn set_version(&mut self, version: Version)
{
match version.into_bits(&mut self.data[0..1], &Version::MASK_ARR)
{
Ok(_) => {}
Err(error) => { println!("Error: {}", error); }
}
}
pub fn set_packet_type(&mut self, packet_type: PacketType)
{
match packet_type.into_bits(&mut self.data[0..1], &PacketType::MASK_ARR)
{
Ok(_) => {}
Err(error) => { println!("Error: {}", error); }
}
}
pub fn set_ap_id(&mut self, id: ApId)
{
match id.into_bits(&mut self.data[0..2], &ApId::MASK_ARR)
{
Ok(_) => {}
Err(error) => { println!("Error: {}", error); }
}
}
}
impl FromBytes for PacketHeader
{
type Error = &'static str;
fn from_bytes(bytes: &[u8]) -> Result<Parsed<Self>, Self::Error>
{
if bytes.len() < HEADER_SIZE { return Err("ERROR"); }
let mut header = PacketHeader { data: [0u8; HEADER_SIZE] };
header.data.copy_from_slice(&bytes[0..HEADER_SIZE]);
Ok(Parsed::new(HEADER_SIZE, header))
}
}
impl IntoBytes for PacketHeader
{
type Error = &'static str;
fn into_bytes(self, buffer: &mut [u8]) -> Result<usize, Self::Error>
{
if buffer.len() < HEADER_SIZE { return Err("ERROR"); }
buffer[0..HEADER_SIZE].copy_from_slice(&self.data);
Ok(HEADER_SIZE)
}
}

9
src/lib.rs Normal file
View File

@ -0,0 +1,9 @@
//! CCSDS Space Packet Protocol
mod project;
mod bits;
mod header;
pub use crate::bits::*;
pub use crate::header::*;

View File

@ -1,20 +0,0 @@
//! CCSDS Space Packet Protocol
mod project;
/// Print the version of the project.
fn print_version()
{
println!("{} v{}", project::get_name(), project::get_version());
}
/// The usual starting point of your project.
fn main()
{
print_version();
}