Init spacepackets crate

This commit is contained in:
Robin Müller 2022-06-18 22:48:51 +02:00
commit 396cea8445
No known key found for this signature in database
GPG Key ID: 71B58F8A3CDFA9AC
8 changed files with 1405 additions and 0 deletions

34
Cargo.toml Normal file
View File

@ -0,0 +1,34 @@
[package]
name = "spacepackets"
version = "0.1.0"
edition = "2021"
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
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"]

201
LICENSE-APACHE Normal file
View File

@ -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.

3
NOTICE Normal file
View File

@ -0,0 +1,3 @@
Generic implementations for various CCSDS and ECSS packet standards.
This software contains code developed at the University of Stuttgart.

67
examples/test.rs Normal file
View File

@ -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<NetworkEndian>,
some_i32: I32<NetworkEndian>,
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);
}
}

37
src/ecss.rs Normal file
View File

@ -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<u16> = Crc::<u16>::new(&CRC_16_IBM_3740);
pub const CCSDS_HEADER_LEN: usize = size_of::<crate::zc::SpHeader>();
/// 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<u16>;
}

586
src/lib.rs Normal file
View File

@ -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<u8> for PacketType {
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error> {
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<u8> for SequenceFlags {
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error> {
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<PacketId> {
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<u16> 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<PacketSequenceCtrl> {
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<u16> 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<u8>,
) -> 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<Self> {
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> {
Self::new(apid, PacketType::Tm, ssc, data_len)
}
pub fn tc(apid: u16, ssc: u16, data_len: u16) -> Option<Self> {
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<u8>,
) -> 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<NetworkEndian>,
psc: U16<NetworkEndian>,
data_len: U16<NetworkEndian>,
}
impl SpHeader {
pub fn new(
packet_id: PacketId,
psc: PacketSequenceCtrl,
data_len: u16,
version: Option<u8>,
) -> 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<Self> {
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<u8>,
) -> 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);
}
}

476
src/tc.rs Normal file
View File

@ -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::<zc::PusTcDataFieldHeader>();
pub const PUS_TC_MIN_LEN_WITHOUT_APP_DATA: usize =
CCSDS_HEADER_LEN + PUC_TC_SECONDARY_HEADER_LEN + size_of::<CrcType>();
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<NetworkEndian>,
}
impl TryFrom<ser::PusTcDataFieldHeader> for PusTcDataFieldHeader {
type Error = PusError;
fn try_from(value: ser::PusTcDataFieldHeader) -> Result<Self, Self::Error> {
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> {
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<super::zc::PusTcDataFieldHeader> for PusTcDataFieldHeader {
type Error = ();
fn try_from(value: super::zc::PusTcDataFieldHeader) -> Result<Self, Self::Error> {
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<u16>,
}
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::<crate::zc::SpHeader>() as u16 - 1;
}
fn crc_from_raw_data(&self) -> Result<u16, PusError> {
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<usize, PusError> {
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::<super::zc::PusTcDataFieldHeader>();
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<u8>) -> Result<usize, PusError> {
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<u16> {
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);
}
}

1
src/tm.rs Normal file
View File

@ -0,0 +1 @@