commit 396cea844588551c0be2f88e16229369f776f33d Author: Robin Mueller Date: Sat Jun 18 22:48:51 2022 +0200 Init spacepackets crate diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..20017a4 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "spacepackets" +version = "0.1.0" +edition = "2021" +authors = ["Robin Mueller "] +description = "Generic implementations for various CCSDS and ECSS packet standards" +homepage = "https://egit.irs.uni-stuttgart.de/rust/spacepackets" +repository = "https://egit.irs.uni-stuttgart.de/rust/spacepackets" +license = "Apache-2.0" +keywords = ["no-std", "space", "packets", "ccsds", "ecss"] +categories = ["aerospace", "aerospace::space-protocols", "no-std", "hardware-support", "embedded"] +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +num = "0.4" +serde = "1.0.137" +zerocopy = "0.6.1" +crc = "3.0.0" +delegate = "0.7.0" + +[dependencies.heapless] +version = "0.7.14" +optional = true + +[dev-dependencies] + +[dev-dependencies.postcard] +version = "0.7.3" +features = ["use-std"] + +[features] + +default = ["heapless", "alloc"] +alloc = ["serde/alloc"] diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..16fe87b --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..292e580 --- /dev/null +++ b/NOTICE @@ -0,0 +1,3 @@ +Generic implementations for various CCSDS and ECSS packet standards. + +This software contains code developed at the University of Stuttgart. diff --git a/examples/test.rs b/examples/test.rs new file mode 100644 index 0000000..49a7405 --- /dev/null +++ b/examples/test.rs @@ -0,0 +1,67 @@ +use postcard::{from_bytes, to_stdvec}; +use serde::{Deserialize, Serialize}; +use zerocopy::byteorder::{I32, U16}; +use zerocopy::{AsBytes, FromBytes, NetworkEndian, Unaligned}; + +#[derive(AsBytes, FromBytes, Unaligned, Debug, Eq, PartialEq)] +#[repr(C, packed)] +struct ZeroCopyTest { + some_bool: u8, + some_u16: U16, + some_i32: I32, + some_float: [u8; 4], +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +struct PostcardTest { + some_bool: u8, + some_u16: u16, + some_i32: i32, + some_float: f32, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +struct SliceSerTest<'slice> { + some_u8: u8, + some_u32: u32, + some_slice: &'slice [u8], +} + +fn main() { + let pc_test = PostcardTest { + some_bool: true as u8, + some_u16: 0x42, + some_i32: -200, + some_float: 7.7_f32, + }; + + let out = to_stdvec(&pc_test).unwrap(); + println!("{:#04x?}", out); + + let sample_hk = ZeroCopyTest { + some_bool: true as u8, + some_u16: U16::from(0x42), + some_i32: I32::from(-200), + some_float: 7.7_f32.to_be_bytes(), + }; + let mut slice = [0; 11]; + sample_hk.write_to(slice.as_mut_slice()); + println!("{:#04x?}", slice); + + let ser_vec; + { + let test_buf = [0, 1, 2, 3]; + let test_with_slice = SliceSerTest { + some_u8: 12, + some_u32: 1, + some_slice: test_buf.as_slice(), + }; + ser_vec = to_stdvec(&test_with_slice).unwrap(); + println!("{:#04x?}", out); + } + + { + let test_deser: SliceSerTest = from_bytes(ser_vec.as_slice()).unwrap(); + println!("{:?}", test_deser); + } +} diff --git a/src/ecss.rs b/src/ecss.rs new file mode 100644 index 0000000..35eb7f2 --- /dev/null +++ b/src/ecss.rs @@ -0,0 +1,37 @@ +use crate::{CcsdsPacket, PacketError}; +use core::mem::size_of; +use crc::{Crc, CRC_16_IBM_3740}; +use serde::{Deserialize, Serialize}; + +/// CRC algorithm used by the PUS standard +pub const CRC_CCITT_FALSE: Crc = Crc::::new(&CRC_16_IBM_3740); +pub const CCSDS_HEADER_LEN: usize = size_of::(); + +/// All PUS versions. Only PUS C is supported by this library +#[derive(PartialEq, Copy, Clone, Serialize, Deserialize, Debug)] +pub enum PusVersion { + EsaPus = 0, + PusA = 1, + PusC = 2, +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum PusError { + VersionNotSupported(PusVersion), + IncorrectCrc(u16), + RawDataTooShort(usize), + NoRawData, + /// CRC16 needs to be calculated first + CrcCalculationMissing, + OtherPacketError(PacketError), +} + +pub trait PusPacket: CcsdsPacket { + const PUS_VERSION: PusVersion = PusVersion::PusC; + + fn service(&self) -> u8; + fn subservice(&self) -> u8; + + fn user_data(&self) -> Option<&[u8]>; + fn crc16(&self) -> Option; +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..916df41 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,586 @@ +//! # Space related components including CCSDS and ECSS packet standards +#![no_std] +extern crate alloc; + +use crate::ecss::CCSDS_HEADER_LEN; +use serde::{Deserialize, Serialize}; + +pub mod ecss; +pub mod tc; +pub mod tm; + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum PacketError { + /// The passed slice is too small. Returns the required size of the failed size chgeck + ToBytesSliceTooSmall(usize), + /// The [zerocopy] library failed to write to bytes + ToBytesZeroCopyError, + FromBytesZeroCopyError, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone)] +pub enum PacketType { + Tm = 0, + Tc = 1, +} + +impl TryFrom for PacketType { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + x if x == PacketType::Tm as u8 => Ok(PacketType::Tm), + x if x == PacketType::Tc as u8 => Ok(PacketType::Tc), + _ => Err(()), + } + } +} + +pub fn packet_type_in_raw_packet_id(packet_id: u16) -> PacketType { + PacketType::try_from((packet_id >> 12) as u8 & 0b1).unwrap() +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone)] +pub enum SequenceFlags { + ContinuationSegment = 0b00, + FirstSegment = 0b01, + LastSegment = 0b10, + Unsegmented = 0b11, +} + +impl TryFrom for SequenceFlags { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + x if x == SequenceFlags::ContinuationSegment as u8 => { + Ok(SequenceFlags::ContinuationSegment) + } + x if x == SequenceFlags::FirstSegment as u8 => Ok(SequenceFlags::FirstSegment), + x if x == SequenceFlags::LastSegment as u8 => Ok(SequenceFlags::LastSegment), + x if x == SequenceFlags::Unsegmented as u8 => Ok(SequenceFlags::Unsegmented), + _ => Err(()), + } + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone)] +pub struct PacketId { + pub ptype: PacketType, + pub sec_header_flag: bool, + apid: u16, +} + +impl PacketId { + pub fn new(ptype: PacketType, sec_header_flag: bool, apid: u16) -> Option { + let mut pid = PacketId { + ptype, + sec_header_flag, + apid: 0, + }; + pid.set_apid(apid).then(|| pid) + } + + pub fn set_apid(&mut self, apid: u16) -> bool { + if apid > num::pow(2, 11) - 1 { + return false; + } + self.apid = apid; + true + } + + pub fn apid(&self) -> u16 { + self.apid + } + + pub fn raw(&self) -> u16 { + ((self.ptype as u16) << 12) | ((self.sec_header_flag as u16) << 11) | self.apid + } +} + +impl From for PacketId { + fn from(raw_id: u16) -> Self { + PacketId { + ptype: PacketType::try_from(((raw_id >> 12) & 0b1) as u8).unwrap(), + sec_header_flag: ((raw_id >> 11) & 0b1) != 0, + apid: raw_id & 0x7FF, + } + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Copy, Clone)] +pub struct PacketSequenceCtrl { + pub seq_flags: SequenceFlags, + ssc: u16, +} + +impl PacketSequenceCtrl { + pub fn new(seq_flags: SequenceFlags, ssc: u16) -> Option { + let mut psc = PacketSequenceCtrl { seq_flags, ssc: 0 }; + psc.set_ssc(ssc).then(|| psc) + } + pub fn raw(&self) -> u16 { + ((self.seq_flags as u16) << 14) | self.ssc + } + + pub fn set_ssc(&mut self, ssc: u16) -> bool { + if ssc > num::pow(2, 14) - 1 { + return false; + } + self.ssc = ssc; + true + } + + pub fn ssc(&self) -> u16 { + self.ssc + } +} + +impl From for PacketSequenceCtrl { + fn from(raw_id: u16) -> Self { + PacketSequenceCtrl { + seq_flags: SequenceFlags::try_from(((raw_id >> 14) & 0b11) as u8).unwrap(), + ssc: raw_id & SSC_MASK, + } + } +} + +macro_rules! sph_from_other { + ($Self: path, $other: path) => { + impl From<$other> for $Self { + fn from(other: $other) -> Self { + Self::from_composite_fields( + other.packet_id(), + other.psc(), + other.data_len(), + Some(other.ccsds_version()), + ) + } + } + }; +} + +const SSC_MASK: u16 = 0x3FFF; +const VERSION_MASK: u16 = 0xE000; + +/// Generic trait to access fields of a CCSDS space packet header according to CCSDS 133.0-B-2 +pub trait CcsdsPacket { + fn ccsds_version(&self) -> u8; + fn packet_id(&self) -> PacketId; + fn psc(&self) -> PacketSequenceCtrl; + + /// Retrieve data length field + fn data_len(&self) -> u16; + /// Retrieve the total packet size based on the data length field + fn total_len(&self) -> usize { + usize::from(self.data_len()) + CCSDS_HEADER_LEN + 1 + } + + /// Retrieve 13 bit Packet Identification field. Can usually be retrieved with a bitwise AND + /// of the first 2 bytes with 0x1FFF + #[inline] + fn packet_id_raw(&self) -> u16 { + self.packet_id().raw() + } + /// Retrieve Packet Sequence Count + #[inline] + fn psc_raw(&self) -> u16 { + self.psc().raw() + } + + #[inline] + /// Retrieve Packet Type (TM: 0, TC: 1) + fn ptype(&self) -> PacketType { + // This call should never fail because only 0 and 1 can be passed to the try_from call + self.packet_id().ptype + } + + #[inline] + fn is_tm(&self) -> bool { + self.ptype() == PacketType::Tm + } + + #[inline] + fn is_tc(&self) -> bool { + self.ptype() == PacketType::Tc + } + + /// Retrieve the secondary header flag. Returns true if a secondary header is present + /// and false if it is not + #[inline] + fn sec_header_flag(&self) -> bool { + self.packet_id().sec_header_flag + } + + /// Retrieve Application Process ID + #[inline] + fn apid(&self) -> u16 { + self.packet_id().apid + } + + #[inline] + fn ssc(&self) -> u16 { + self.psc().ssc + } + + #[inline] + fn sequence_flags(&self) -> SequenceFlags { + // This call should never fail because the mask ensures that only valid values are passed + // into the try_from function + self.psc().seq_flags + } +} + +pub trait CcsdsPrimaryHeader { + fn from_composite_fields( + packet_id: PacketId, + psc: PacketSequenceCtrl, + data_len: u16, + version: Option, + ) -> Self; +} + +pub mod ser { + use crate::{ + CcsdsPacket, CcsdsPrimaryHeader, PacketId, PacketSequenceCtrl, PacketType, SequenceFlags, + }; + + /// Space Packet Primary Header according to CCSDS 133.0-B-2 + #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Copy, Clone)] + pub struct SpHeader { + pub version: u8, + pub packet_id: PacketId, + pub psc: PacketSequenceCtrl, + pub data_len: u16, + } + impl Default for SpHeader { + fn default() -> Self { + SpHeader { + version: 0, + packet_id: PacketId { + ptype: PacketType::Tm, + apid: 0, + sec_header_flag: true, + }, + psc: PacketSequenceCtrl { + seq_flags: SequenceFlags::Unsegmented, + ssc: 0, + }, + data_len: 0, + } + } + } + impl SpHeader { + pub fn new(apid: u16, ptype: PacketType, ssc: u16, data_len: u16) -> Option { + if ssc > num::pow(2, 14) - 1 || apid > num::pow(2, 11) - 1 { + return None; + } + let mut header = SpHeader::default(); + header.packet_id.apid = apid; + header.packet_id.ptype = ptype; + header.psc.ssc = ssc; + header.data_len = data_len; + Some(header) + } + + pub fn tm(apid: u16, ssc: u16, data_len: u16) -> Option { + Self::new(apid, PacketType::Tm, ssc, data_len) + } + + pub fn tc(apid: u16, ssc: u16, data_len: u16) -> Option { + Self::new(apid, PacketType::Tc, ssc, data_len) + } + } + + impl CcsdsPacket for SpHeader { + #[inline] + fn ccsds_version(&self) -> u8 { + self.version + } + + #[inline] + fn packet_id(&self) -> PacketId { + self.packet_id + } + + #[inline] + fn psc(&self) -> PacketSequenceCtrl { + self.psc + } + + #[inline] + fn data_len(&self) -> u16 { + self.data_len + } + } + + impl CcsdsPrimaryHeader for SpHeader { + fn from_composite_fields( + packet_id: PacketId, + psc: PacketSequenceCtrl, + data_len: u16, + version: Option, + ) -> Self { + let mut version_to_set = 0b000; + if let Some(version) = version { + version_to_set = version; + } + SpHeader { + version: version_to_set, + packet_id, + psc, + data_len, + } + } + } + + sph_from_other!(SpHeader, crate::zc::SpHeader); +} + +pub mod zc { + use crate::{CcsdsPacket, CcsdsPrimaryHeader, PacketId, PacketSequenceCtrl, VERSION_MASK}; + use zerocopy::byteorder::NetworkEndian; + use zerocopy::{AsBytes, FromBytes, Unaligned, U16}; + + #[derive(FromBytes, AsBytes, Unaligned, Debug)] + #[repr(C)] + pub struct SpHeader { + version_packet_id: U16, + psc: U16, + data_len: U16, + } + + impl SpHeader { + pub fn new( + packet_id: PacketId, + psc: PacketSequenceCtrl, + data_len: u16, + version: Option, + ) -> Self { + let mut version_packet_id = packet_id.raw(); + if let Some(version) = version { + version_packet_id = ((version as u16) << 13) | packet_id.raw() + } + SpHeader { + version_packet_id: U16::from(version_packet_id), + psc: U16::from(psc.raw()), + data_len: U16::from(data_len), + } + } + + pub fn from_bytes(slice: &(impl AsRef<[u8]> + ?Sized)) -> Option { + SpHeader::read_from(slice.as_ref()) + } + + pub fn to_bytes(&self, slice: &mut (impl AsMut<[u8]> + ?Sized)) -> Option<()> { + self.write_to(slice.as_mut()) + } + } + + impl CcsdsPacket for SpHeader { + #[inline] + fn ccsds_version(&self) -> u8 { + ((self.version_packet_id.get() >> 13) as u8) & 0b111 + } + + fn packet_id(&self) -> PacketId { + PacketId::from(self.packet_id_raw()) + } + fn psc(&self) -> PacketSequenceCtrl { + PacketSequenceCtrl::from(self.psc_raw()) + } + + #[inline] + fn data_len(&self) -> u16 { + self.data_len.get() + } + + fn packet_id_raw(&self) -> u16 { + self.version_packet_id.get() & (!VERSION_MASK) + } + + fn psc_raw(&self) -> u16 { + self.psc.get() + } + } + + impl CcsdsPrimaryHeader for SpHeader { + fn from_composite_fields( + packet_id: PacketId, + psc: PacketSequenceCtrl, + data_len: u16, + version: Option, + ) -> Self { + SpHeader::new(packet_id, psc, data_len, version) + } + } + + sph_from_other!(SpHeader, crate::ser::SpHeader); +} + +#[cfg(test)] +mod tests { + use crate::ser::SpHeader; + use crate::{ + packet_type_in_raw_packet_id, zc, CcsdsPacket, CcsdsPrimaryHeader, PacketId, + PacketSequenceCtrl, PacketType, SequenceFlags, + }; + use alloc::vec; + use postcard::{from_bytes, to_stdvec}; + + #[test] + fn test_helpers() { + assert_eq!( + SequenceFlags::try_from(0b00).expect("SEQ flag creation failed"), + SequenceFlags::ContinuationSegment + ); + assert_eq!( + SequenceFlags::try_from(0b01).expect("SEQ flag creation failed"), + SequenceFlags::FirstSegment + ); + assert_eq!( + SequenceFlags::try_from(0b10).expect("SEQ flag creation failed"), + SequenceFlags::LastSegment + ); + assert_eq!( + SequenceFlags::try_from(0b11).expect("SEQ flag creation failed"), + SequenceFlags::Unsegmented + ); + assert!(SequenceFlags::try_from(0b100).is_err()); + assert!(PacketType::try_from(0b10).is_err()); + let packet_id = + PacketId::new(PacketType::Tm, false, 0x42).expect("Packet ID creation failed"); + assert_eq!(packet_id.raw(), 0x0042); + let packet_id_from_raw = PacketId::from(packet_id.raw()); + assert_eq!( + packet_type_in_raw_packet_id(packet_id.raw()), + PacketType::Tm + ); + assert_eq!(packet_id_from_raw, packet_id); + + let packet_id_invalid = PacketId::new(PacketType::Tc, true, 0xFFFF); + assert!(packet_id_invalid.is_none()); + let packet_id_from_new = PacketId::new(PacketType::Tm, false, 0x42).unwrap(); + assert_eq!(packet_id_from_new, packet_id); + let mut psc = PacketSequenceCtrl::new(SequenceFlags::ContinuationSegment, 77) + .expect("PSC creation failed"); + assert_eq!(psc.raw(), 77); + let psc_from_raw = PacketSequenceCtrl::from(psc.raw()); + assert_eq!(psc_from_raw, psc); + // Fails because SSC is limited to 14 bits + assert!(!psc.set_ssc(num::pow(2, 15))); + assert_eq!(psc.raw(), 77); + + let psc_invalid = PacketSequenceCtrl::new(SequenceFlags::FirstSegment, 0xFFFF); + assert!(psc_invalid.is_none()); + let psc_from_new = PacketSequenceCtrl::new(SequenceFlags::ContinuationSegment, 77).unwrap(); + assert_eq!(psc_from_new, psc); + } + + #[test] + fn test_serde_sph() { + let sp_header = SpHeader::tc(0x42, 12, 0).expect("Error creating SP header"); + assert_eq!(sp_header.ccsds_version(), 0b000); + assert!(sp_header.is_tc()); + assert!(sp_header.sec_header_flag()); + assert_eq!(sp_header.ptype(), PacketType::Tc); + assert_eq!(sp_header.ssc(), 12); + assert_eq!(sp_header.apid(), 0x42); + assert_eq!(sp_header.sequence_flags(), SequenceFlags::Unsegmented); + assert_eq!(sp_header.data_len(), 0); + let output = to_stdvec(&sp_header).unwrap(); + let sp_header: SpHeader = from_bytes(&output).unwrap(); + assert_eq!(sp_header.version, 0b000); + assert!(sp_header.packet_id.sec_header_flag); + assert_eq!(sp_header.ptype(), PacketType::Tc); + assert_eq!(sp_header.ssc(), 12); + assert_eq!(sp_header.apid(), 0x42); + assert_eq!(sp_header.sequence_flags(), SequenceFlags::Unsegmented); + assert_eq!(sp_header.packet_id_raw(), 0x1842); + assert_eq!(sp_header.psc_raw(), 0xC00C); + assert_eq!(sp_header.ccsds_version(), 0b000); + assert_eq!(sp_header.data_len, 0); + + let sp_header = SpHeader::tm(0x7, 22, 36).expect("Error creating SP header"); + assert_eq!(sp_header.ccsds_version(), 0b000); + assert!(sp_header.is_tm()); + assert!(sp_header.sec_header_flag()); + assert_eq!(sp_header.ptype(), PacketType::Tm); + assert_eq!(sp_header.ssc(), 22); + assert_eq!(sp_header.apid(), 0x07); + assert_eq!(sp_header.sequence_flags(), SequenceFlags::Unsegmented); + assert_eq!(sp_header.packet_id_raw(), 0x0807); + assert_eq!(sp_header.psc_raw(), 0xC016); + assert_eq!(sp_header.data_len(), 36); + assert_eq!(sp_header.ccsds_version(), 0b000); + + let from_comp_fields = SpHeader::from_composite_fields( + PacketId::new(PacketType::Tc, true, 0x42).unwrap(), + PacketSequenceCtrl::new(SequenceFlags::Unsegmented, 0x7).unwrap(), + 0, + None, + ); + assert_eq!(from_comp_fields.ptype(), PacketType::Tc); + assert_eq!(from_comp_fields.apid(), 0x42); + assert!(from_comp_fields.sec_header_flag()); + assert_eq!( + from_comp_fields.sequence_flags(), + SequenceFlags::Unsegmented + ); + assert_eq!(from_comp_fields.ssc(), 0x7); + assert_eq!(from_comp_fields.data_len(), 0); + } + + #[test] + fn test_zc_sph() { + use zerocopy::AsBytes; + + let sp_header = + SpHeader::tc(0x7FF, num::pow(2, 14) - 1, 0).expect("Error creating SP header"); + assert_eq!(sp_header.ptype(), PacketType::Tc); + assert_eq!(sp_header.apid(), 0x7FF); + assert_eq!(sp_header.data_len(), 0); + assert_eq!(sp_header.ccsds_version(), 0b000); + assert!(sp_header.is_tc()); + let sp_header_zc = zc::SpHeader::from(sp_header); + let slice = sp_header_zc.as_bytes(); + assert_eq!(slice.len(), 6); + assert_eq!(slice[0], 0x1F); + assert_eq!(slice[1], 0xFF); + assert_eq!(slice[2], 0xFF); + assert_eq!(slice[3], 0xFF); + assert_eq!(slice[4], 0x00); + assert_eq!(slice[5], 0x00); + + let mut slice = [0; 6]; + sp_header_zc.write_to(slice.as_mut_slice()); + assert_eq!(slice.len(), 6); + assert_eq!(slice[0], 0x1F); + assert_eq!(slice[1], 0xFF); + assert_eq!(slice[2], 0xFF); + assert_eq!(slice[3], 0xFF); + assert_eq!(slice[4], 0x00); + assert_eq!(slice[5], 0x00); + + let mut test_vec = vec![0_u8; 6]; + let slice = test_vec.as_mut_slice(); + sp_header_zc.write_to(slice); + let slice = test_vec.as_slice(); + assert_eq!(slice.len(), 6); + assert_eq!(slice[0], 0x1F); + assert_eq!(slice[1], 0xFF); + assert_eq!(slice[2], 0xFF); + assert_eq!(slice[3], 0xFF); + assert_eq!(slice[4], 0x00); + assert_eq!(slice[5], 0x00); + + let sp_header = zc::SpHeader::from_bytes(slice); + assert!(sp_header.is_some()); + let sp_header = sp_header.unwrap(); + assert_eq!(sp_header.ccsds_version(), 0b000); + assert_eq!(sp_header.packet_id_raw(), 0x1FFF); + assert_eq!(sp_header.apid(), 0x7FF); + assert_eq!(sp_header.ptype(), PacketType::Tc); + assert_eq!(sp_header.data_len(), 0); + } +} diff --git a/src/tc.rs b/src/tc.rs new file mode 100644 index 0000000..155e591 --- /dev/null +++ b/src/tc.rs @@ -0,0 +1,476 @@ +use crate::ecss::PusVersion; +use crate::CCSDS_HEADER_LEN; +use core::mem::size_of; + +type CrcType = u16; + +/// PUS C secondary header length is fixed +pub const PUC_TC_SECONDARY_HEADER_LEN: usize = size_of::(); +pub const PUS_TC_MIN_LEN_WITHOUT_APP_DATA: usize = + CCSDS_HEADER_LEN + PUC_TC_SECONDARY_HEADER_LEN + size_of::(); +const PUS_VERSION: PusVersion = PusVersion::PusC; + +#[derive(Copy, Clone, PartialEq, Debug)] +enum AckOpts { + Acceptance = 0b1000, + Start = 0b0100, + Progress = 0b0010, + Completion = 0b0001, +} + +pub const ACK_ALL: u8 = AckOpts::Acceptance as u8 + | AckOpts::Start as u8 + | AckOpts::Progress as u8 + | AckOpts::Completion as u8; + +pub trait PusTcSecondaryHeader { + fn ack_flags(&self) -> u8; + fn service(&self) -> u8; + fn subservice(&self) -> u8; + fn source_id(&self) -> u16; +} + +pub mod zc { + use crate::ecss::{PusError, PusVersion}; + use crate::tc::{ser, PusTcSecondaryHeader}; + use zerocopy::{AsBytes, FromBytes, NetworkEndian, Unaligned, U16}; + + #[derive(FromBytes, AsBytes, Unaligned)] + #[repr(C)] + pub struct PusTcDataFieldHeader { + version_ack: u8, + service: u8, + subservice: u8, + source_id: U16, + } + + impl TryFrom for PusTcDataFieldHeader { + type Error = PusError; + fn try_from(value: ser::PusTcDataFieldHeader) -> Result { + if value.version != PusVersion::PusC { + return Err(PusError::VersionNotSupported(value.version)); + } + Ok(PusTcDataFieldHeader { + version_ack: ((value.version as u8) << 4) | value.ack, + service: value.service, + subservice: value.subservice, + source_id: U16::from(value.source_id), + }) + } + } + + impl PusTcSecondaryHeader for PusTcDataFieldHeader { + fn ack_flags(&self) -> u8 { + self.version_ack & 0b1111 + } + + fn service(&self) -> u8 { + self.service + } + + fn subservice(&self) -> u8 { + self.subservice + } + + fn source_id(&self) -> u16 { + self.source_id.get() + } + } + + impl PusTcDataFieldHeader { + pub fn to_bytes(&self, slice: &mut (impl AsMut<[u8]> + ?Sized)) -> Option<()> { + self.write_to(slice.as_mut()) + } + + pub fn from_bytes(slice: &(impl AsRef<[u8]> + ?Sized)) -> Option { + Self::read_from(slice.as_ref()) + } + } +} + +pub mod ser { + use crate::ecss::{PusError, PusPacket, PusVersion, CRC_CCITT_FALSE}; + use crate::ser::SpHeader; + use crate::tc::{ + PusTcSecondaryHeader, ACK_ALL, PUC_TC_SECONDARY_HEADER_LEN, + PUS_TC_MIN_LEN_WITHOUT_APP_DATA, PUS_VERSION, + }; + use crate::{zc, CcsdsPacket, PacketError, PacketId, PacketSequenceCtrl, PacketType}; + use alloc::vec::Vec; + use core::mem::size_of; + use delegate::delegate; + use serde::{Deserialize, Serialize}; + use zerocopy::AsBytes; + + #[derive(PartialEq, Copy, Clone, Serialize, Deserialize)] + pub struct PusTcDataFieldHeader { + pub service: u8, + pub subservice: u8, + pub source_id: u16, + pub ack: u8, + pub version: PusVersion, + } + + impl PusTcSecondaryHeader for PusTcDataFieldHeader { + fn ack_flags(&self) -> u8 { + self.ack + } + + fn service(&self) -> u8 { + self.service + } + + fn subservice(&self) -> u8 { + self.subservice + } + + fn source_id(&self) -> u16 { + self.source_id + } + } + impl TryFrom for PusTcDataFieldHeader { + type Error = (); + + fn try_from(value: super::zc::PusTcDataFieldHeader) -> Result { + Ok(PusTcDataFieldHeader { + service: value.service(), + subservice: value.subservice(), + source_id: value.source_id(), + ack: value.ack_flags(), + version: PUS_VERSION, + }) + } + } + + impl PusTcDataFieldHeader { + pub fn new(service: u8, subservice: u8, ack: u8) -> Self { + PusTcDataFieldHeader { + service, + subservice, + ack: ack & 0b1111, + source_id: 0, + version: PusVersion::PusC, + } + } + } + + #[derive(PartialEq, Copy, Clone, Serialize, Deserialize)] + pub struct PusTc<'slice> { + pub sph: SpHeader, + pub data_field_header: PusTcDataFieldHeader, + #[serde(skip)] + raw_data: Option<&'slice [u8]>, + app_data: Option<&'slice [u8]>, + crc16: Option, + } + + impl<'slice> PusTc<'slice> { + pub fn new( + sph: &mut SpHeader, + service: u8, + subservice: u8, + app_data: Option<&'slice [u8]>, + ) -> Self { + sph.packet_id.ptype = PacketType::Tc; + PusTc { + sph: *sph, + raw_data: None, + app_data, + data_field_header: PusTcDataFieldHeader::new(service, subservice, ACK_ALL), + crc16: None, + } + } + + pub fn len_packed(&self) -> usize { + let mut length = super::PUS_TC_MIN_LEN_WITHOUT_APP_DATA; + if let Some(app_data) = self.app_data { + length += app_data.len(); + } + length + } + + /// Calculate the CCSDS space packet data length field and sets it + pub fn set_ccsds_data_len(&mut self) { + self.sph.data_len = + self.len_packed() as u16 - size_of::() as u16 - 1; + } + + fn crc_from_raw_data(&self) -> Result { + if let Some(raw_data) = self.raw_data { + if raw_data.len() < 2 { + return Err(PusError::RawDataTooShort(raw_data.len())); + } + return Ok(u16::from_be_bytes( + raw_data[raw_data.len() - 2..raw_data.len()] + .try_into() + .unwrap(), + )); + } + Err(PusError::NoRawData) + } + + pub fn calc_crc16(&mut self) { + let mut digest = CRC_CCITT_FALSE.digest(); + let sph_zc = crate::zc::SpHeader::from(self.sph); + digest.update(sph_zc.as_bytes()); + let pus_tc_header = + super::zc::PusTcDataFieldHeader::try_from(self.data_field_header).unwrap(); + digest.update(pus_tc_header.as_bytes()); + if let Some(app_data) = self.app_data { + digest.update(app_data); + } + self.crc16 = Some(digest.finalize()) + } + + /// This function updates two important internal fields: The CCSDS packet length in the + /// space packet header and the CRC16 field. This function should be called before + /// the TC packet is serialized + pub fn update_packet_fields(&mut self) { + self.set_ccsds_data_len(); + self.calc_crc16(); + } + + pub fn copy_to_buf( + &self, + slice: &mut (impl AsMut<[u8]> + ?Sized), + ) -> Result { + if self.crc16.is_none() { + return Err(PusError::CrcCalculationMissing); + } + let mut_slice = slice.as_mut(); + let mut curr_idx = 0; + let sph_zc = crate::zc::SpHeader::from(self.sph); + let tc_header_len = size_of::(); + let mut total_size = super::PUS_TC_MIN_LEN_WITHOUT_APP_DATA; + if let Some(app_data) = self.app_data { + total_size += app_data.len(); + }; + if total_size > mut_slice.len() { + return Err(PusError::OtherPacketError( + PacketError::ToBytesSliceTooSmall(total_size), + )); + } + sph_zc + .to_bytes(&mut mut_slice[curr_idx..curr_idx + 6]) + .ok_or(PusError::OtherPacketError( + PacketError::ToBytesZeroCopyError, + ))?; + curr_idx += 6; + // The PUS version is hardcoded to PUS C + let pus_tc_header = + super::zc::PusTcDataFieldHeader::try_from(self.data_field_header).unwrap(); + + pus_tc_header + .to_bytes(&mut mut_slice[curr_idx..curr_idx + tc_header_len]) + .ok_or(PusError::OtherPacketError( + PacketError::ToBytesZeroCopyError, + ))?; + curr_idx += tc_header_len; + if let Some(app_data) = self.app_data { + mut_slice[curr_idx..curr_idx + app_data.len()].copy_from_slice(app_data); + curr_idx += app_data.len(); + } + mut_slice[curr_idx..curr_idx + 2] + .copy_from_slice(self.crc16.unwrap().to_be_bytes().as_slice()); + curr_idx += 2; + Ok(curr_idx) + } + + pub fn append_to_vec(&self, vec: &mut Vec) -> Result { + if self.crc16.is_none() { + return Err(PusError::CrcCalculationMissing); + } + let sph_zc = crate::zc::SpHeader::from(self.sph); + let mut appended_len = super::PUS_TC_MIN_LEN_WITHOUT_APP_DATA; + if let Some(app_data) = self.app_data { + appended_len += app_data.len(); + }; + vec.extend_from_slice(sph_zc.as_bytes()); + // The PUS version is hardcoded to PUS C + let pus_tc_header = + super::zc::PusTcDataFieldHeader::try_from(self.data_field_header).unwrap(); + vec.extend_from_slice(pus_tc_header.as_bytes()); + if let Some(app_data) = self.app_data { + vec.extend_from_slice(app_data); + } + vec.extend_from_slice(self.crc16.unwrap().to_be_bytes().as_slice()); + Ok(appended_len) + } + + pub fn new_from_raw_slice( + slice: &'slice (impl AsRef<[u8]> + ?Sized), + ) -> Result<(Self, usize), PusError> { + let slice_ref = slice.as_ref(); + let raw_data_len = slice_ref.len(); + if raw_data_len < PUS_TC_MIN_LEN_WITHOUT_APP_DATA { + return Err(PusError::RawDataTooShort(raw_data_len)); + } + let mut current_idx = 0; + let sph = zc::SpHeader::from_bytes(&slice_ref[current_idx..current_idx + 6]).ok_or( + PusError::OtherPacketError(PacketError::FromBytesZeroCopyError), + )?; + current_idx += 6; + let total_len = sph.total_len(); + if raw_data_len < total_len || total_len < PUS_TC_MIN_LEN_WITHOUT_APP_DATA { + return Err(PusError::RawDataTooShort(raw_data_len)); + } + let sec_header = crate::tc::zc::PusTcDataFieldHeader::from_bytes( + &slice_ref[current_idx..current_idx + PUC_TC_SECONDARY_HEADER_LEN], + ) + .ok_or(PusError::OtherPacketError( + PacketError::FromBytesZeroCopyError, + ))?; + current_idx += PUC_TC_SECONDARY_HEADER_LEN; + let mut pus_tc = PusTc { + sph: SpHeader::from(sph), + data_field_header: PusTcDataFieldHeader::try_from(sec_header).unwrap(), + raw_data: Some(slice_ref), + app_data: match current_idx { + _ if current_idx == total_len - 2 => None, + _ if current_idx > total_len - 2 => { + return Err(PusError::RawDataTooShort(raw_data_len)) + } + _ => Some(&slice_ref[current_idx..total_len - 2]), + }, + crc16: None, + }; + pus_tc.crc_from_raw_data()?; + pus_tc.verify()?; + Ok((pus_tc, total_len)) + } + + fn verify(&mut self) -> Result<(), PusError> { + let mut digest = CRC_CCITT_FALSE.digest(); + if self.raw_data.is_none() { + return Err(PusError::NoRawData); + } + let raw_data = self.raw_data.unwrap(); + digest.update(raw_data.as_ref()); + if digest.finalize() == 0 { + return Ok(()); + } + let crc16 = self.crc_from_raw_data()?; + Err(PusError::IncorrectCrc(crc16)) + } + } + + //noinspection RsTraitImplementation + impl CcsdsPacket for PusTc<'_> { + delegate!(to self.sph { + fn ccsds_version(&self) -> u8; + fn packet_id(&self) -> PacketId; + fn psc(&self) -> PacketSequenceCtrl; + fn data_len(&self) -> u16; + }); + } + + //noinspection RsTraitImplementation + impl PusPacket for PusTc<'_> { + delegate!(to self.data_field_header { + fn service(&self) -> u8; + fn subservice(&self) -> u8; + }); + + fn user_data(&self) -> Option<&[u8]> { + self.app_data + } + + fn crc16(&self) -> Option { + self.crc16 + } + } + + //noinspection RsTraitImplementation + impl PusTcSecondaryHeader for PusTc<'_> { + delegate!(to self.data_field_header { + fn service(&self) -> u8; + fn subservice(&self) -> u8; + fn source_id(&self) -> u16; + fn ack_flags(&self) -> u8; + }); + } +} + +#[cfg(test)] +mod tests { + use crate::ecss::PusPacket; + use crate::ser::SpHeader; + use crate::tc::ser::PusTc; + use crate::tc::PusTcSecondaryHeader; + use crate::tc::ACK_ALL; + use crate::{CcsdsPacket, PacketType}; + use alloc::vec::Vec; + use postcard::to_stdvec; + + #[test] + fn test_tc() { + let mut sph = SpHeader::tc(0x01, 0, 0).unwrap(); + let mut pus_tc = PusTc::new(&mut sph, 17, 1, None); + let _out = to_stdvec(&pus_tc).unwrap(); + assert_eq!(pus_tc.crc16(), None); + let mut test_buf: [u8; 32] = [0; 32]; + pus_tc.update_packet_fields(); + verify_test_tc(&pus_tc); + assert_eq!(pus_tc.len_packed(), 13); + let size = pus_tc + .copy_to_buf(test_buf.as_mut_slice()) + .expect("Error writing TC to buffer"); + assert_eq!(size, 13); + let (tc_from_raw, mut size) = PusTc::new_from_raw_slice(&test_buf) + .expect("Creating PUS TC struct from raw buffer failed"); + assert_eq!(size, 13); + verify_test_tc(&tc_from_raw); + verify_test_tc_raw(PacketType::Tm, &test_buf); + + let mut test_vec = Vec::new(); + size = pus_tc + .append_to_vec(&mut test_vec) + .expect("Error writing TC to vector"); + assert_eq!(size, 13); + assert_eq!(&test_buf[0..pus_tc.len_packed()], test_vec.as_slice()); + verify_test_tc_raw(PacketType::Tm, &test_vec.as_slice()); + } + + fn verify_test_tc(tc: &PusTc) { + assert_eq!(PusPacket::service(tc), 17); + assert_eq!(PusPacket::subservice(tc), 1); + assert_eq!(tc.user_data(), None); + assert_eq!(tc.source_id(), 0); + assert_eq!(tc.apid(), 0x01); + assert_eq!(tc.ack_flags(), ACK_ALL); + assert_eq!(tc.sph, SpHeader::tc(0x01, 0, 6).unwrap()); + } + + fn verify_test_tc_raw(ptype: PacketType, slice: &impl AsRef<[u8]>) { + // Reference comparison implementation: + // https://github.com/robamu-org/py-spacepackets/blob/main/examples/example_pus.py + let slice = slice.as_ref(); + // 0x1801 is the generic + if ptype == PacketType::Tm { + assert_eq!(slice[0], 0x18); + } else { + assert_eq!(slice[0], 0x08); + } + // APID is 0x01 + assert_eq!(slice[1], 0x01); + // Unsegmented packets + assert_eq!(slice[2], 0xc0); + // Sequence count 0 + assert_eq!(slice[3], 0x00); + assert_eq!(slice[4], 0x00); + // Space data length of 6 equals total packet length of 13 + assert_eq!(slice[5], 0x06); + // PUS Version C 0b0010 and ACK flags 0b1111 + assert_eq!(slice[6], 0x2f); + // Service 17 + assert_eq!(slice[7], 0x11); + // Subservice 1 + assert_eq!(slice[8], 0x01); + // Source ID 0 + assert_eq!(slice[9], 0x00); + assert_eq!(slice[10], 0x00); + // CRC first byte assuming big endian format is 0x16 and 0x1d + assert_eq!(slice[11], 0x16); + assert_eq!(slice[12], 0x1d); + } +} diff --git a/src/tm.rs b/src/tm.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/tm.rs @@ -0,0 +1 @@ +