From 46c02a152fc765f1a9ba5fb39f0d0e870eb83a38 Mon Sep 17 00:00:00 2001 From: Myrddin Dundragon Date: Fri, 20 Jun 2025 20:49:51 -0400 Subject: [PATCH] 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. --- Cargo.lock | 7 + README.md | 50 ++++++- src/bits.rs | 46 ++++++ src/header.rs | 389 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 9 ++ src/main.rs | 20 --- 6 files changed, 500 insertions(+), 21 deletions(-) create mode 100644 Cargo.lock create mode 100644 src/bits.rs create mode 100644 src/header.rs create mode 100644 src/lib.rs delete mode 100644 src/main.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..efa0a9a --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/README.md b/README.md index 3e741c6..0f3cb01 100644 --- a/README.md +++ b/README.md @@ -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 for error handling. + + +## References +[SPP]: https://ccsds.org/wp-content/uploads/gravity_forms/5-448e85c647331d9cbaf66c096458bdd5/2025/01//133x0b2e2.pdf "CCSDS Space Packet Protocol PDF" diff --git a/src/bits.rs b/src/bits.rs new file mode 100644 index 0000000..f496263 --- /dev/null +++ b/src/bits.rs @@ -0,0 +1,46 @@ +pub trait FromBytes: Sized +{ + type Error; + + fn from_bytes(bytes: &[u8]) -> Result, Self::Error>; +} + +pub trait IntoBytes +{ + type Error; + + fn into_bytes(self, buffer: &mut [u8]) -> Result; +} + +pub trait FromBits: Sized +{ + type Error; + + fn from_bits(bytes: &[u8], mask: &[u8]) -> Result; +} + +pub trait IntoBits +{ + type Error; + + fn into_bits(self, bytes: &mut [u8], mask: &[u8]) -> Result<(), Self::Error>; +} + + +pub struct Parsed +{ + pub bytes_read: usize, + pub data: T +} + +impl Parsed +{ + pub fn new(bytes_read: usize, data: T) -> Self + { + Parsed + { + bytes_read, + data + } + } +} diff --git a/src/header.rs b/src/header.rs new file mode 100644 index 0000000..58d804f --- /dev/null +++ b/src/header.rs @@ -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 for ApId +{ + fn from(value: u16) -> Self + { + ApId + { + value + } + } +} + +impl From for u16 +{ + fn from(apid: ApId) -> u16 + { + apid.value + } +} + +impl FromBytes for ApId +{ + type Error = &'static str; + + fn from_bytes(bytes: &[u8]) -> Result, 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 + { + 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 + { + 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, 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 + { + 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 + { + 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, 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 + { + 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 + { + 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, 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 + { + if buffer.len() < HEADER_SIZE { return Err("ERROR"); } + + buffer[0..HEADER_SIZE].copy_from_slice(&self.data); + + Ok(HEADER_SIZE) + } +} + diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..85eb74f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,9 @@ +//! CCSDS Space Packet Protocol + +mod project; + +mod bits; +mod header; + +pub use crate::bits::*; +pub use crate::header::*; diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index b3744ed..0000000 --- a/src/main.rs +++ /dev/null @@ -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(); -}