Init spacepackets crate
This commit is contained in:
commit
396cea8445
34
Cargo.toml
Normal file
34
Cargo.toml
Normal 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
201
LICENSE-APACHE
Normal 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
3
NOTICE
Normal 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
67
examples/test.rs
Normal 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
37
src/ecss.rs
Normal 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
586
src/lib.rs
Normal 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
476
src/tc.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user