From 0e2a413505f5b86a0a3c8221ee020a1e2c0c4045 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 21 Jul 2023 20:23:24 +0200 Subject: [PATCH 01/45] start adding CFDP state machines --- satrs-core/src/cfdp/dest.rs | 13 +++++++++++++ satrs-core/src/cfdp/mod.rs | 25 +++++++++++++++++++++++++ satrs-core/src/lib.rs | 1 + 3 files changed, 39 insertions(+) create mode 100644 satrs-core/src/cfdp/dest.rs create mode 100644 satrs-core/src/cfdp/mod.rs diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs new file mode 100644 index 0000000..13e5cbe --- /dev/null +++ b/satrs-core/src/cfdp/dest.rs @@ -0,0 +1,13 @@ +use super::{TransactionStep, State}; + +pub struct DestinationHandler { + step: TransactionStep, + state: State +} + +impl DestinationHandler { + + pub fn state_machine() {} + + +} diff --git a/satrs-core/src/cfdp/mod.rs b/satrs-core/src/cfdp/mod.rs new file mode 100644 index 0000000..f62bd14 --- /dev/null +++ b/satrs-core/src/cfdp/mod.rs @@ -0,0 +1,25 @@ +pub mod dest; + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum TransactionStep { + Idle = 0, + TransactionStart = 1, + ReceivingFileDataPdus = 2, + SendingAckPdu = 3, + TransferCompletion = 4, + SendingFinishedPdu = 5 +} + +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum State { + Idle = 0, + BusyClass1Nacked = 2, + BusyClass2Acked = 3, +} + +#[cfg(test)] +mod tests { + #[test] + fn basic_test() { + } +} diff --git a/satrs-core/src/lib.rs b/satrs-core/src/lib.rs index 43c8cfb..6484cc8 100644 --- a/satrs-core/src/lib.rs +++ b/satrs-core/src/lib.rs @@ -40,6 +40,7 @@ pub mod request; pub mod res_code; pub mod seq_count; pub mod tmtc; +pub mod cfdp; pub use spacepackets; From beebf0056554b6055f1988affa1d6d4b474763e4 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 21 Jul 2023 21:36:21 +0200 Subject: [PATCH 02/45] continue --- satrs-core/src/cfdp/dest.rs | 24 ++++++++++++++++++------ satrs-core/src/cfdp/mod.rs | 5 ++--- satrs-core/src/lib.rs | 2 +- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index 13e5cbe..09e21fa 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -1,13 +1,25 @@ -use super::{TransactionStep, State}; +use spacepackets::cfdp::{pdu::FileDirectiveType, PduType}; + +use super::{State, TransactionStep}; pub struct DestinationHandler { step: TransactionStep, - state: State + state: State, } impl DestinationHandler { - - pub fn state_machine() {} - - + pub fn insert_packet( + &mut self, + pdu_type: PduType, + pdu_directive: Option, + raw_packet: &[u8], + ) { + } + pub fn state_machine(&mut self) { + match self.state { + State::Idle => todo!(), + State::BusyClass1Nacked => todo!(), + State::BusyClass2Acked => todo!(), + } + } } diff --git a/satrs-core/src/cfdp/mod.rs b/satrs-core/src/cfdp/mod.rs index f62bd14..811c820 100644 --- a/satrs-core/src/cfdp/mod.rs +++ b/satrs-core/src/cfdp/mod.rs @@ -7,7 +7,7 @@ pub enum TransactionStep { ReceivingFileDataPdus = 2, SendingAckPdu = 3, TransferCompletion = 4, - SendingFinishedPdu = 5 + SendingFinishedPdu = 5, } #[derive(Copy, Clone, PartialEq, Eq)] @@ -20,6 +20,5 @@ pub enum State { #[cfg(test)] mod tests { #[test] - fn basic_test() { - } + fn basic_test() {} } diff --git a/satrs-core/src/lib.rs b/satrs-core/src/lib.rs index 6484cc8..949ffa5 100644 --- a/satrs-core/src/lib.rs +++ b/satrs-core/src/lib.rs @@ -20,6 +20,7 @@ extern crate downcast_rs; #[cfg(any(feature = "std", test))] extern crate std; +pub mod cfdp; pub mod error; #[cfg(feature = "alloc")] #[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] @@ -40,7 +41,6 @@ pub mod request; pub mod res_code; pub mod seq_count; pub mod tmtc; -pub mod cfdp; pub use spacepackets; From 2213a255088bf945d4a718716a2f500901257ee8 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 22 Jul 2023 18:46:12 +0200 Subject: [PATCH 03/45] how to best do this.. --- satrs-core/src/cfdp/dest.rs | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index 09e21fa..026ed95 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -1,6 +1,5 @@ -use spacepackets::cfdp::{pdu::FileDirectiveType, PduType}; - use super::{State, TransactionStep}; +use spacepackets::cfdp::{pdu::FileDirectiveType, PduType}; pub struct DestinationHandler { step: TransactionStep, @@ -13,8 +12,29 @@ impl DestinationHandler { pdu_type: PduType, pdu_directive: Option, raw_packet: &[u8], - ) { + ) -> Result<(), ()> { + match pdu_type { + PduType::FileDirective => { + if pdu_directive.is_none() { + return Err(()); + } + self.handle_file_directive(pdu_directive.unwrap(), raw_packet) + } + PduType::FileData => self.handle_file_data(raw_packet), + } } + + pub fn handle_file_data(&mut self, raw_packet: &[u8]) -> Result<(), ()> { + Ok(()) + } + pub fn handle_file_directive( + &mut self, + pdu_directive: FileDirectiveType, + raw_packet: &[u8], + ) -> Result<(), ()> { + Ok(()) + } + pub fn state_machine(&mut self) { match self.state { State::Idle => todo!(), From 6c87ae0b67b6c9471682f9226226824255121324 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Tue, 25 Jul 2023 00:43:45 +0200 Subject: [PATCH 04/45] continue dest handler --- satrs-core/src/cfdp/dest.rs | 41 +++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index 026ed95..a58a2e3 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -1,12 +1,24 @@ use super::{State, TransactionStep}; -use spacepackets::cfdp::{pdu::FileDirectiveType, PduType}; +use spacepackets::cfdp::{ + pdu::{CommonPduConfig, FileDirectiveType}, + PduType, +}; pub struct DestinationHandler { step: TransactionStep, state: State, + //pdu_conf: CommonPduConfig, } impl DestinationHandler { + pub fn new() -> Self { + Self { + step: TransactionStep::Idle, + state: State::Idle, + //pdu_conf: CommonPduConfig::new_with_defaults(), + } + } + pub fn insert_packet( &mut self, pdu_type: PduType, @@ -27,6 +39,7 @@ impl DestinationHandler { pub fn handle_file_data(&mut self, raw_packet: &[u8]) -> Result<(), ()> { Ok(()) } + pub fn handle_file_directive( &mut self, pdu_directive: FileDirectiveType, @@ -38,8 +51,32 @@ impl DestinationHandler { pub fn state_machine(&mut self) { match self.state { State::Idle => todo!(), - State::BusyClass1Nacked => todo!(), + State::BusyClass1Nacked => self.fsm_nacked(), State::BusyClass2Acked => todo!(), } } + + fn fsm_nacked(&self) { + match self.step { + TransactionStep::Idle => { + // TODO: Should not happen. Determine what to do later + } + TransactionStep::TransactionStart => {} + TransactionStep::ReceivingFileDataPdus => todo!(), + TransactionStep::SendingAckPdu => todo!(), + TransactionStep::TransferCompletion => todo!(), + TransactionStep::SendingFinishedPdu => todo!(), + } + } + + /// Get the step, which denotes the exact step of a pending CFDP transaction when applicable. + pub fn step(&self) -> TransactionStep { + self.step + } + + /// Get the step, which denotes whether the CFDP handler is active, and which CFDP class + /// is used if it is active. + pub fn state(&self) -> State { + self.state + } } From 9bbd2cdad1ef756f23f1e0cc3d0afef91dcc2a1c Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 26 Jul 2023 22:27:02 +0200 Subject: [PATCH 05/45] continue dest handler --- satrs-core/Cargo.toml | 2 +- satrs-core/src/cfdp/dest.rs | 64 +++++++++++++++++++++++++++++++++---- satrs-core/src/cfdp/mod.rs | 4 +-- 3 files changed, 60 insertions(+), 10 deletions(-) diff --git a/satrs-core/Cargo.toml b/satrs-core/Cargo.toml index 29d4122..f342f95 100644 --- a/satrs-core/Cargo.toml +++ b/satrs-core/Cargo.toml @@ -64,7 +64,7 @@ optional = true # version = "0.6" # path = "../spacepackets" git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git" -rev = "62df510147b" +rev = "041959e546e6e72b24eb50986c425a924015e3f4" default-features = false [dev-dependencies] diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index a58a2e3..d8848b6 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -1,13 +1,29 @@ use super::{State, TransactionStep}; use spacepackets::cfdp::{ - pdu::{CommonPduConfig, FileDirectiveType}, + pdu::{metadata::MetadataPdu, CommonPduConfig, FileDirectiveType, PduError}, PduType, }; pub struct DestinationHandler { step: TransactionStep, state: State, - //pdu_conf: CommonPduConfig, + pdu_conf: CommonPduConfig, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum DestError { + /// File directive expected, but none specified + DirectiveExpected, + CantProcessPacketType(FileDirectiveType), + // Received new metadata PDU while being already being busy with a file transfer. + RecvdMetadataButIsBusy, + Pdu(PduError), +} + +impl From for DestError { + fn from(value: PduError) -> Self { + Self::Pdu(value) + } } impl DestinationHandler { @@ -15,7 +31,7 @@ impl DestinationHandler { Self { step: TransactionStep::Idle, state: State::Idle, - //pdu_conf: CommonPduConfig::new_with_defaults(), + pdu_conf: CommonPduConfig::new_with_defaults(), } } @@ -24,11 +40,11 @@ impl DestinationHandler { pdu_type: PduType, pdu_directive: Option, raw_packet: &[u8], - ) -> Result<(), ()> { + ) -> Result<(), DestError> { match pdu_type { PduType::FileDirective => { if pdu_directive.is_none() { - return Err(()); + return Err(DestError::DirectiveExpected); } self.handle_file_directive(pdu_directive.unwrap(), raw_packet) } @@ -36,7 +52,7 @@ impl DestinationHandler { } } - pub fn handle_file_data(&mut self, raw_packet: &[u8]) -> Result<(), ()> { + pub fn handle_file_data(&mut self, raw_packet: &[u8]) -> Result<(), DestError> { Ok(()) } @@ -44,7 +60,16 @@ impl DestinationHandler { &mut self, pdu_directive: FileDirectiveType, raw_packet: &[u8], - ) -> Result<(), ()> { + ) -> Result<(), DestError> { + match pdu_directive { + FileDirectiveType::EofPdu => todo!(), + FileDirectiveType::FinishedPdu => todo!(), + FileDirectiveType::AckPdu => todo!(), + FileDirectiveType::MetadataPdu => self.handle_metadata_pdu(raw_packet), + FileDirectiveType::NakPdu => todo!(), + FileDirectiveType::PromptPdu => todo!(), + FileDirectiveType::KeepAlivePdu => todo!(), + }; Ok(()) } @@ -56,6 +81,19 @@ impl DestinationHandler { } } + pub fn handle_metadata_pdu(&mut self, raw_packet: &[u8]) -> Result<(), DestError> { + if self.state != State::Idle { + return Err(DestError::RecvdMetadataButIsBusy); + } + let metadata_pdu = MetadataPdu::from_bytes(raw_packet)?; + + Ok(()) + } + + pub fn handle_eof_pdu(&mut self, raw_packet: &[u8]) -> Result<(), DestError> { + Ok(()) + } + fn fsm_nacked(&self) { match self.step { TransactionStep::Idle => { @@ -80,3 +118,15 @@ impl DestinationHandler { self.state } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic() { + let dest_handler = DestinationHandler::new(); + assert_eq!(dest_handler.state(), State::Idle); + assert_eq!(dest_handler.step(), TransactionStep::Idle); + } +} diff --git a/satrs-core/src/cfdp/mod.rs b/satrs-core/src/cfdp/mod.rs index 811c820..1cad510 100644 --- a/satrs-core/src/cfdp/mod.rs +++ b/satrs-core/src/cfdp/mod.rs @@ -1,6 +1,6 @@ pub mod dest; -#[derive(Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum TransactionStep { Idle = 0, TransactionStart = 1, @@ -10,7 +10,7 @@ pub enum TransactionStep { SendingFinishedPdu = 5, } -#[derive(Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum State { Idle = 0, BusyClass1Nacked = 2, From c0e1cb8bcf8b3c2ae844be64b82490249cfbdee9 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 26 Jul 2023 22:33:54 +0200 Subject: [PATCH 06/45] continue dest handler --- satrs-core/Cargo.toml | 6 +++--- satrs-core/src/cfdp/dest.rs | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/satrs-core/Cargo.toml b/satrs-core/Cargo.toml index f342f95..8fb5198 100644 --- a/satrs-core/Cargo.toml +++ b/satrs-core/Cargo.toml @@ -62,9 +62,9 @@ optional = true [dependencies.spacepackets] # version = "0.6" -# path = "../spacepackets" -git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git" -rev = "041959e546e6e72b24eb50986c425a924015e3f4" +path = "../../spacepackets" +# git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git" +# rev = "041959e546e6e72b24eb50986c425a924015e3f4" default-features = false [dev-dependencies] diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index d8848b6..8f8cd85 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -86,6 +86,7 @@ impl DestinationHandler { return Err(DestError::RecvdMetadataButIsBusy); } let metadata_pdu = MetadataPdu::from_bytes(raw_packet)?; + let params = metadata_pdu.metadata_params(); Ok(()) } From 0ea0f90b25665c3e2c31d0ac4d8f743a988c1063 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 26 Jul 2023 23:33:54 +0200 Subject: [PATCH 07/45] add transaction params struct --- satrs-core/src/cfdp/dest.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index 8f8cd85..abaeb1d 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -1,6 +1,9 @@ use super::{State, TransactionStep}; use spacepackets::cfdp::{ - pdu::{metadata::MetadataPdu, CommonPduConfig, FileDirectiveType, PduError}, + pdu::{ + metadata::{MetadataGenericParams, MetadataPdu}, + CommonPduConfig, FileDirectiveType, PduError, + }, PduType, }; @@ -8,6 +11,12 @@ pub struct DestinationHandler { step: TransactionStep, state: State, pdu_conf: CommonPduConfig, + transaction_params: TransactionParams, +} + +#[derive(Default)] +struct TransactionParams { + metadata_params: MetadataGenericParams, } #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -32,6 +41,7 @@ impl DestinationHandler { step: TransactionStep::Idle, state: State::Idle, pdu_conf: CommonPduConfig::new_with_defaults(), + transaction_params: Default::default(), } } @@ -86,8 +96,7 @@ impl DestinationHandler { return Err(DestError::RecvdMetadataButIsBusy); } let metadata_pdu = MetadataPdu::from_bytes(raw_packet)?; - let params = metadata_pdu.metadata_params(); - + self.transaction_params.metadata_params = *metadata_pdu.metadata_params(); Ok(()) } From f5c0b0f6bb124c396ae7ce4133d8a8f524a0872c Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 26 Jul 2023 23:50:09 +0200 Subject: [PATCH 08/45] continue dest handler --- satrs-core/src/cfdp/dest.rs | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index abaeb1d..1444039 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -14,9 +14,24 @@ pub struct DestinationHandler { transaction_params: TransactionParams, } -#[derive(Default)] struct TransactionParams { metadata_params: MetadataGenericParams, + src_file_name: [u8; u8::MAX as usize], + src_file_name_len: usize, + dest_file_name: [u8; u8::MAX as usize], + dest_file_name_len: usize, +} + +impl Default for TransactionParams { + fn default() -> Self { + Self { + metadata_params: Default::default(), + src_file_name: [0; u8::MAX as usize], + src_file_name_len: Default::default(), + dest_file_name: [0; u8::MAX as usize], + dest_file_name_len: Default::default(), + } + } } #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -26,6 +41,8 @@ pub enum DestError { CantProcessPacketType(FileDirectiveType), // Received new metadata PDU while being already being busy with a file transfer. RecvdMetadataButIsBusy, + EmptySrcFileField, + EmptyDestFileField, Pdu(PduError), } @@ -97,6 +114,20 @@ impl DestinationHandler { } let metadata_pdu = MetadataPdu::from_bytes(raw_packet)?; self.transaction_params.metadata_params = *metadata_pdu.metadata_params(); + let src_name = metadata_pdu.src_file_name(); + if src_name.is_empty() { + return Err(DestError::EmptySrcFileField); + } + self.transaction_params.src_file_name[..src_name.len_value()] + .copy_from_slice(src_name.value().unwrap()); + self.transaction_params.src_file_name_len = src_name.len_value(); + let dest_name = metadata_pdu.dest_file_name(); + if dest_name.is_empty() { + return Err(DestError::EmptyDestFileField); + } + self.transaction_params.dest_file_name[..dest_name.len_value()] + .copy_from_slice(dest_name.value().unwrap()); + self.transaction_params.dest_file_name_len = dest_name.len_value(); Ok(()) } From 95a12957187c35a29b2f48a005514b7c081303cc Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 5 Aug 2023 18:51:50 +0200 Subject: [PATCH 09/45] continue destination state machine --- satrs-core/Cargo.toml | 7 +- satrs-core/src/cfdp/dest.rs | 213 +++++++++++++++++++++++++++++------- satrs-core/src/cfdp/mod.rs | 4 + 3 files changed, 183 insertions(+), 41 deletions(-) diff --git a/satrs-core/Cargo.toml b/satrs-core/Cargo.toml index 8fb5198..15ad34d 100644 --- a/satrs-core/Cargo.toml +++ b/satrs-core/Cargo.toml @@ -21,6 +21,10 @@ embed-doc-image = "0.1" version = "0.6" default-features = false +[dependencies.crc] +version = "3" +optional = true + [dependencies.dyn-clone] version = "1" optional = true @@ -87,7 +91,8 @@ std = [ "serde/std", "spacepackets/std", "num_enum/std", - "thiserror" + "thiserror", + "crc" ] alloc = [ "serde/alloc", diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index 1444039..4494c73 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -1,11 +1,22 @@ -use super::{State, TransactionStep}; +use core::str::{from_utf8, Utf8Error}; +use std::{ + fs::{metadata, File}, + io::{BufReader, Read, Seek, SeekFrom, Write}, + path::{Path, PathBuf}, +}; + +use super::{State, TransactionStep, CRC_32}; use spacepackets::cfdp::{ pdu::{ + eof::EofPdu, + file_data::FileDataPdu, + finished::DeliveryCode, metadata::{MetadataGenericParams, MetadataPdu}, CommonPduConfig, FileDirectiveType, PduError, }, - PduType, + ConditionCode, PduType, }; +use thiserror::Error; pub struct DestinationHandler { step: TransactionStep, @@ -20,6 +31,10 @@ struct TransactionParams { src_file_name_len: usize, dest_file_name: [u8; u8::MAX as usize], dest_file_name_len: usize, + dest_path_buf: PathBuf, + condition_code: ConditionCode, + delivery_code: DeliveryCode, + cksum_buf: [u8; 1024], } impl Default for TransactionParams { @@ -30,26 +45,45 @@ impl Default for TransactionParams { src_file_name_len: Default::default(), dest_file_name: [0; u8::MAX as usize], dest_file_name_len: Default::default(), + dest_path_buf: Default::default(), + condition_code: ConditionCode::NoError, + delivery_code: DeliveryCode::Incomplete, + cksum_buf: [0; 1024], } } } -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum DestError { - /// File directive expected, but none specified - DirectiveExpected, - CantProcessPacketType(FileDirectiveType), - // Received new metadata PDU while being already being busy with a file transfer. - RecvdMetadataButIsBusy, - EmptySrcFileField, - EmptyDestFileField, - Pdu(PduError), +impl TransactionParams { + fn reset(&mut self) { + self.condition_code = ConditionCode::NoError; + self.delivery_code = DeliveryCode::Incomplete; + } } -impl From for DestError { - fn from(value: PduError) -> Self { - Self::Pdu(value) - } +#[derive(Debug, Error)] +pub enum DestError { + /// File directive expected, but none specified + #[error("expected file directive")] + DirectiveExpected, + #[error("can not process packet type {0:?}")] + CantProcessPacketType(FileDirectiveType), + #[error("can not process file data PDUs in current state")] + WrongStateForFileDataAndEof, + // Received new metadata PDU while being already being busy with a file transfer. + #[error("busy with transfer")] + RecvdMetadataButIsBusy, + #[error("empty source file field")] + EmptySrcFileField, + #[error("empty dest file field")] + EmptyDestFileField, + #[error("pdu error {0}")] + Pdu(#[from] PduError), + #[error("io error {0}")] + Io(#[from] std::io::Error), + #[error("path conversion error {0}")] + PathConversion(#[from] Utf8Error), + #[error("error building dest path from source file name and dest folder")] + PathConcatError, } impl DestinationHandler { @@ -62,6 +96,14 @@ impl DestinationHandler { } } + pub fn state_machine(&mut self) -> Result<(), DestError> { + match self.state { + State::Idle => todo!(), + State::BusyClass1Nacked => self.fsm_nacked(), + State::BusyClass2Acked => todo!(), + } + } + pub fn insert_packet( &mut self, pdu_type: PduType, @@ -79,40 +121,35 @@ impl DestinationHandler { } } - pub fn handle_file_data(&mut self, raw_packet: &[u8]) -> Result<(), DestError> { - Ok(()) - } - pub fn handle_file_directive( &mut self, pdu_directive: FileDirectiveType, raw_packet: &[u8], ) -> Result<(), DestError> { match pdu_directive { - FileDirectiveType::EofPdu => todo!(), - FileDirectiveType::FinishedPdu => todo!(), - FileDirectiveType::AckPdu => todo!(), - FileDirectiveType::MetadataPdu => self.handle_metadata_pdu(raw_packet), - FileDirectiveType::NakPdu => todo!(), - FileDirectiveType::PromptPdu => todo!(), - FileDirectiveType::KeepAlivePdu => todo!(), + FileDirectiveType::EofPdu => self.handle_eof_pdu(raw_packet)?, + FileDirectiveType::FinishedPdu + | FileDirectiveType::NakPdu + | FileDirectiveType::KeepAlivePdu => { + return Err(DestError::CantProcessPacketType(pdu_directive)); + } + FileDirectiveType::AckPdu => { + todo!( + "check whether ACK pdu handling is applicable by checking the acked directive field" + ) + } + FileDirectiveType::MetadataPdu => self.handle_metadata_pdu(raw_packet)?, + FileDirectiveType::PromptPdu => self.handle_prompt_pdu(raw_packet)?, }; Ok(()) } - pub fn state_machine(&mut self) { - match self.state { - State::Idle => todo!(), - State::BusyClass1Nacked => self.fsm_nacked(), - State::BusyClass2Acked => todo!(), - } - } - pub fn handle_metadata_pdu(&mut self, raw_packet: &[u8]) -> Result<(), DestError> { if self.state != State::Idle { return Err(DestError::RecvdMetadataButIsBusy); } let metadata_pdu = MetadataPdu::from_bytes(raw_packet)?; + self.transaction_params.reset(); self.transaction_params.metadata_params = *metadata_pdu.metadata_params(); let src_name = metadata_pdu.src_file_name(); if src_name.is_empty() { @@ -131,21 +168,78 @@ impl DestinationHandler { Ok(()) } + pub fn handle_file_data(&mut self, raw_packet: &[u8]) -> Result<(), DestError> { + if self.state == State::Idle || self.step != TransactionStep::ReceivingFileDataPdus { + return Err(DestError::WrongStateForFileDataAndEof); + } + let fd_pdu = FileDataPdu::from_bytes(raw_packet)?; + let mut dest_file = File::options() + .write(true) + .open(&self.transaction_params.dest_path_buf)?; + dest_file.seek(SeekFrom::Start(fd_pdu.offset()))?; + dest_file.write_all(fd_pdu.file_data())?; + Ok(()) + } pub fn handle_eof_pdu(&mut self, raw_packet: &[u8]) -> Result<(), DestError> { + if self.state == State::Idle || self.step != TransactionStep::ReceivingFileDataPdus { + return Err(DestError::WrongStateForFileDataAndEof); + } + let eof_pdu = EofPdu::from_bytes(raw_packet)?; + let checksum = eof_pdu.file_checksum(); + self.checksum_check(checksum)?; + if self.state == State::BusyClass1Nacked { + self.step = TransactionStep::TransferCompletion; + } else { + self.step = TransactionStep::SendingAckPdu; + } Ok(()) } - fn fsm_nacked(&self) { + pub fn handle_prompt_pdu(&mut self, raw_packet: &[u8]) -> Result<(), DestError> { + todo!(); + Ok(()) + } + + fn checksum_check(&mut self, expected_checksum: u32) -> Result<(), DestError> { + let mut digest = CRC_32.digest(); + let file_to_check = File::open(&self.transaction_params.dest_path_buf)?; + let mut buf_reader = BufReader::new(file_to_check); + loop { + let bytes_read = buf_reader.read(&mut self.transaction_params.cksum_buf)?; + if bytes_read == 0 { + break; + } + digest.update(&self.transaction_params.cksum_buf[0..bytes_read]); + } + if digest.finalize() == expected_checksum { + self.transaction_params.condition_code = ConditionCode::NoError; + self.transaction_params.delivery_code = DeliveryCode::Complete; + } else { + self.transaction_params.condition_code = ConditionCode::FileChecksumFailure; + } + Ok(()) + } + + fn fsm_nacked(&mut self) -> Result<(), DestError> { match self.step { TransactionStep::Idle => { // TODO: Should not happen. Determine what to do later } - TransactionStep::TransactionStart => {} - TransactionStep::ReceivingFileDataPdus => todo!(), + TransactionStep::TransactionStart => { + self.transaction_start()?; + } + TransactionStep::ReceivingFileDataPdus => { + todo!("advance the fsm if everything is finished") + } + TransactionStep::TransferCompletion => { + self.transfer_completion()?; + } TransactionStep::SendingAckPdu => todo!(), - TransactionStep::TransferCompletion => todo!(), - TransactionStep::SendingFinishedPdu => todo!(), + TransactionStep::SendingFinishedPdu => { + self.send_finished_pdu()?; + } } + Ok(()) } /// Get the step, which denotes the exact step of a pending CFDP transaction when applicable. @@ -158,6 +252,45 @@ impl DestinationHandler { pub fn state(&self) -> State { self.state } + + fn transaction_start(&mut self) -> Result<(), DestError> { + let dest_path = Path::new(from_utf8( + &self.transaction_params.dest_file_name[..self.transaction_params.dest_file_name_len], + )?); + + self.transaction_params.dest_path_buf = dest_path.to_path_buf(); + + let metadata = metadata(dest_path)?; + if metadata.is_dir() { + // Create new destination path by concatenating the last part of the source source + // name and the destination folder. For example, for a source file of /tmp/hello.txt + // and a destination name of /home/test, the resulting file name should be + // /home/test/hello.txt + let source_path = Path::new(from_utf8( + &self.transaction_params.src_file_name[..self.transaction_params.src_file_name_len], + )?); + + let source_name = source_path.file_name(); + if source_name.is_none() { + return Err(DestError::PathConcatError); + } + let source_name = source_name.unwrap(); + self.transaction_params.dest_path_buf.push(source_name); + } + // This function does exactly what we require: Create a new file if it does not exist yet + // and trucate an existing one. + File::create(&self.transaction_params.dest_path_buf)?; + Ok(()) + } + + fn transfer_completion(&mut self) -> Result<(), DestError> { + todo!(); + Ok(()) + } + + fn send_finished_pdu(&mut self) -> Result<(), DestError> { + Ok(()) + } } #[cfg(test)] diff --git a/satrs-core/src/cfdp/mod.rs b/satrs-core/src/cfdp/mod.rs index 1cad510..2fba15e 100644 --- a/satrs-core/src/cfdp/mod.rs +++ b/satrs-core/src/cfdp/mod.rs @@ -1,3 +1,5 @@ +use crc::{Crc, CRC_32_CKSUM}; + pub mod dest; #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -17,6 +19,8 @@ pub enum State { BusyClass2Acked = 3, } +pub const CRC_32: Crc = Crc::::new(&CRC_32_CKSUM); + #[cfg(test)] mod tests { #[test] From 05391bbafe5de4ef97ef030ff595d1e2d670a36b Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Thu, 10 Aug 2023 20:24:47 +0200 Subject: [PATCH 10/45] changes for spacepackets update --- satrs-core/src/cfdp/dest.rs | 116 +++++++++++++++++++++++++++++------- 1 file changed, 96 insertions(+), 20 deletions(-) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index 4494c73..427aa5b 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -6,23 +6,35 @@ use std::{ }; use super::{State, TransactionStep, CRC_32}; -use spacepackets::cfdp::{ - pdu::{ - eof::EofPdu, - file_data::FileDataPdu, - finished::DeliveryCode, - metadata::{MetadataGenericParams, MetadataPdu}, - CommonPduConfig, FileDirectiveType, PduError, +use spacepackets::{ + cfdp::{ + pdu::{ + eof::EofPdu, + file_data::FileDataPdu, + finished::{DeliveryCode, FileStatus, FinishedPdu}, + metadata::{MetadataGenericParams, MetadataPdu}, + CommonPduConfig, FileDirectiveType, PduError, PduHeader, + }, + tlv::EntityIdTlv, + ConditionCode, PduType, }, - ConditionCode, PduType, + util::UnsignedByteField, }; use thiserror::Error; pub struct DestinationHandler { + id: UnsignedByteField, step: TransactionStep, state: State, pdu_conf: CommonPduConfig, transaction_params: TransactionParams, + packets_to_send_ctx: PacketsToSendContext, +} + +#[derive(Debug, Default)] +struct PacketsToSendContext { + packet_available: bool, + directive: Option, } struct TransactionParams { @@ -34,6 +46,7 @@ struct TransactionParams { dest_path_buf: PathBuf, condition_code: ConditionCode, delivery_code: DeliveryCode, + file_status: FileStatus, cksum_buf: [u8; 1024], } @@ -48,6 +61,7 @@ impl Default for TransactionParams { dest_path_buf: Default::default(), condition_code: ConditionCode::NoError, delivery_code: DeliveryCode::Incomplete, + file_status: FileStatus::Unreported, cksum_buf: [0; 1024], } } @@ -87,12 +101,14 @@ pub enum DestError { } impl DestinationHandler { - pub fn new() -> Self { + pub fn new(id: impl Into) -> Self { Self { + id: id.into(), step: TransactionStep::Idle, state: State::Idle, - pdu_conf: CommonPduConfig::new_with_defaults(), + pdu_conf: Default::default(), transaction_params: Default::default(), + packets_to_send_ctx: Default::default(), } } @@ -121,6 +137,55 @@ impl DestinationHandler { } } + pub fn packet_to_send_ready(&self) -> bool { + self.packets_to_send_ctx.packet_available + } + + pub fn get_next_packet_to_send( + &self, + buf: &mut [u8], + ) -> Result, DestError> { + if !self.packet_to_send_ready() { + return Ok(None); + } + let directive = self.packets_to_send_ctx.directive.unwrap(); + let mut writte_size = 0; + match directive { + FileDirectiveType::EofPdu => todo!(), + FileDirectiveType::FinishedPdu => { + let pdu_header = PduHeader::new_no_file_data(self.pdu_conf, 0); + let finished_pdu = if self.transaction_params.condition_code + == ConditionCode::NoError + || self.transaction_params.condition_code + == ConditionCode::UnsupportedChecksumType + { + FinishedPdu::new_default( + pdu_header, + self.transaction_params.delivery_code, + self.transaction_params.file_status, + ) + } else { + // TODO: Are there cases where this ID is actually the source entity ID? + let entity_id = EntityIdTlv::new(self.id); + FinishedPdu::new_with_error( + pdu_header, + self.transaction_params.condition_code, + self.transaction_params.delivery_code, + self.transaction_params.file_status, + entity_id, + ) + }; + writte_size = finished_pdu.write_to_bytes(buf)?; + } + FileDirectiveType::AckPdu => todo!(), + FileDirectiveType::MetadataPdu => todo!(), + FileDirectiveType::NakPdu => todo!(), + FileDirectiveType::PromptPdu => todo!(), + FileDirectiveType::KeepAlivePdu => todo!(), + } + Ok(Some((directive, writte_size))) + } + pub fn handle_file_directive( &mut self, pdu_directive: FileDirectiveType, @@ -180,13 +245,22 @@ impl DestinationHandler { dest_file.write_all(fd_pdu.file_data())?; Ok(()) } + pub fn handle_eof_pdu(&mut self, raw_packet: &[u8]) -> Result<(), DestError> { if self.state == State::Idle || self.step != TransactionStep::ReceivingFileDataPdus { return Err(DestError::WrongStateForFileDataAndEof); } let eof_pdu = EofPdu::from_bytes(raw_packet)?; let checksum = eof_pdu.file_checksum(); - self.checksum_check(checksum)?; + // For a standard disk based file system, which is assumed to be used for now, the file + // will always be retained. This might change in the future. + self.transaction_params.file_status = FileStatus::Retained; + if self.checksum_check(checksum)? { + self.transaction_params.condition_code = ConditionCode::NoError; + self.transaction_params.delivery_code = DeliveryCode::Complete; + } else { + self.transaction_params.condition_code = ConditionCode::FileChecksumFailure; + } if self.state == State::BusyClass1Nacked { self.step = TransactionStep::TransferCompletion; } else { @@ -200,7 +274,7 @@ impl DestinationHandler { Ok(()) } - fn checksum_check(&mut self, expected_checksum: u32) -> Result<(), DestError> { + fn checksum_check(&mut self, expected_checksum: u32) -> Result { let mut digest = CRC_32.digest(); let file_to_check = File::open(&self.transaction_params.dest_path_buf)?; let mut buf_reader = BufReader::new(file_to_check); @@ -212,12 +286,9 @@ impl DestinationHandler { digest.update(&self.transaction_params.cksum_buf[0..bytes_read]); } if digest.finalize() == expected_checksum { - self.transaction_params.condition_code = ConditionCode::NoError; - self.transaction_params.delivery_code = DeliveryCode::Complete; - } else { - self.transaction_params.condition_code = ConditionCode::FileChecksumFailure; + return Ok(true); } - Ok(()) + Ok(false) } fn fsm_nacked(&mut self) -> Result<(), DestError> { @@ -236,7 +307,7 @@ impl DestinationHandler { } TransactionStep::SendingAckPdu => todo!(), TransactionStep::SendingFinishedPdu => { - self.send_finished_pdu()?; + self.prepare_finished_pdu()?; } } Ok(()) @@ -288,18 +359,23 @@ impl DestinationHandler { Ok(()) } - fn send_finished_pdu(&mut self) -> Result<(), DestError> { + fn prepare_finished_pdu(&mut self) -> Result<(), DestError> { + self.packets_to_send_ctx.packet_available = true; + self.packets_to_send_ctx.directive = Some(FileDirectiveType::FinishedPdu); Ok(()) } } #[cfg(test)] mod tests { + use spacepackets::util::UnsignedByteFieldU8; + use super::*; #[test] fn test_basic() { - let dest_handler = DestinationHandler::new(); + let test_id = UnsignedByteFieldU8::new(1); + let dest_handler = DestinationHandler::new(test_id); assert_eq!(dest_handler.state(), State::Idle); assert_eq!(dest_handler.step(), TransactionStep::Idle); } From c8c18c54df005846ac49c1f3551a56df9128b382 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Thu, 10 Aug 2023 23:05:55 +0200 Subject: [PATCH 11/45] now its getting tricky again --- satrs-core/src/cfdp/dest.rs | 44 +++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index 427aa5b..8ff77d1 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -149,7 +149,7 @@ impl DestinationHandler { return Ok(None); } let directive = self.packets_to_send_ctx.directive.unwrap(); - let mut writte_size = 0; + let mut written_size = 0; match directive { FileDirectiveType::EofPdu => todo!(), FileDirectiveType::FinishedPdu => { @@ -175,7 +175,7 @@ impl DestinationHandler { entity_id, ) }; - writte_size = finished_pdu.write_to_bytes(buf)?; + written_size = finished_pdu.write_to_bytes(buf)?; } FileDirectiveType::AckPdu => todo!(), FileDirectiveType::MetadataPdu => todo!(), @@ -183,7 +183,7 @@ impl DestinationHandler { FileDirectiveType::PromptPdu => todo!(), FileDirectiveType::KeepAlivePdu => todo!(), } - Ok(Some((directive, writte_size))) + Ok(Some((directive, written_size))) } pub fn handle_file_directive( @@ -292,23 +292,21 @@ impl DestinationHandler { } fn fsm_nacked(&mut self) -> Result<(), DestError> { - match self.step { - TransactionStep::Idle => { - // TODO: Should not happen. Determine what to do later - } - TransactionStep::TransactionStart => { - self.transaction_start()?; - } - TransactionStep::ReceivingFileDataPdus => { - todo!("advance the fsm if everything is finished") - } - TransactionStep::TransferCompletion => { - self.transfer_completion()?; - } - TransactionStep::SendingAckPdu => todo!(), - TransactionStep::SendingFinishedPdu => { - self.prepare_finished_pdu()?; - } + if self.step == TransactionStep::Idle {} + if self.step == TransactionStep::TransactionStart { + self.transaction_start()?; + } + if self.step == TransactionStep::ReceivingFileDataPdus { + todo!("advance the fsm if everything is finished") + } + if self.step == TransactionStep::TransferCompletion { + self.transfer_completion()?; + } + if self.step == TransactionStep::SendingAckPdu { + todo!(); + } + if self.step == TransactionStep::SendingFinishedPdu { + return Ok(()); } Ok(()) } @@ -355,13 +353,17 @@ impl DestinationHandler { } fn transfer_completion(&mut self) -> Result<(), DestError> { - todo!(); + if self.transaction_params.metadata_params.closure_requested { + self.prepare_finished_pdu()?; + } + todo!("user indication"); Ok(()) } fn prepare_finished_pdu(&mut self) -> Result<(), DestError> { self.packets_to_send_ctx.packet_available = true; self.packets_to_send_ctx.directive = Some(FileDirectiveType::FinishedPdu); + self.step = TransactionStep::SendingFinishedPdu; Ok(()) } } From f3d862ac1995aaad7bf2be1a6efbe0b52ab4a828 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 11 Aug 2023 12:17:41 +0200 Subject: [PATCH 12/45] improved impl, added reset method for handler --- satrs-core/src/cfdp/dest.rs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index 8ff77d1..0731341 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -149,9 +149,7 @@ impl DestinationHandler { return Ok(None); } let directive = self.packets_to_send_ctx.directive.unwrap(); - let mut written_size = 0; - match directive { - FileDirectiveType::EofPdu => todo!(), + let written_size = match directive { FileDirectiveType::FinishedPdu => { let pdu_header = PduHeader::new_no_file_data(self.pdu_conf, 0); let finished_pdu = if self.transaction_params.condition_code @@ -175,14 +173,16 @@ impl DestinationHandler { entity_id, ) }; - written_size = finished_pdu.write_to_bytes(buf)?; + finished_pdu.write_to_bytes(buf)? } FileDirectiveType::AckPdu => todo!(), - FileDirectiveType::MetadataPdu => todo!(), FileDirectiveType::NakPdu => todo!(), - FileDirectiveType::PromptPdu => todo!(), FileDirectiveType::KeepAlivePdu => todo!(), - } + _ => { + // This should never happen and is considered an internal impl error + panic!("invalid file directive {directive:?} for dest handler send packet"); + } + }; Ok(Some((directive, written_size))) } @@ -306,6 +306,7 @@ impl DestinationHandler { todo!(); } if self.step == TransactionStep::SendingFinishedPdu { + self.reset(); return Ok(()); } Ok(()) @@ -360,6 +361,13 @@ impl DestinationHandler { Ok(()) } + fn reset(&mut self) { + self.step = TransactionStep::Idle; + self.state = State::Idle; + self.packets_to_send_ctx.packet_available = false; + self.transaction_params.reset(); + } + fn prepare_finished_pdu(&mut self) -> Result<(), DestError> { self.packets_to_send_ctx.packet_available = true; self.packets_to_send_ctx.directive = Some(FileDirectiveType::FinishedPdu); From beb80b2188fa191afe29769457f7560eac963695 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 11 Aug 2023 12:44:24 +0200 Subject: [PATCH 13/45] start adding CFDP user --- satrs-core/src/cfdp/mod.rs | 22 +++++++++++++++++ satrs-core/src/cfdp/user.rs | 48 +++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 satrs-core/src/cfdp/user.rs diff --git a/satrs-core/src/cfdp/mod.rs b/satrs-core/src/cfdp/mod.rs index 2fba15e..527ca05 100644 --- a/satrs-core/src/cfdp/mod.rs +++ b/satrs-core/src/cfdp/mod.rs @@ -1,6 +1,28 @@ use crc::{Crc, CRC_32_CKSUM}; +use spacepackets::util::UnsignedByteField; pub mod dest; +pub mod user; + +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub struct TransactionId { + source_id: UnsignedByteField, + seq_num: UnsignedByteField, +} + +impl TransactionId { + pub fn new(source_id: UnsignedByteField, seq_num: UnsignedByteField) -> Self { + Self { source_id, seq_num } + } + + pub fn source_id(&self) -> &UnsignedByteField { + &self.source_id + } + + pub fn seq_num(&self) -> &UnsignedByteField { + &self.seq_num + } +} #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum TransactionStep { diff --git a/satrs-core/src/cfdp/user.rs b/satrs-core/src/cfdp/user.rs new file mode 100644 index 0000000..354a95a --- /dev/null +++ b/satrs-core/src/cfdp/user.rs @@ -0,0 +1,48 @@ +use spacepackets::{ + cfdp::{ + pdu::{ + file_data::RecordContinuationState, + finished::{DeliveryCode, FileStatus}, + }, + ConditionCode, + }, + util::UnsignedByteField, +}; + +use super::TransactionId; + +#[derive(Debug, Copy, Clone)] +pub struct TransactionFinishedParams { + pub id: TransactionId, + pub condition_code: ConditionCode, + pub delivery_code: DeliveryCode, + pub file_status: FileStatus, +} + +#[derive(Debug)] +pub struct MetadataReceivedParams<'src_file, 'dest_file, 'msgs_to_user> { + pub id: TransactionId, + pub source_id: UnsignedByteField, + pub file_size: u64, + pub src_file_name: &'src_file str, + pub dest_file_name: &'dest_file str, + // TODO: This is pretty low-level. Is there a better way to do this? + pub msgs_to_user: &'msgs_to_user [u8], +} + +#[derive(Debug)] +pub struct FileSegmentRecvdParams<'seg_meta> { + pub id: TransactionId, + pub offset: u64, + pub length: usize, + pub rec_cont_state: Option, + pub segment_metadata: Option<&'seg_meta [u8]>, +} + +pub trait CfdpUser { + fn transaction_indication(&mut self, id: &TransactionId); + fn eof_sent_indication(&mut self, id: &TransactionId); + fn transaction_finished_indication(&mut self, finished_params: &TransactionFinishedParams); + fn metadata_recvd_indication(&mut self, md_recvd_params: &MetadataReceivedParams); + fn file_segment_recvd_indication(&mut self, segment_recvd_params: &FileSegmentRecvdParams); +} From 471a955bb1aa8b319918170abf54d1d31a21cb30 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 11 Aug 2023 13:03:32 +0200 Subject: [PATCH 14/45] rudimentary user trait --- satrs-core/src/cfdp/user.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/satrs-core/src/cfdp/user.rs b/satrs-core/src/cfdp/user.rs index 354a95a..c07fa99 100644 --- a/satrs-core/src/cfdp/user.rs +++ b/satrs-core/src/cfdp/user.rs @@ -45,4 +45,16 @@ pub trait CfdpUser { fn transaction_finished_indication(&mut self, finished_params: &TransactionFinishedParams); fn metadata_recvd_indication(&mut self, md_recvd_params: &MetadataReceivedParams); fn file_segment_recvd_indication(&mut self, segment_recvd_params: &FileSegmentRecvdParams); + // TODO: The standard does not strictly specify how the report information looks.. + fn report_indication(&mut self, id: &TransactionId); + fn suspended_indication(&mut self, id: &TransactionId, condition_code: ConditionCode); + fn resumed_indication(&mut self, id: &TransactionId, progress: u64); + fn fault_indication( + &mut self, + id: &TransactionId, + condition_code: ConditionCode, + progress: u64, + ); + fn abandoned_indication(&mut self, id: &TransactionId, condition_code: ConditionCode, progress: u64); + fn eof_recvd_indication(&mut self, id: &TransactionId); } From f69035a8688eb369a39dee4eb9f4cd61d2b9452c Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 11 Aug 2023 13:12:48 +0200 Subject: [PATCH 15/45] introduce CFDP user --- satrs-core/src/cfdp/dest.rs | 65 +++++++++++++++++++++++++++++++++++-- satrs-core/src/cfdp/user.rs | 7 +++- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index 0731341..4d392c7 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -5,7 +5,8 @@ use std::{ path::{Path, PathBuf}, }; -use super::{State, TransactionStep, CRC_32}; +use super::{user::CfdpUser, State, TransactionStep, CRC_32}; +use alloc::boxed::Box; use spacepackets::{ cfdp::{ pdu::{ @@ -29,6 +30,7 @@ pub struct DestinationHandler { pdu_conf: CommonPduConfig, transaction_params: TransactionParams, packets_to_send_ctx: PacketsToSendContext, + cfdp_user: Box, } #[derive(Debug, Default)] @@ -101,7 +103,7 @@ pub enum DestError { } impl DestinationHandler { - pub fn new(id: impl Into) -> Self { + pub fn new(id: impl Into, cfdp_user: Box) -> Self { Self { id: id.into(), step: TransactionStep::Idle, @@ -109,6 +111,7 @@ impl DestinationHandler { pdu_conf: Default::default(), transaction_params: Default::default(), packets_to_send_ctx: Default::default(), + cfdp_user, } } @@ -382,10 +385,66 @@ mod tests { use super::*; + pub struct TestCfdpUser {} + + impl CfdpUser for TestCfdpUser { + fn transaction_indication(&mut self, id: &crate::cfdp::TransactionId) {} + + fn eof_sent_indication(&mut self, id: &crate::cfdp::TransactionId) {} + + fn transaction_finished_indication( + &mut self, + finished_params: &crate::cfdp::user::TransactionFinishedParams, + ) { + } + + fn metadata_recvd_indication( + &mut self, + md_recvd_params: &crate::cfdp::user::MetadataReceivedParams, + ) { + } + + fn file_segment_recvd_indication( + &mut self, + segment_recvd_params: &crate::cfdp::user::FileSegmentRecvdParams, + ) { + } + + fn report_indication(&mut self, id: &crate::cfdp::TransactionId) {} + + fn suspended_indication( + &mut self, + id: &crate::cfdp::TransactionId, + condition_code: ConditionCode, + ) { + } + + fn resumed_indication(&mut self, id: &crate::cfdp::TransactionId, progress: u64) {} + + fn fault_indication( + &mut self, + id: &crate::cfdp::TransactionId, + condition_code: ConditionCode, + progress: u64, + ) { + } + + fn abandoned_indication( + &mut self, + id: &crate::cfdp::TransactionId, + condition_code: ConditionCode, + progress: u64, + ) { + } + + fn eof_recvd_indication(&mut self, id: &crate::cfdp::TransactionId) {} + } + #[test] fn test_basic() { let test_id = UnsignedByteFieldU8::new(1); - let dest_handler = DestinationHandler::new(test_id); + let test_user = TestCfdpUser {}; + let dest_handler = DestinationHandler::new(test_id, test_user); assert_eq!(dest_handler.state(), State::Idle); assert_eq!(dest_handler.step(), TransactionStep::Idle); } diff --git a/satrs-core/src/cfdp/user.rs b/satrs-core/src/cfdp/user.rs index c07fa99..b06045a 100644 --- a/satrs-core/src/cfdp/user.rs +++ b/satrs-core/src/cfdp/user.rs @@ -55,6 +55,11 @@ pub trait CfdpUser { condition_code: ConditionCode, progress: u64, ); - fn abandoned_indication(&mut self, id: &TransactionId, condition_code: ConditionCode, progress: u64); + fn abandoned_indication( + &mut self, + id: &TransactionId, + condition_code: ConditionCode, + progress: u64, + ); fn eof_recvd_indication(&mut self, id: &TransactionId); } From 8a73a99f2630213796cc4cc2ede6a25943970d1e Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 11 Aug 2023 13:50:06 +0200 Subject: [PATCH 16/45] use the user handler for the first time --- satrs-core/src/cfdp/dest.rs | 92 +++++++++++++++++++++++++++---------- 1 file changed, 69 insertions(+), 23 deletions(-) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index 4d392c7..caaa7e5 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -5,7 +5,10 @@ use std::{ path::{Path, PathBuf}, }; -use super::{user::CfdpUser, State, TransactionStep, CRC_32}; +use super::{ + user::{CfdpUser, MetadataReceivedParams}, + State, TransactionId, TransactionStep, CRC_32, +}; use alloc::boxed::Box; use spacepackets::{ cfdp::{ @@ -27,7 +30,6 @@ pub struct DestinationHandler { id: UnsignedByteField, step: TransactionStep, state: State, - pdu_conf: CommonPduConfig, transaction_params: TransactionParams, packets_to_send_ctx: PacketsToSendContext, cfdp_user: Box, @@ -39,28 +41,46 @@ struct PacketsToSendContext { directive: Option, } -struct TransactionParams { - metadata_params: MetadataGenericParams, +#[derive(Debug)] +struct FileProperties { src_file_name: [u8; u8::MAX as usize], src_file_name_len: usize, dest_file_name: [u8; u8::MAX as usize], dest_file_name_len: usize, dest_path_buf: PathBuf, +} + +#[derive(Debug)] +struct TransactionParams { + transaction_id: Option, + metadata_params: MetadataGenericParams, + pdu_conf: CommonPduConfig, + file_properties: FileProperties, condition_code: ConditionCode, delivery_code: DeliveryCode, file_status: FileStatus, cksum_buf: [u8; 1024], } -impl Default for TransactionParams { +impl Default for FileProperties { fn default() -> Self { Self { - metadata_params: Default::default(), src_file_name: [0; u8::MAX as usize], src_file_name_len: Default::default(), dest_file_name: [0; u8::MAX as usize], dest_file_name_len: Default::default(), dest_path_buf: Default::default(), + } + } +} + +impl Default for TransactionParams { + fn default() -> Self { + Self { + transaction_id: None, + metadata_params: Default::default(), + pdu_conf: Default::default(), + file_properties: Default::default(), condition_code: ConditionCode::NoError, delivery_code: DeliveryCode::Incomplete, file_status: FileStatus::Unreported, @@ -108,7 +128,6 @@ impl DestinationHandler { id: id.into(), step: TransactionStep::Idle, state: State::Idle, - pdu_conf: Default::default(), transaction_params: Default::default(), packets_to_send_ctx: Default::default(), cfdp_user, @@ -154,7 +173,7 @@ impl DestinationHandler { let directive = self.packets_to_send_ctx.directive.unwrap(); let written_size = match directive { FileDirectiveType::FinishedPdu => { - let pdu_header = PduHeader::new_no_file_data(self.pdu_conf, 0); + let pdu_header = PduHeader::new_no_file_data(self.transaction_params.pdu_conf, 0); let finished_pdu = if self.transaction_params.condition_code == ConditionCode::NoError || self.transaction_params.condition_code @@ -223,16 +242,17 @@ impl DestinationHandler { if src_name.is_empty() { return Err(DestError::EmptySrcFileField); } - self.transaction_params.src_file_name[..src_name.len_value()] + self.transaction_params.file_properties.src_file_name[..src_name.len_value()] .copy_from_slice(src_name.value().unwrap()); - self.transaction_params.src_file_name_len = src_name.len_value(); + self.transaction_params.file_properties.src_file_name_len = src_name.len_value(); let dest_name = metadata_pdu.dest_file_name(); if dest_name.is_empty() { return Err(DestError::EmptyDestFileField); } - self.transaction_params.dest_file_name[..dest_name.len_value()] + self.transaction_params.file_properties.dest_file_name[..dest_name.len_value()] .copy_from_slice(dest_name.value().unwrap()); - self.transaction_params.dest_file_name_len = dest_name.len_value(); + self.transaction_params.file_properties.dest_file_name_len = dest_name.len_value(); + self.transaction_params.pdu_conf = *metadata_pdu.pdu_header().common_pdu_conf(); Ok(()) } @@ -243,7 +263,7 @@ impl DestinationHandler { let fd_pdu = FileDataPdu::from_bytes(raw_packet)?; let mut dest_file = File::options() .write(true) - .open(&self.transaction_params.dest_path_buf)?; + .open(&self.transaction_params.file_properties.dest_path_buf)?; dest_file.seek(SeekFrom::Start(fd_pdu.offset()))?; dest_file.write_all(fd_pdu.file_data())?; Ok(()) @@ -279,7 +299,7 @@ impl DestinationHandler { fn checksum_check(&mut self, expected_checksum: u32) -> Result { let mut digest = CRC_32.digest(); - let file_to_check = File::open(&self.transaction_params.dest_path_buf)?; + let file_to_check = File::open(&self.transaction_params.file_properties.dest_path_buf)?; let mut buf_reader = BufReader::new(file_to_check); loop { let bytes_read = buf_reader.read(&mut self.transaction_params.cksum_buf)?; @@ -327,11 +347,32 @@ impl DestinationHandler { } fn transaction_start(&mut self) -> Result<(), DestError> { - let dest_path = Path::new(from_utf8( - &self.transaction_params.dest_file_name[..self.transaction_params.dest_file_name_len], - )?); - - self.transaction_params.dest_path_buf = dest_path.to_path_buf(); + let dest_name = from_utf8( + &self.transaction_params.file_properties.dest_file_name + [..self.transaction_params.file_properties.dest_file_name_len], + )?; + let dest_path = Path::new(dest_name); + self.transaction_params.file_properties.dest_path_buf = dest_path.to_path_buf(); + let source_id = self.transaction_params.pdu_conf.source_id(); + let id = TransactionId::new( + source_id, + self.transaction_params.pdu_conf.transaction_seq_num, + ); + let src_name = from_utf8( + &self.transaction_params.file_properties.src_file_name + [0..self.transaction_params.file_properties.src_file_name_len], + )?; + let metadata_recvd_params = MetadataReceivedParams { + id, + source_id, + file_size: self.transaction_params.metadata_params.file_size, + src_file_name: src_name, + dest_file_name: dest_name, + msgs_to_user: &[], + }; + self.transaction_params.transaction_id = Some(id); + self.cfdp_user + .metadata_recvd_indication(&metadata_recvd_params); let metadata = metadata(dest_path)?; if metadata.is_dir() { @@ -340,7 +381,8 @@ impl DestinationHandler { // and a destination name of /home/test, the resulting file name should be // /home/test/hello.txt let source_path = Path::new(from_utf8( - &self.transaction_params.src_file_name[..self.transaction_params.src_file_name_len], + &self.transaction_params.file_properties.src_file_name + [..self.transaction_params.file_properties.src_file_name_len], )?); let source_name = source_path.file_name(); @@ -348,15 +390,19 @@ impl DestinationHandler { return Err(DestError::PathConcatError); } let source_name = source_name.unwrap(); - self.transaction_params.dest_path_buf.push(source_name); + self.transaction_params + .file_properties + .dest_path_buf + .push(source_name); } // This function does exactly what we require: Create a new file if it does not exist yet // and trucate an existing one. - File::create(&self.transaction_params.dest_path_buf)?; + File::create(&self.transaction_params.file_properties.dest_path_buf)?; Ok(()) } fn transfer_completion(&mut self) -> Result<(), DestError> { + // This function should never be called with metadata parameters not set if self.transaction_params.metadata_params.closure_requested { self.prepare_finished_pdu()?; } @@ -444,7 +490,7 @@ mod tests { fn test_basic() { let test_id = UnsignedByteFieldU8::new(1); let test_user = TestCfdpUser {}; - let dest_handler = DestinationHandler::new(test_id, test_user); + let dest_handler = DestinationHandler::new(test_id, Box::new(test_user)); assert_eq!(dest_handler.state(), State::Idle); assert_eq!(dest_handler.step(), TransactionStep::Idle); } From 7469be6b720894c9548de700b60284059f0c1ddc Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 11 Aug 2023 13:53:09 +0200 Subject: [PATCH 17/45] use space here --- satrs-core/Cargo.toml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/satrs-core/Cargo.toml b/satrs-core/Cargo.toml index 15ad34d..21cb2ad 100644 --- a/satrs-core/Cargo.toml +++ b/satrs-core/Cargo.toml @@ -83,23 +83,23 @@ version = "1" [features] default = ["std"] std = [ - "downcast-rs/std", - "alloc", - "bus", - "postcard/use-std", - "crossbeam-channel/std", - "serde/std", - "spacepackets/std", - "num_enum/std", - "thiserror", + "downcast-rs/std", + "alloc", + "bus", + "postcard/use-std", + "crossbeam-channel/std", + "serde/std", + "spacepackets/std", + "num_enum/std", + "thiserror", "crc" ] alloc = [ - "serde/alloc", - "spacepackets/alloc", - "hashbrown", - "dyn-clone", - "downcast-rs" + "serde/alloc", + "spacepackets/alloc", + "hashbrown", + "dyn-clone", + "downcast-rs" ] serde = ["dep:serde", "spacepackets/serde"] crossbeam = ["crossbeam-channel"] From c1252f949e204fbee030628811f79c1d3373e841 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 11 Aug 2023 13:56:33 +0200 Subject: [PATCH 18/45] fix all clippy warnings --- satrs-core/src/cfdp/dest.rs | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index caaa7e5..fcb698a 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -292,9 +292,8 @@ impl DestinationHandler { Ok(()) } - pub fn handle_prompt_pdu(&mut self, raw_packet: &[u8]) -> Result<(), DestError> { + pub fn handle_prompt_pdu(&mut self, _raw_packet: &[u8]) -> Result<(), DestError> { todo!(); - Ok(()) } fn checksum_check(&mut self, expected_checksum: u32) -> Result { @@ -407,7 +406,6 @@ impl DestinationHandler { self.prepare_finished_pdu()?; } todo!("user indication"); - Ok(()) } fn reset(&mut self) { @@ -434,56 +432,56 @@ mod tests { pub struct TestCfdpUser {} impl CfdpUser for TestCfdpUser { - fn transaction_indication(&mut self, id: &crate::cfdp::TransactionId) {} + fn transaction_indication(&mut self, _id: &crate::cfdp::TransactionId) {} - fn eof_sent_indication(&mut self, id: &crate::cfdp::TransactionId) {} + fn eof_sent_indication(&mut self, _id: &crate::cfdp::TransactionId) {} fn transaction_finished_indication( &mut self, - finished_params: &crate::cfdp::user::TransactionFinishedParams, + _finished_params: &crate::cfdp::user::TransactionFinishedParams, ) { } fn metadata_recvd_indication( &mut self, - md_recvd_params: &crate::cfdp::user::MetadataReceivedParams, + _md_recvd_params: &crate::cfdp::user::MetadataReceivedParams, ) { } fn file_segment_recvd_indication( &mut self, - segment_recvd_params: &crate::cfdp::user::FileSegmentRecvdParams, + _segment_recvd_params: &crate::cfdp::user::FileSegmentRecvdParams, ) { } - fn report_indication(&mut self, id: &crate::cfdp::TransactionId) {} + fn report_indication(&mut self, _id: &crate::cfdp::TransactionId) {} fn suspended_indication( &mut self, - id: &crate::cfdp::TransactionId, - condition_code: ConditionCode, + _id: &crate::cfdp::TransactionId, + _condition_code: ConditionCode, ) { } - fn resumed_indication(&mut self, id: &crate::cfdp::TransactionId, progress: u64) {} + fn resumed_indication(&mut self, _id: &crate::cfdp::TransactionId,_progresss: u64) {} fn fault_indication( &mut self, - id: &crate::cfdp::TransactionId, - condition_code: ConditionCode, - progress: u64, + _id: &crate::cfdp::TransactionId, + _condition_code: ConditionCode, + _progress: u64, ) { } fn abandoned_indication( &mut self, - id: &crate::cfdp::TransactionId, - condition_code: ConditionCode, - progress: u64, + _id: &crate::cfdp::TransactionId, + _condition_code: ConditionCode, + _progress: u64, ) { } - fn eof_recvd_indication(&mut self, id: &crate::cfdp::TransactionId) {} + fn eof_recvd_indication(&mut self, _id: &crate::cfdp::TransactionId) {} } #[test] From 1bae0c30bbd11b0e4eba584b2d76607655c85ee9 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 12 Aug 2023 11:02:32 +0200 Subject: [PATCH 19/45] this might work better --- satrs-core/src/cfdp/dest.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index fcb698a..5afb094 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -9,7 +9,6 @@ use super::{ user::{CfdpUser, MetadataReceivedParams}, State, TransactionId, TransactionStep, CRC_32, }; -use alloc::boxed::Box; use spacepackets::{ cfdp::{ pdu::{ @@ -32,7 +31,7 @@ pub struct DestinationHandler { state: State, transaction_params: TransactionParams, packets_to_send_ctx: PacketsToSendContext, - cfdp_user: Box, + //cfdp_user: Box, } #[derive(Debug, Default)] @@ -123,21 +122,21 @@ pub enum DestError { } impl DestinationHandler { - pub fn new(id: impl Into, cfdp_user: Box) -> Self { + pub fn new(id: impl Into) -> Self { Self { id: id.into(), step: TransactionStep::Idle, state: State::Idle, transaction_params: Default::default(), packets_to_send_ctx: Default::default(), - cfdp_user, + //cfdp_user, } } - pub fn state_machine(&mut self) -> Result<(), DestError> { + pub fn state_machine(&mut self, cfdp_user: &mut impl CfdpUser) -> Result<(), DestError> { match self.state { State::Idle => todo!(), - State::BusyClass1Nacked => self.fsm_nacked(), + State::BusyClass1Nacked => self.fsm_nacked(cfdp_user), State::BusyClass2Acked => todo!(), } } @@ -313,10 +312,10 @@ impl DestinationHandler { Ok(false) } - fn fsm_nacked(&mut self) -> Result<(), DestError> { + fn fsm_nacked(&mut self, cfdp_user: &mut impl CfdpUser) -> Result<(), DestError> { if self.step == TransactionStep::Idle {} if self.step == TransactionStep::TransactionStart { - self.transaction_start()?; + self.transaction_start(cfdp_user)?; } if self.step == TransactionStep::ReceivingFileDataPdus { todo!("advance the fsm if everything is finished") @@ -345,7 +344,7 @@ impl DestinationHandler { self.state } - fn transaction_start(&mut self) -> Result<(), DestError> { + fn transaction_start(&mut self, cfdp_user: &mut impl CfdpUser) -> Result<(), DestError> { let dest_name = from_utf8( &self.transaction_params.file_properties.dest_file_name [..self.transaction_params.file_properties.dest_file_name_len], @@ -370,7 +369,7 @@ impl DestinationHandler { msgs_to_user: &[], }; self.transaction_params.transaction_id = Some(id); - self.cfdp_user + cfdp_user .metadata_recvd_indication(&metadata_recvd_params); let metadata = metadata(dest_path)?; @@ -488,7 +487,7 @@ mod tests { fn test_basic() { let test_id = UnsignedByteFieldU8::new(1); let test_user = TestCfdpUser {}; - let dest_handler = DestinationHandler::new(test_id, Box::new(test_user)); + let dest_handler = DestinationHandler::new(test_id); assert_eq!(dest_handler.state(), State::Idle); assert_eq!(dest_handler.step(), TransactionStep::Idle); } From 6fdaf02cc752f70f7dedd558edd3e6cf5a7d72fc Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 12 Aug 2023 11:37:53 +0200 Subject: [PATCH 20/45] continued dest handler --- satrs-core/src/cfdp/dest.rs | 54 ++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index 5afb094..5484008 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -369,8 +369,7 @@ impl DestinationHandler { msgs_to_user: &[], }; self.transaction_params.transaction_id = Some(id); - cfdp_user - .metadata_recvd_indication(&metadata_recvd_params); + cfdp_user.metadata_recvd_indication(&metadata_recvd_params); let metadata = metadata(dest_path)?; if metadata.is_dir() { @@ -424,21 +423,39 @@ impl DestinationHandler { #[cfg(test)] mod tests { - use spacepackets::util::UnsignedByteFieldU8; + use spacepackets::util::UnsignedByteFieldU16; use super::*; - pub struct TestCfdpUser {} + const LOCAL_ID: UnsignedByteFieldU16 = UnsignedByteFieldU16::new(1); + const REMOTE_ID: UnsignedByteFieldU16 = UnsignedByteFieldU16::new(2); + + #[derive(Default)] + struct TestCfdpUser { + next_expected_seq_num: u64, + } + + impl TestCfdpUser { + fn generic_id_check(&self, id: &crate::cfdp::TransactionId) { + assert_eq!(id.source_id, REMOTE_ID.into()); + assert_eq!(id.seq_num().value(), self.next_expected_seq_num); + } + } impl CfdpUser for TestCfdpUser { - fn transaction_indication(&mut self, _id: &crate::cfdp::TransactionId) {} + fn transaction_indication(&mut self, id: &crate::cfdp::TransactionId) { + self.generic_id_check(id); + } - fn eof_sent_indication(&mut self, _id: &crate::cfdp::TransactionId) {} + fn eof_sent_indication(&mut self, id: &crate::cfdp::TransactionId) { + self.generic_id_check(id); + } fn transaction_finished_indication( &mut self, - _finished_params: &crate::cfdp::user::TransactionFinishedParams, + finished_params: &crate::cfdp::user::TransactionFinishedParams, ) { + self.generic_id_check(&finished_params.id); } fn metadata_recvd_indication( @@ -462,7 +479,7 @@ mod tests { ) { } - fn resumed_indication(&mut self, _id: &crate::cfdp::TransactionId,_progresss: u64) {} + fn resumed_indication(&mut self, _id: &crate::cfdp::TransactionId, _progresss: u64) {} fn fault_indication( &mut self, @@ -483,12 +500,23 @@ mod tests { fn eof_recvd_indication(&mut self, _id: &crate::cfdp::TransactionId) {} } + fn init_check(handler: &DestinationHandler) { + assert_eq!(handler.state(), State::Idle); + assert_eq!(handler.step(), TransactionStep::Idle); + } + #[test] fn test_basic() { - let test_id = UnsignedByteFieldU8::new(1); - let test_user = TestCfdpUser {}; - let dest_handler = DestinationHandler::new(test_id); - assert_eq!(dest_handler.state(), State::Idle); - assert_eq!(dest_handler.step(), TransactionStep::Idle); + let dest_handler = DestinationHandler::new(LOCAL_ID); + init_check(&dest_handler); + } + + #[test] + fn test_empty_file_transfer() { + let test_user = TestCfdpUser::default(); + let mut dest_handler = DestinationHandler::new(LOCAL_ID); + init_check(&dest_handler); + // TODO: Create Metadata PDU and EOF PDU for empty file transfer. + //dest_handler.insert_packet(pdu_type, pdu_directive, raw_packet) } } From a415cd8f6c4d665d814c381cf56814996bff14ef Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 16 Aug 2023 14:54:56 +0200 Subject: [PATCH 21/45] use released version again --- satrs-core/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/satrs-core/Cargo.toml b/satrs-core/Cargo.toml index 21cb2ad..1eb1da8 100644 --- a/satrs-core/Cargo.toml +++ b/satrs-core/Cargo.toml @@ -65,8 +65,8 @@ default-features = false optional = true [dependencies.spacepackets] -# version = "0.6" -path = "../../spacepackets" +version = "0.7.0-beta.0" +# path = "../../spacepackets" # git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git" # rev = "041959e546e6e72b24eb50986c425a924015e3f4" default-features = false From eb857416840bfcd805060ebb104edd3e3094b388 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 16 Aug 2023 19:28:28 +0200 Subject: [PATCH 22/45] this should get the job done --- satrs-core/Cargo.toml | 8 +++++-- satrs-core/src/cfdp/dest.rs | 42 +++++++++++++++++++++++++++++++++---- satrs-core/src/cfdp/user.rs | 4 ++-- 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/satrs-core/Cargo.toml b/satrs-core/Cargo.toml index 1eb1da8..8cdd2f7 100644 --- a/satrs-core/Cargo.toml +++ b/satrs-core/Cargo.toml @@ -17,6 +17,9 @@ delegate = ">=0.8, <0.11" paste = "1" embed-doc-image = "0.1" +[dependencies.smallvec] +version = "1" + [dependencies.num_enum] version = "0.6" default-features = false @@ -67,8 +70,9 @@ optional = true [dependencies.spacepackets] version = "0.7.0-beta.0" # path = "../../spacepackets" -# git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git" -# rev = "041959e546e6e72b24eb50986c425a924015e3f4" +git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git" +# rev = "" +branch = "update-lv-tlv" default-features = false [dev-dependencies] diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index 5484008..525325c 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -9,6 +9,7 @@ use super::{ user::{CfdpUser, MetadataReceivedParams}, State, TransactionId, TransactionStep, CRC_32, }; +use smallvec::SmallVec; use spacepackets::{ cfdp::{ pdu::{ @@ -18,7 +19,7 @@ use spacepackets::{ metadata::{MetadataGenericParams, MetadataPdu}, CommonPduConfig, FileDirectiveType, PduError, PduHeader, }, - tlv::EntityIdTlv, + tlv::{msg_to_user::MsgToUserTlv, EntityIdTlv, TlvType}, ConditionCode, PduType, }, util::UnsignedByteField, @@ -58,7 +59,10 @@ struct TransactionParams { condition_code: ConditionCode, delivery_code: DeliveryCode, file_status: FileStatus, + //msgs_to_user: Vec>, cksum_buf: [u8; 1024], + msgs_to_user_size: usize, + msgs_to_user_buf: [u8; 1024], } impl Default for FileProperties { @@ -84,6 +88,8 @@ impl Default for TransactionParams { delivery_code: DeliveryCode::Incomplete, file_status: FileStatus::Unreported, cksum_buf: [0; 1024], + msgs_to_user_size: 0, + msgs_to_user_buf: [0; 1024], } } } @@ -242,16 +248,29 @@ impl DestinationHandler { return Err(DestError::EmptySrcFileField); } self.transaction_params.file_properties.src_file_name[..src_name.len_value()] - .copy_from_slice(src_name.value().unwrap()); + .copy_from_slice(src_name.value()); self.transaction_params.file_properties.src_file_name_len = src_name.len_value(); let dest_name = metadata_pdu.dest_file_name(); if dest_name.is_empty() { return Err(DestError::EmptyDestFileField); } self.transaction_params.file_properties.dest_file_name[..dest_name.len_value()] - .copy_from_slice(dest_name.value().unwrap()); + .copy_from_slice(dest_name.value()); self.transaction_params.file_properties.dest_file_name_len = dest_name.len_value(); self.transaction_params.pdu_conf = *metadata_pdu.pdu_header().common_pdu_conf(); + self.transaction_params.msgs_to_user_size = 0; + if metadata_pdu.options().is_some() { + for option_tlv in metadata_pdu.options_iter().unwrap() { + if option_tlv.is_standard_tlv() + && option_tlv.tlv_type().unwrap() == TlvType::MsgToUser + { + self.transaction_params + .msgs_to_user_buf + .copy_from_slice(option_tlv.raw_data().unwrap()); + self.transaction_params.msgs_to_user_size += option_tlv.len_full(); + } + } + } Ok(()) } @@ -360,13 +379,27 @@ impl DestinationHandler { &self.transaction_params.file_properties.src_file_name [0..self.transaction_params.file_properties.src_file_name_len], )?; + let mut msgs_to_user = SmallVec::<[MsgToUserTlv<'_>; 16]>::new(); + let mut num_msgs_to_user = 0; + if self.transaction_params.msgs_to_user_size > 0 { + let mut index = 0; + while index < self.transaction_params.msgs_to_user_size { + // This should never panic as the validity of the options was checked beforehand. + let msgs_to_user_tlv = + MsgToUserTlv::from_bytes(&self.transaction_params.msgs_to_user_buf[index..]) + .expect("message to user creation failed unexpectedly"); + msgs_to_user.push(msgs_to_user_tlv); + index += msgs_to_user_tlv.len_full(); + num_msgs_to_user += 1; + } + } let metadata_recvd_params = MetadataReceivedParams { id, source_id, file_size: self.transaction_params.metadata_params.file_size, src_file_name: src_name, dest_file_name: dest_name, - msgs_to_user: &[], + msgs_to_user: &msgs_to_user[..num_msgs_to_user], }; self.transaction_params.transaction_id = Some(id); cfdp_user.metadata_recvd_indication(&metadata_recvd_params); @@ -516,6 +549,7 @@ mod tests { let test_user = TestCfdpUser::default(); let mut dest_handler = DestinationHandler::new(LOCAL_ID); init_check(&dest_handler); + // TODO: Create Metadata PDU and EOF PDU for empty file transfer. //dest_handler.insert_packet(pdu_type, pdu_directive, raw_packet) } diff --git a/satrs-core/src/cfdp/user.rs b/satrs-core/src/cfdp/user.rs index b06045a..9047bbd 100644 --- a/satrs-core/src/cfdp/user.rs +++ b/satrs-core/src/cfdp/user.rs @@ -4,6 +4,7 @@ use spacepackets::{ file_data::RecordContinuationState, finished::{DeliveryCode, FileStatus}, }, + tlv::msg_to_user::MsgToUserTlv, ConditionCode, }, util::UnsignedByteField, @@ -26,8 +27,7 @@ pub struct MetadataReceivedParams<'src_file, 'dest_file, 'msgs_to_user> { pub file_size: u64, pub src_file_name: &'src_file str, pub dest_file_name: &'dest_file str, - // TODO: This is pretty low-level. Is there a better way to do this? - pub msgs_to_user: &'msgs_to_user [u8], + pub msgs_to_user: &'msgs_to_user [MsgToUserTlv<'msgs_to_user>], } #[derive(Debug)] From d2cdcf9c79e970c51b70967c6374ae141fb5e8c4 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 16 Aug 2023 19:29:16 +0200 Subject: [PATCH 23/45] remove obsolete code --- satrs-core/src/cfdp/dest.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index 525325c..51b161f 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -59,7 +59,6 @@ struct TransactionParams { condition_code: ConditionCode, delivery_code: DeliveryCode, file_status: FileStatus, - //msgs_to_user: Vec>, cksum_buf: [u8; 1024], msgs_to_user_size: usize, msgs_to_user_buf: [u8; 1024], From c664cdb332bb1737a6cebd4f5f6a5e2a7107412d Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 16 Aug 2023 19:47:40 +0200 Subject: [PATCH 24/45] start first test --- satrs-core/src/cfdp/dest.rs | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index 51b161f..ae88b01 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -146,7 +146,11 @@ impl DestinationHandler { } } - pub fn insert_packet( + pub fn insert_packet(_raw_packet: &[u8]) -> Result<(), DestError> { + todo!(); + //Ok(()) + } + pub fn insert_packet_with_known_type( &mut self, pdu_type: PduType, pdu_directive: Option, @@ -455,13 +459,19 @@ impl DestinationHandler { #[cfg(test)] mod tests { - use spacepackets::util::UnsignedByteFieldU16; + use spacepackets::{ + cfdp::{lv::Lv, ChecksumType}, + util::{UbfU16, UnsignedByteFieldU16}, + }; use super::*; const LOCAL_ID: UnsignedByteFieldU16 = UnsignedByteFieldU16::new(1); const REMOTE_ID: UnsignedByteFieldU16 = UnsignedByteFieldU16::new(2); + const SRC_NAME: &str = "source-file.txt"; + const DEST_NAME: &str = "dest-file.txt"; + #[derive(Default)] struct TestCfdpUser { next_expected_seq_num: u64, @@ -545,11 +555,30 @@ mod tests { #[test] fn test_empty_file_transfer() { + let mut buf: [u8; 512] = [0; 512]; let test_user = TestCfdpUser::default(); let mut dest_handler = DestinationHandler::new(LOCAL_ID); init_check(&dest_handler); + let seq_num = UbfU16::new(0); + let pdu_conf = CommonPduConfig::new_with_byte_fields(REMOTE_ID, LOCAL_ID, seq_num).unwrap(); + let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0); + let metadata_params = MetadataGenericParams::new(false, ChecksumType::Crc32, 0); + let metadata_pdu = MetadataPdu::new( + pdu_header, + metadata_params, + Lv::new_from_str(SRC_NAME).unwrap(), + Lv::new_from_str(DEST_NAME).unwrap(), + None, + ); + let written_len = metadata_pdu + .write_to_bytes(&mut buf) + .expect("writing metadata PDU failed"); // TODO: Create Metadata PDU and EOF PDU for empty file transfer. - //dest_handler.insert_packet(pdu_type, pdu_directive, raw_packet) + dest_handler.insert_packet( + PduType::FileDirective, + Some(FileDirectiveType::MetadataPdu), + &buf[..written_len], + ); } } From a0f2d858cee05411c480f18c9a40e976b355b4c9 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 16 Aug 2023 19:51:26 +0200 Subject: [PATCH 25/45] feature gate destination module --- satrs-core/src/cfdp/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/satrs-core/src/cfdp/mod.rs b/satrs-core/src/cfdp/mod.rs index 527ca05..f50faff 100644 --- a/satrs-core/src/cfdp/mod.rs +++ b/satrs-core/src/cfdp/mod.rs @@ -1,6 +1,7 @@ use crc::{Crc, CRC_32_CKSUM}; use spacepackets::util::UnsignedByteField; +#[cfg(feature = "std")] pub mod dest; pub mod user; From 143b0869a4db27e56817cc30deb5839313863f85 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 16 Aug 2023 19:53:29 +0200 Subject: [PATCH 26/45] crc dependency is mandatory --- satrs-core/Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/satrs-core/Cargo.toml b/satrs-core/Cargo.toml index 8cdd2f7..d0e258b 100644 --- a/satrs-core/Cargo.toml +++ b/satrs-core/Cargo.toml @@ -26,7 +26,6 @@ default-features = false [dependencies.crc] version = "3" -optional = true [dependencies.dyn-clone] version = "1" @@ -96,7 +95,6 @@ std = [ "spacepackets/std", "num_enum/std", "thiserror", - "crc" ] alloc = [ "serde/alloc", From afd9395ceeb7886af06167060caf914ac6afc8dd Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Thu, 17 Aug 2023 22:11:27 +0200 Subject: [PATCH 27/45] phew --- satrs-core/Cargo.toml | 2 +- satrs-core/src/cfdp/dest.rs | 65 +++++++++++++-------- satrs-core/src/cfdp/mod.rs | 113 +++++++++++++++++++++++++++++++++++- 3 files changed, 153 insertions(+), 27 deletions(-) diff --git a/satrs-core/Cargo.toml b/satrs-core/Cargo.toml index d0e258b..c53fd08 100644 --- a/satrs-core/Cargo.toml +++ b/satrs-core/Cargo.toml @@ -71,7 +71,7 @@ version = "0.7.0-beta.0" # path = "../../spacepackets" git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git" # rev = "" -branch = "update-lv-tlv" +branch = "pdu-additions" default-features = false [dev-dependencies] diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index ae88b01..04273eb 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -7,7 +7,7 @@ use std::{ use super::{ user::{CfdpUser, MetadataReceivedParams}, - State, TransactionId, TransactionStep, CRC_32, + PacketInfo, PacketTarget, State, TransactionId, TransactionStep, CRC_32, }; use smallvec::SmallVec; use spacepackets::{ @@ -20,7 +20,7 @@ use spacepackets::{ CommonPduConfig, FileDirectiveType, PduError, PduHeader, }, tlv::{msg_to_user::MsgToUserTlv, EntityIdTlv, TlvType}, - ConditionCode, PduType, + ConditionCode, PduType, TransmissionMode, }, util::UnsignedByteField, }; @@ -142,28 +142,29 @@ impl DestinationHandler { match self.state { State::Idle => todo!(), State::BusyClass1Nacked => self.fsm_nacked(cfdp_user), - State::BusyClass2Acked => todo!(), + State::BusyClass2Acked => todo!("acknowledged mode not implemented yet"), } } - pub fn insert_packet(_raw_packet: &[u8]) -> Result<(), DestError> { - todo!(); - //Ok(()) - } - pub fn insert_packet_with_known_type( - &mut self, - pdu_type: PduType, - pdu_directive: Option, - raw_packet: &[u8], - ) -> Result<(), DestError> { - match pdu_type { + pub fn insert_packet(&mut self, packet_info: &PacketInfo) -> Result<(), DestError> { + if packet_info.target() != PacketTarget::DestEntity { + // Unwrap is okay here, a PacketInfo for a file data PDU should always have the + // destination as the target. + return Err(DestError::CantProcessPacketType( + packet_info.pdu_directive().unwrap(), + )); + } + match packet_info.pdu_type { PduType::FileDirective => { - if pdu_directive.is_none() { + if packet_info.pdu_directive.is_none() { return Err(DestError::DirectiveExpected); } - self.handle_file_directive(pdu_directive.unwrap(), raw_packet) + self.handle_file_directive( + packet_info.pdu_directive.unwrap(), + packet_info.raw_packet, + ) } - PduType::FileData => self.handle_file_data(raw_packet), + PduType::FileData => self.handle_file_data(packet_info.raw_packet), } } @@ -262,6 +263,13 @@ impl DestinationHandler { self.transaction_params.file_properties.dest_file_name_len = dest_name.len_value(); self.transaction_params.pdu_conf = *metadata_pdu.pdu_header().common_pdu_conf(); self.transaction_params.msgs_to_user_size = 0; + if metadata_pdu.pdu_header().common_pdu_conf().trans_mode + == TransmissionMode::Unacknowledged + { + self.state = State::BusyClass1Nacked; + } else { + self.state = State::BusyClass2Acked; + } if metadata_pdu.options().is_some() { for option_tlv in metadata_pdu.options_iter().unwrap() { if option_tlv.is_standard_tlv() @@ -459,6 +467,9 @@ impl DestinationHandler { #[cfg(test)] mod tests { + #[allow(unused_imports)] + use std::println; + use spacepackets::{ cfdp::{lv::Lv, ChecksumType}, util::{UbfU16, UnsignedByteFieldU16}, @@ -556,12 +567,14 @@ mod tests { #[test] fn test_empty_file_transfer() { let mut buf: [u8; 512] = [0; 512]; - let test_user = TestCfdpUser::default(); + let mut test_user = TestCfdpUser::default(); let mut dest_handler = DestinationHandler::new(LOCAL_ID); init_check(&dest_handler); let seq_num = UbfU16::new(0); - let pdu_conf = CommonPduConfig::new_with_byte_fields(REMOTE_ID, LOCAL_ID, seq_num).unwrap(); + let mut pdu_conf = + CommonPduConfig::new_with_byte_fields(REMOTE_ID, LOCAL_ID, seq_num).unwrap(); + pdu_conf.trans_mode = TransmissionMode::Unacknowledged; let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0); let metadata_params = MetadataGenericParams::new(false, ChecksumType::Crc32, 0); let metadata_pdu = MetadataPdu::new( @@ -574,11 +587,13 @@ mod tests { let written_len = metadata_pdu .write_to_bytes(&mut buf) .expect("writing metadata PDU failed"); - // TODO: Create Metadata PDU and EOF PDU for empty file transfer. - dest_handler.insert_packet( - PduType::FileDirective, - Some(FileDirectiveType::MetadataPdu), - &buf[..written_len], - ); + let packet_info = + PacketInfo::new(&buf[..written_len]).expect("generating packet info failed"); + let insert_result = dest_handler.insert_packet(&packet_info); + if let Err(e) = insert_result { + panic!("insert result error: {e}"); + } + let result = dest_handler.state_machine(&mut test_user); + assert!(result.is_ok()); } } diff --git a/satrs-core/src/cfdp/mod.rs b/satrs-core/src/cfdp/mod.rs index f50faff..4e74d24 100644 --- a/satrs-core/src/cfdp/mod.rs +++ b/satrs-core/src/cfdp/mod.rs @@ -1,11 +1,21 @@ use crc::{Crc, CRC_32_CKSUM}; -use spacepackets::util::UnsignedByteField; +use spacepackets::{ + cfdp::{ + pdu::{FileDirectiveType, PduError, PduHeader}, + PduType, + }, + util::UnsignedByteField, +}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; #[cfg(feature = "std")] pub mod dest; pub mod user; #[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct TransactionId { source_id: UnsignedByteField, seq_num: UnsignedByteField, @@ -26,6 +36,7 @@ impl TransactionId { } #[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum TransactionStep { Idle = 0, TransactionStart = 1, @@ -36,6 +47,7 @@ pub enum TransactionStep { } #[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum State { Idle = 0, BusyClass1Nacked = 2, @@ -44,6 +56,105 @@ pub enum State { pub const CRC_32: Crc = Crc::::new(&CRC_32_CKSUM); +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum PacketTarget { + SourceEntity, + DestEntity, +} + +/// This is a helper struct which contains base information about a particular PDU packet. +/// This is also necessary information for CFDP packet routing. For example, some packet types +/// like file data PDUs can only be used by CFDP source entities. +pub struct PacketInfo<'raw_packet> { + pdu_type: PduType, + pdu_directive: Option, + target: PacketTarget, + raw_packet: &'raw_packet [u8], +} + +impl<'raw> PacketInfo<'raw> { + pub fn new(raw_packet: &'raw [u8]) -> Result { + let (pdu_header, header_len) = PduHeader::from_bytes(raw_packet)?; + if pdu_header.pdu_type() == PduType::FileData { + return Ok(Self { + pdu_type: pdu_header.pdu_type(), + pdu_directive: None, + target: PacketTarget::DestEntity, + raw_packet, + }); + } + if pdu_header.pdu_datafield_len() < 1 { + return Err(PduError::FormatError); + } + // Route depending on PDU type and directive type if applicable. Retrieve directive type + // from the raw stream for better performance (with sanity and directive code check). + // The routing is based on section 4.5 of the CFDP standard which specifies the PDU forwarding + // procedure. + let directive = FileDirectiveType::try_from(raw_packet[header_len]).map_err(|_| { + PduError::InvalidDirectiveType { + found: raw_packet[header_len], + expected: None, + } + })?; + let packet_target = match directive { + // Section c) of 4.5.3: These PDUs should always be targeted towards the file sender a.k.a. + // the source handler + FileDirectiveType::NakPdu + | FileDirectiveType::FinishedPdu + | FileDirectiveType::KeepAlivePdu => PacketTarget::SourceEntity, + // Section b) of 4.5.3: These PDUs should always be targeted towards the file receiver a.k.a. + // the destination handler + FileDirectiveType::MetadataPdu + | FileDirectiveType::EofPdu + | FileDirectiveType::PromptPdu => PacketTarget::DestEntity, + // Section a): Recipient depends of the type of PDU that is being acknowledged. We can simply + // extract the PDU type from the raw stream. If it is an EOF PDU, this packet is passed to + // the source handler, for a Finished PDU, it is passed to the destination handler. + FileDirectiveType::AckPdu => { + let acked_directive = FileDirectiveType::try_from(raw_packet[header_len + 1]) + .map_err(|_| PduError::InvalidDirectiveType { + found: raw_packet[header_len], + expected: None, + })?; + if acked_directive == FileDirectiveType::EofPdu { + PacketTarget::SourceEntity + } else if acked_directive == FileDirectiveType::FinishedPdu { + PacketTarget::DestEntity + } else { + // TODO: Maybe a better error? This might be confusing.. + return Err(PduError::InvalidDirectiveType { + found: raw_packet[header_len + 1], + expected: None, + }); + } + } + }; + Ok(Self { + pdu_type: pdu_header.pdu_type(), + pdu_directive: Some(directive), + target: packet_target, + raw_packet, + }) + } + + pub fn pdu_type(&self) -> PduType { + self.pdu_type + } + + pub fn pdu_directive(&self) -> Option { + self.pdu_directive + } + + pub fn target(&self) -> PacketTarget { + self.target + } + + pub fn raw_packet(&self) -> &[u8] { + self.raw_packet + } +} + #[cfg(test)] mod tests { #[test] From 8798a3457e3223e9fdc076e2a84f5b4fe47caa48 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 1 Sep 2023 18:36:04 +0200 Subject: [PATCH 28/45] added small file transfer unittest --- satrs-core/src/cfdp/dest.rs | 323 ++++++++++++++++++++++++------------ 1 file changed, 214 insertions(+), 109 deletions(-) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index 04273eb..29c6b01 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -5,6 +5,8 @@ use std::{ path::{Path, PathBuf}, }; +use crate::cfdp::user::TransactionFinishedParams; + use super::{ user::{CfdpUser, MetadataReceivedParams}, PacketInfo, PacketTarget, State, TransactionId, TransactionStep, CRC_32, @@ -30,9 +32,8 @@ pub struct DestinationHandler { id: UnsignedByteField, step: TransactionStep, state: State, - transaction_params: TransactionParams, + tparams: TransactionParams, packets_to_send_ctx: PacketsToSendContext, - //cfdp_user: Box, } #[derive(Debug, Default)] @@ -51,14 +52,32 @@ struct FileProperties { } #[derive(Debug)] -struct TransactionParams { +struct TransferState { transaction_id: Option, - metadata_params: MetadataGenericParams, - pdu_conf: CommonPduConfig, - file_properties: FileProperties, + progress: usize, condition_code: ConditionCode, delivery_code: DeliveryCode, file_status: FileStatus, + metadata_params: MetadataGenericParams, +} + +impl Default for TransferState { + fn default() -> Self { + Self { + transaction_id: None, + progress: Default::default(), + condition_code: ConditionCode::NoError, + delivery_code: DeliveryCode::Incomplete, + file_status: FileStatus::Unreported, + metadata_params: Default::default(), + } + } +} +#[derive(Debug)] +struct TransactionParams { + tstate: TransferState, + pdu_conf: CommonPduConfig, + file_properties: FileProperties, cksum_buf: [u8; 1024], msgs_to_user_size: usize, msgs_to_user_buf: [u8; 1024], @@ -76,27 +95,33 @@ impl Default for FileProperties { } } +impl TransactionParams { + fn file_size(&self) -> usize { + self.tstate.metadata_params.file_size as usize + } + + fn metadata_params(&self) -> &MetadataGenericParams { + &self.tstate.metadata_params + } +} + impl Default for TransactionParams { fn default() -> Self { Self { - transaction_id: None, - metadata_params: Default::default(), pdu_conf: Default::default(), - file_properties: Default::default(), - condition_code: ConditionCode::NoError, - delivery_code: DeliveryCode::Incomplete, - file_status: FileStatus::Unreported, cksum_buf: [0; 1024], msgs_to_user_size: 0, msgs_to_user_buf: [0; 1024], + tstate: Default::default(), + file_properties: Default::default(), } } } impl TransactionParams { fn reset(&mut self) { - self.condition_code = ConditionCode::NoError; - self.delivery_code = DeliveryCode::Incomplete; + self.tstate.condition_code = ConditionCode::NoError; + self.tstate.delivery_code = DeliveryCode::Incomplete; } } @@ -132,7 +157,7 @@ impl DestinationHandler { id: id.into(), step: TransactionStep::Idle, state: State::Idle, - transaction_params: Default::default(), + tparams: Default::default(), packets_to_send_ctx: Default::default(), //cfdp_user, } @@ -182,25 +207,23 @@ impl DestinationHandler { let directive = self.packets_to_send_ctx.directive.unwrap(); let written_size = match directive { FileDirectiveType::FinishedPdu => { - let pdu_header = PduHeader::new_no_file_data(self.transaction_params.pdu_conf, 0); - let finished_pdu = if self.transaction_params.condition_code - == ConditionCode::NoError - || self.transaction_params.condition_code - == ConditionCode::UnsupportedChecksumType + let pdu_header = PduHeader::new_no_file_data(self.tparams.pdu_conf, 0); + let finished_pdu = if self.tparams.tstate.condition_code == ConditionCode::NoError + || self.tparams.tstate.condition_code == ConditionCode::UnsupportedChecksumType { FinishedPdu::new_default( pdu_header, - self.transaction_params.delivery_code, - self.transaction_params.file_status, + self.tparams.tstate.delivery_code, + self.tparams.tstate.file_status, ) } else { // TODO: Are there cases where this ID is actually the source entity ID? let entity_id = EntityIdTlv::new(self.id); FinishedPdu::new_with_error( pdu_header, - self.transaction_params.condition_code, - self.transaction_params.delivery_code, - self.transaction_params.file_status, + self.tparams.tstate.condition_code, + self.tparams.tstate.delivery_code, + self.tparams.tstate.file_status, entity_id, ) }; @@ -245,43 +268,42 @@ impl DestinationHandler { return Err(DestError::RecvdMetadataButIsBusy); } let metadata_pdu = MetadataPdu::from_bytes(raw_packet)?; - self.transaction_params.reset(); - self.transaction_params.metadata_params = *metadata_pdu.metadata_params(); + self.tparams.reset(); + self.tparams.tstate.metadata_params = *metadata_pdu.metadata_params(); let src_name = metadata_pdu.src_file_name(); if src_name.is_empty() { return Err(DestError::EmptySrcFileField); } - self.transaction_params.file_properties.src_file_name[..src_name.len_value()] + self.tparams.file_properties.src_file_name[..src_name.len_value()] .copy_from_slice(src_name.value()); - self.transaction_params.file_properties.src_file_name_len = src_name.len_value(); + self.tparams.file_properties.src_file_name_len = src_name.len_value(); let dest_name = metadata_pdu.dest_file_name(); if dest_name.is_empty() { return Err(DestError::EmptyDestFileField); } - self.transaction_params.file_properties.dest_file_name[..dest_name.len_value()] + self.tparams.file_properties.dest_file_name[..dest_name.len_value()] .copy_from_slice(dest_name.value()); - self.transaction_params.file_properties.dest_file_name_len = dest_name.len_value(); - self.transaction_params.pdu_conf = *metadata_pdu.pdu_header().common_pdu_conf(); - self.transaction_params.msgs_to_user_size = 0; - if metadata_pdu.pdu_header().common_pdu_conf().trans_mode - == TransmissionMode::Unacknowledged - { - self.state = State::BusyClass1Nacked; - } else { - self.state = State::BusyClass2Acked; - } + self.tparams.file_properties.dest_file_name_len = dest_name.len_value(); + self.tparams.pdu_conf = *metadata_pdu.pdu_header().common_pdu_conf(); + self.tparams.msgs_to_user_size = 0; if metadata_pdu.options().is_some() { for option_tlv in metadata_pdu.options_iter().unwrap() { if option_tlv.is_standard_tlv() && option_tlv.tlv_type().unwrap() == TlvType::MsgToUser { - self.transaction_params + self.tparams .msgs_to_user_buf .copy_from_slice(option_tlv.raw_data().unwrap()); - self.transaction_params.msgs_to_user_size += option_tlv.len_full(); + self.tparams.msgs_to_user_size += option_tlv.len_full(); } } } + if self.tparams.pdu_conf.trans_mode == TransmissionMode::Unacknowledged { + self.state = State::BusyClass1Nacked; + } else { + self.state = State::BusyClass2Acked; + } + self.step = TransactionStep::TransactionStart; Ok(()) } @@ -292,7 +314,7 @@ impl DestinationHandler { let fd_pdu = FileDataPdu::from_bytes(raw_packet)?; let mut dest_file = File::options() .write(true) - .open(&self.transaction_params.file_properties.dest_path_buf)?; + .open(&self.tparams.file_properties.dest_path_buf)?; dest_file.seek(SeekFrom::Start(fd_pdu.offset()))?; dest_file.write_all(fd_pdu.file_data())?; Ok(()) @@ -306,12 +328,12 @@ impl DestinationHandler { let checksum = eof_pdu.file_checksum(); // For a standard disk based file system, which is assumed to be used for now, the file // will always be retained. This might change in the future. - self.transaction_params.file_status = FileStatus::Retained; + self.tparams.tstate.file_status = FileStatus::Retained; if self.checksum_check(checksum)? { - self.transaction_params.condition_code = ConditionCode::NoError; - self.transaction_params.delivery_code = DeliveryCode::Complete; + self.tparams.tstate.condition_code = ConditionCode::NoError; + self.tparams.tstate.delivery_code = DeliveryCode::Complete; } else { - self.transaction_params.condition_code = ConditionCode::FileChecksumFailure; + self.tparams.tstate.condition_code = ConditionCode::FileChecksumFailure; } if self.state == State::BusyClass1Nacked { self.step = TransactionStep::TransferCompletion; @@ -327,14 +349,14 @@ impl DestinationHandler { fn checksum_check(&mut self, expected_checksum: u32) -> Result { let mut digest = CRC_32.digest(); - let file_to_check = File::open(&self.transaction_params.file_properties.dest_path_buf)?; + let file_to_check = File::open(&self.tparams.file_properties.dest_path_buf)?; let mut buf_reader = BufReader::new(file_to_check); loop { - let bytes_read = buf_reader.read(&mut self.transaction_params.cksum_buf)?; + let bytes_read = buf_reader.read(&mut self.tparams.cksum_buf)?; if bytes_read == 0 { break; } - digest.update(&self.transaction_params.cksum_buf[0..bytes_read]); + digest.update(&self.tparams.cksum_buf[0..bytes_read]); } if digest.finalize() == expected_checksum { return Ok(true); @@ -347,14 +369,16 @@ impl DestinationHandler { if self.step == TransactionStep::TransactionStart { self.transaction_start(cfdp_user)?; } - if self.step == TransactionStep::ReceivingFileDataPdus { - todo!("advance the fsm if everything is finished") + if self.step == TransactionStep::ReceivingFileDataPdus + && self.tparams.tstate.progress == self.tparams.file_size() + { + self.step = TransactionStep::TransferCompletion; } if self.step == TransactionStep::TransferCompletion { - self.transfer_completion()?; + self.transfer_completion(cfdp_user)?; } if self.step == TransactionStep::SendingAckPdu { - todo!(); + todo!("no support for acknowledged mode yet"); } if self.step == TransactionStep::SendingFinishedPdu { self.reset(); @@ -376,28 +400,25 @@ impl DestinationHandler { fn transaction_start(&mut self, cfdp_user: &mut impl CfdpUser) -> Result<(), DestError> { let dest_name = from_utf8( - &self.transaction_params.file_properties.dest_file_name - [..self.transaction_params.file_properties.dest_file_name_len], + &self.tparams.file_properties.dest_file_name + [..self.tparams.file_properties.dest_file_name_len], )?; let dest_path = Path::new(dest_name); - self.transaction_params.file_properties.dest_path_buf = dest_path.to_path_buf(); - let source_id = self.transaction_params.pdu_conf.source_id(); - let id = TransactionId::new( - source_id, - self.transaction_params.pdu_conf.transaction_seq_num, - ); + self.tparams.file_properties.dest_path_buf = dest_path.to_path_buf(); + let source_id = self.tparams.pdu_conf.source_id(); + let id = TransactionId::new(source_id, self.tparams.pdu_conf.transaction_seq_num); let src_name = from_utf8( - &self.transaction_params.file_properties.src_file_name - [0..self.transaction_params.file_properties.src_file_name_len], + &self.tparams.file_properties.src_file_name + [0..self.tparams.file_properties.src_file_name_len], )?; let mut msgs_to_user = SmallVec::<[MsgToUserTlv<'_>; 16]>::new(); let mut num_msgs_to_user = 0; - if self.transaction_params.msgs_to_user_size > 0 { + if self.tparams.msgs_to_user_size > 0 { let mut index = 0; - while index < self.transaction_params.msgs_to_user_size { + while index < self.tparams.msgs_to_user_size { // This should never panic as the validity of the options was checked beforehand. let msgs_to_user_tlv = - MsgToUserTlv::from_bytes(&self.transaction_params.msgs_to_user_buf[index..]) + MsgToUserTlv::from_bytes(&self.tparams.msgs_to_user_buf[index..]) .expect("message to user creation failed unexpectedly"); msgs_to_user.push(msgs_to_user_tlv); index += msgs_to_user_tlv.len_full(); @@ -407,54 +428,64 @@ impl DestinationHandler { let metadata_recvd_params = MetadataReceivedParams { id, source_id, - file_size: self.transaction_params.metadata_params.file_size, + file_size: self.tparams.tstate.metadata_params.file_size, src_file_name: src_name, dest_file_name: dest_name, msgs_to_user: &msgs_to_user[..num_msgs_to_user], }; - self.transaction_params.transaction_id = Some(id); + self.tparams.tstate.transaction_id = Some(id); cfdp_user.metadata_recvd_indication(&metadata_recvd_params); - let metadata = metadata(dest_path)?; - if metadata.is_dir() { - // Create new destination path by concatenating the last part of the source source - // name and the destination folder. For example, for a source file of /tmp/hello.txt - // and a destination name of /home/test, the resulting file name should be - // /home/test/hello.txt - let source_path = Path::new(from_utf8( - &self.transaction_params.file_properties.src_file_name - [..self.transaction_params.file_properties.src_file_name_len], - )?); + if dest_path.exists() { + let dest_metadata = metadata(dest_path)?; + if dest_metadata.is_dir() { + // Create new destination path by concatenating the last part of the source source + // name and the destination folder. For example, for a source file of /tmp/hello.txt + // and a destination name of /home/test, the resulting file name should be + // /home/test/hello.txt + let source_path = Path::new(from_utf8( + &self.tparams.file_properties.src_file_name + [..self.tparams.file_properties.src_file_name_len], + )?); - let source_name = source_path.file_name(); - if source_name.is_none() { - return Err(DestError::PathConcatError); + let source_name = source_path.file_name(); + if source_name.is_none() { + return Err(DestError::PathConcatError); + } + let source_name = source_name.unwrap(); + self.tparams.file_properties.dest_path_buf.push(source_name); } - let source_name = source_name.unwrap(); - self.transaction_params - .file_properties - .dest_path_buf - .push(source_name); } // This function does exactly what we require: Create a new file if it does not exist yet // and trucate an existing one. - File::create(&self.transaction_params.file_properties.dest_path_buf)?; + File::create(&self.tparams.file_properties.dest_path_buf)?; + self.step = TransactionStep::ReceivingFileDataPdus; Ok(()) } - fn transfer_completion(&mut self) -> Result<(), DestError> { + fn transfer_completion(&mut self, cfdp_user: &mut impl CfdpUser) -> Result<(), DestError> { // This function should never be called with metadata parameters not set - if self.transaction_params.metadata_params.closure_requested { + if self.tparams.metadata_params().closure_requested { self.prepare_finished_pdu()?; + self.step = TransactionStep::SendingFinishedPdu; + } else { + self.step = TransactionStep::Idle; } - todo!("user indication"); + let transaction_finished_params = TransactionFinishedParams { + id: self.tparams.tstate.transaction_id.unwrap(), + condition_code: self.tparams.tstate.condition_code, + delivery_code: self.tparams.tstate.delivery_code, + file_status: self.tparams.tstate.file_status, + }; + cfdp_user.transaction_finished_indication(&transaction_finished_params); + Ok(()) } fn reset(&mut self) { self.step = TransactionStep::Idle; self.state = State::Idle; self.packets_to_send_ctx.packet_available = false; - self.transaction_params.reset(); + self.tparams.reset(); } fn prepare_finished_pdu(&mut self) -> Result<(), DestError> { @@ -467,6 +498,7 @@ impl DestinationHandler { #[cfg(test)] mod tests { + use std::env::temp_dir; #[allow(unused_imports)] use std::println; @@ -558,34 +590,50 @@ mod tests { assert_eq!(handler.step(), TransactionStep::Idle); } + fn init_full_filenames() -> (PathBuf, PathBuf) { + let mut file_path = temp_dir(); + let mut src_path = file_path.clone(); + src_path.push(SRC_NAME); + file_path.push(DEST_NAME); + (src_path, file_path) + } + #[test] fn test_basic() { let dest_handler = DestinationHandler::new(LOCAL_ID); init_check(&dest_handler); } - #[test] - fn test_empty_file_transfer() { - let mut buf: [u8; 512] = [0; 512]; - let mut test_user = TestCfdpUser::default(); - let mut dest_handler = DestinationHandler::new(LOCAL_ID); - init_check(&dest_handler); - - let seq_num = UbfU16::new(0); + fn create_pdu_header(seq_num: impl Into) -> PduHeader { let mut pdu_conf = CommonPduConfig::new_with_byte_fields(REMOTE_ID, LOCAL_ID, seq_num).unwrap(); pdu_conf.trans_mode = TransmissionMode::Unacknowledged; - let pdu_header = PduHeader::new_no_file_data(pdu_conf, 0); - let metadata_params = MetadataGenericParams::new(false, ChecksumType::Crc32, 0); - let metadata_pdu = MetadataPdu::new( - pdu_header, + PduHeader::new_no_file_data(pdu_conf, 0) + } + + fn create_metadata_pdu<'filename>( + pdu_header: &PduHeader, + src_name: &'filename Path, + dest_name: &'filename Path, + file_size: u64, + ) -> MetadataPdu<'filename, 'filename, 'static> { + let metadata_params = MetadataGenericParams::new(false, ChecksumType::Crc32, file_size); + MetadataPdu::new( + *pdu_header, metadata_params, - Lv::new_from_str(SRC_NAME).unwrap(), - Lv::new_from_str(DEST_NAME).unwrap(), + Lv::new_from_str(src_name.as_os_str().to_str().unwrap()).unwrap(), + Lv::new_from_str(dest_name.as_os_str().to_str().unwrap()).unwrap(), None, - ); + ) + } + + fn insert_metadata_pdu( + metadata_pdu: &MetadataPdu, + buf: &mut [u8], + dest_handler: &mut DestinationHandler, + ) { let written_len = metadata_pdu - .write_to_bytes(&mut buf) + .write_to_bytes(buf) .expect("writing metadata PDU failed"); let packet_info = PacketInfo::new(&buf[..written_len]).expect("generating packet info failed"); @@ -593,7 +641,64 @@ mod tests { if let Err(e) = insert_result { panic!("insert result error: {e}"); } + } + + #[test] + fn test_empty_file_transfer() { + let (src_name, dest_name) = init_full_filenames(); + println!("src name: {src_name:?}, dest name: {dest_name:?}"); + let mut buf: [u8; 512] = [0; 512]; + let mut test_user = TestCfdpUser::default(); + let mut dest_handler = DestinationHandler::new(LOCAL_ID); + init_check(&dest_handler); + + let seq_num = UbfU16::new(0); + let pdu_header = create_pdu_header(seq_num); + let metadata_pdu = + create_metadata_pdu(&pdu_header, src_name.as_path(), dest_name.as_path(), 0); + insert_metadata_pdu(&metadata_pdu, &mut buf, &mut dest_handler); let result = dest_handler.state_machine(&mut test_user); - assert!(result.is_ok()); + if let Err(e) = result { + panic!("dest handler fsm error: {e}"); + } + assert_ne!(dest_handler.state(), State::Idle); + assert_eq!(dest_handler.step(), TransactionStep::Idle); + } + + #[test] + fn test_small_file_transfer() { + let (src_name, dest_name) = init_full_filenames(); + let file_data = "Hello World!".as_bytes(); + let mut buf: [u8; 512] = [0; 512]; + let mut test_user = TestCfdpUser::default(); + let mut dest_handler = DestinationHandler::new(LOCAL_ID); + init_check(&dest_handler); + + let seq_num = UbfU16::new(0); + let pdu_header = create_pdu_header(seq_num); + let metadata_pdu = create_metadata_pdu( + &pdu_header, + src_name.as_path(), + dest_name.as_path(), + file_data.len() as u64, + ); + insert_metadata_pdu(&metadata_pdu, &mut buf, &mut dest_handler); + let result = dest_handler.state_machine(&mut test_user); + if let Err(e) = result { + panic!("dest handler fsm error: {e}"); + } + assert_ne!(dest_handler.state(), State::Idle); + assert_eq!(dest_handler.step(), TransactionStep::ReceivingFileDataPdus); + + let offset = 0; + let filedata_pdu = FileDataPdu::new_no_seg_metadata(pdu_header, offset, file_data); + filedata_pdu + .write_to_bytes(&mut buf) + .expect("writing file data PDU failed"); + let packet_info = PacketInfo::new(&buf).expect("creating packet info failed"); + let result = dest_handler.insert_packet(&packet_info); + if let Err(e) = result { + panic!("destination handler packet insertion error: {e}"); + } } } From 309ceda5a50cb692036ef025b0d8707b813b9ae4 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 1 Sep 2023 19:01:28 +0200 Subject: [PATCH 29/45] added the full set of indication tests --- satrs-core/src/cfdp/dest.rs | 52 ++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index 29c6b01..c923af4 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -502,6 +502,7 @@ mod tests { #[allow(unused_imports)] use std::println; + use alloc::string::String; use spacepackets::{ cfdp::{lv::Lv, ChecksumType}, util::{UbfU16, UnsignedByteFieldU16}, @@ -518,11 +519,14 @@ mod tests { #[derive(Default)] struct TestCfdpUser { next_expected_seq_num: u64, + expected_full_src_name: String, + expected_full_dest_name: String, + expected_file_size: usize, } impl TestCfdpUser { fn generic_id_check(&self, id: &crate::cfdp::TransactionId) { - assert_eq!(id.source_id, REMOTE_ID.into()); + assert_eq!(id.source_id, LOCAL_ID.into()); assert_eq!(id.seq_num().value(), self.next_expected_seq_num); } } @@ -545,8 +549,20 @@ mod tests { fn metadata_recvd_indication( &mut self, - _md_recvd_params: &crate::cfdp::user::MetadataReceivedParams, + md_recvd_params: &crate::cfdp::user::MetadataReceivedParams, ) { + self.generic_id_check(&md_recvd_params.id); + assert_eq!( + String::from(md_recvd_params.src_file_name), + self.expected_full_src_name + ); + assert_eq!( + String::from(md_recvd_params.dest_file_name), + self.expected_full_dest_name + ); + assert_eq!(md_recvd_params.msgs_to_user.len(), 0); + assert_eq!(md_recvd_params.source_id, LOCAL_ID.into()); + assert_eq!(md_recvd_params.file_size as usize, self.expected_file_size); } fn file_segment_recvd_indication( @@ -562,6 +578,7 @@ mod tests { _id: &crate::cfdp::TransactionId, _condition_code: ConditionCode, ) { + panic!("unexpected suspended indication"); } fn resumed_indication(&mut self, _id: &crate::cfdp::TransactionId, _progresss: u64) {} @@ -572,6 +589,7 @@ mod tests { _condition_code: ConditionCode, _progress: u64, ) { + panic!("unexpected fault indication"); } fn abandoned_indication( @@ -580,9 +598,12 @@ mod tests { _condition_code: ConditionCode, _progress: u64, ) { + panic!("unexpected abandoned indication"); } - fn eof_recvd_indication(&mut self, _id: &crate::cfdp::TransactionId) {} + fn eof_recvd_indication(&mut self, id: &crate::cfdp::TransactionId) { + self.generic_id_check(id); + } } fn init_check(handler: &DestinationHandler) { @@ -600,13 +621,13 @@ mod tests { #[test] fn test_basic() { - let dest_handler = DestinationHandler::new(LOCAL_ID); + let dest_handler = DestinationHandler::new(REMOTE_ID); init_check(&dest_handler); } fn create_pdu_header(seq_num: impl Into) -> PduHeader { let mut pdu_conf = - CommonPduConfig::new_with_byte_fields(REMOTE_ID, LOCAL_ID, seq_num).unwrap(); + CommonPduConfig::new_with_byte_fields(LOCAL_ID, REMOTE_ID, seq_num).unwrap(); pdu_conf.trans_mode = TransmissionMode::Unacknowledged; PduHeader::new_no_file_data(pdu_conf, 0) } @@ -646,10 +667,15 @@ mod tests { #[test] fn test_empty_file_transfer() { let (src_name, dest_name) = init_full_filenames(); - println!("src name: {src_name:?}, dest name: {dest_name:?}"); let mut buf: [u8; 512] = [0; 512]; - let mut test_user = TestCfdpUser::default(); - let mut dest_handler = DestinationHandler::new(LOCAL_ID); + let mut test_user = TestCfdpUser { + next_expected_seq_num: 0, + expected_full_src_name: src_name.to_string_lossy().into(), + expected_full_dest_name: dest_name.to_string_lossy().into(), + expected_file_size: 0, + }; + // We treat the destination handler like it is a remote entity. + let mut dest_handler = DestinationHandler::new(REMOTE_ID); init_check(&dest_handler); let seq_num = UbfU16::new(0); @@ -670,8 +696,14 @@ mod tests { let (src_name, dest_name) = init_full_filenames(); let file_data = "Hello World!".as_bytes(); let mut buf: [u8; 512] = [0; 512]; - let mut test_user = TestCfdpUser::default(); - let mut dest_handler = DestinationHandler::new(LOCAL_ID); + let mut test_user = TestCfdpUser { + next_expected_seq_num: 0, + expected_full_src_name: src_name.to_string_lossy().into(), + expected_full_dest_name: dest_name.to_string_lossy().into(), + expected_file_size: file_data.len(), + }; + // We treat the destination handler like it is a remote entity. + let mut dest_handler = DestinationHandler::new(REMOTE_ID); init_check(&dest_handler); let seq_num = UbfU16::new(0); From 1a38de760ab0dced81dafde1223eca50f8407201 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 1 Sep 2023 19:02:21 +0200 Subject: [PATCH 30/45] added TODO --- satrs-core/src/cfdp/dest.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index c923af4..a15b32d 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -732,5 +732,7 @@ mod tests { if let Err(e) = result { panic!("destination handler packet insertion error: {e}"); } + + // TODO: Send EOF PDU and verify completion of transaction } } From 40c8c36af356c5c03fbeccb6ae84954397be6aea Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 2 Sep 2023 23:52:50 +0200 Subject: [PATCH 31/45] added eof creation --- satrs-core/src/cfdp/dest.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index a15b32d..8b70660 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -733,6 +733,14 @@ mod tests { panic!("destination handler packet insertion error: {e}"); } - // TODO: Send EOF PDU and verify completion of transaction + let mut digest = CRC_32.digest(); + digest.update(file_data); + let crc32 = digest.finalize(); + let eof_pdu = EofPdu::new_no_error(pdu_header, crc32, file_data.len() as u64); + let result = eof_pdu.write_to_bytes(&mut buf); + assert!(result.is_ok()); + let packet_info = PacketInfo::new(&buf).expect("generating packet info failed"); + let result = dest_handler.insert_packet(&packet_info); + assert!(result.is_ok()); } } From dca7449edd11fbb370bfb5dde95ebf4fded6c11c Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sun, 3 Sep 2023 20:48:39 +0200 Subject: [PATCH 32/45] everything seems to work now --- satrs-core/src/cfdp/dest.rs | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index 8b70660..f38324e 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -335,6 +335,9 @@ impl DestinationHandler { } else { self.tparams.tstate.condition_code = ConditionCode::FileChecksumFailure; } + // TODO: Check progress, and implement transfer completion timer as specified in the + // standard. This timer protects against out of order arrival of packets. + if self.tparams.tstate.progress != self.tparams.file_size() {} if self.state == State::BusyClass1Nacked { self.step = TransactionStep::TransferCompletion; } else { @@ -369,11 +372,6 @@ impl DestinationHandler { if self.step == TransactionStep::TransactionStart { self.transaction_start(cfdp_user)?; } - if self.step == TransactionStep::ReceivingFileDataPdus - && self.tparams.tstate.progress == self.tparams.file_size() - { - self.step = TransactionStep::TransferCompletion; - } if self.step == TransactionStep::TransferCompletion { self.transfer_completion(cfdp_user)?; } @@ -382,7 +380,6 @@ impl DestinationHandler { } if self.step == TransactionStep::SendingFinishedPdu { self.reset(); - return Ok(()); } Ok(()) } @@ -464,13 +461,6 @@ impl DestinationHandler { } fn transfer_completion(&mut self, cfdp_user: &mut impl CfdpUser) -> Result<(), DestError> { - // This function should never be called with metadata parameters not set - if self.tparams.metadata_params().closure_requested { - self.prepare_finished_pdu()?; - self.step = TransactionStep::SendingFinishedPdu; - } else { - self.step = TransactionStep::Idle; - } let transaction_finished_params = TransactionFinishedParams { id: self.tparams.tstate.transaction_id.unwrap(), condition_code: self.tparams.tstate.condition_code, @@ -478,6 +468,15 @@ impl DestinationHandler { file_status: self.tparams.tstate.file_status, }; cfdp_user.transaction_finished_indication(&transaction_finished_params); + // This function should never be called with metadata parameters not set + if self.tparams.metadata_params().closure_requested { + self.prepare_finished_pdu()?; + self.step = TransactionStep::SendingFinishedPdu; + } else { + self.reset(); + self.state = State::Idle; + self.step = TransactionStep::Idle; + } Ok(()) } @@ -732,6 +731,8 @@ mod tests { if let Err(e) = result { panic!("destination handler packet insertion error: {e}"); } + let result = dest_handler.state_machine(&mut test_user); + assert!(result.is_ok()); let mut digest = CRC_32.digest(); digest.update(file_data); @@ -742,5 +743,10 @@ mod tests { let packet_info = PacketInfo::new(&buf).expect("generating packet info failed"); let result = dest_handler.insert_packet(&packet_info); assert!(result.is_ok()); + + let result = dest_handler.state_machine(&mut test_user); + assert!(result.is_ok()); + assert_eq!(dest_handler.state(), State::Idle); + assert_eq!(dest_handler.step(), TransactionStep::Idle); } } From 3f3a7e8efcd34806778f6b141a850062df743e89 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sun, 3 Sep 2023 21:05:18 +0200 Subject: [PATCH 33/45] cleaner file handling --- satrs-core/src/cfdp/dest.rs | 53 ++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index f38324e..7c7d68f 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -497,9 +497,9 @@ impl DestinationHandler { #[cfg(test)] mod tests { - use std::env::temp_dir; #[allow(unused_imports)] use std::println; + use std::{env::temp_dir, fs}; use alloc::string::String; use spacepackets::{ @@ -663,9 +663,27 @@ mod tests { } } + fn insert_eof_pdu( + file_data: &[u8], + pdu_header: &PduHeader, + buf: &mut [u8], + dest_handler: &mut DestinationHandler, + ) { + let mut digest = CRC_32.digest(); + digest.update(file_data); + let crc32 = digest.finalize(); + let eof_pdu = EofPdu::new_no_error(*pdu_header, crc32, file_data.len() as u64); + let result = eof_pdu.write_to_bytes(buf); + assert!(result.is_ok()); + let packet_info = PacketInfo::new(&buf).expect("generating packet info failed"); + let result = dest_handler.insert_packet(&packet_info); + assert!(result.is_ok()); + } + #[test] fn test_empty_file_transfer() { let (src_name, dest_name) = init_full_filenames(); + assert!(!Path::exists(&dest_name)); let mut buf: [u8; 512] = [0; 512]; let mut test_user = TestCfdpUser { next_expected_seq_num: 0, @@ -687,13 +705,25 @@ mod tests { panic!("dest handler fsm error: {e}"); } assert_ne!(dest_handler.state(), State::Idle); + assert_eq!(dest_handler.step(), TransactionStep::ReceivingFileDataPdus); + + insert_eof_pdu(&[], &pdu_header, &mut buf, &mut dest_handler); + let result = dest_handler.state_machine(&mut test_user); + assert!(result.is_ok()); + assert_eq!(dest_handler.state(), State::Idle); assert_eq!(dest_handler.step(), TransactionStep::Idle); + assert!(Path::exists(&dest_name)); + let read_content = fs::read(&dest_name).expect("reading back string failed"); + assert_eq!(read_content.len(), 0); + assert!(fs::remove_file(dest_name).is_ok()); } #[test] fn test_small_file_transfer() { let (src_name, dest_name) = init_full_filenames(); - let file_data = "Hello World!".as_bytes(); + assert!(!Path::exists(&dest_name)); + let file_data_str = "Hello World!"; + let file_data = file_data_str.as_bytes(); let mut buf: [u8; 512] = [0; 512]; let mut test_user = TestCfdpUser { next_expected_seq_num: 0, @@ -734,19 +764,18 @@ mod tests { let result = dest_handler.state_machine(&mut test_user); assert!(result.is_ok()); - let mut digest = CRC_32.digest(); - digest.update(file_data); - let crc32 = digest.finalize(); - let eof_pdu = EofPdu::new_no_error(pdu_header, crc32, file_data.len() as u64); - let result = eof_pdu.write_to_bytes(&mut buf); - assert!(result.is_ok()); - let packet_info = PacketInfo::new(&buf).expect("generating packet info failed"); - let result = dest_handler.insert_packet(&packet_info); - assert!(result.is_ok()); - + insert_eof_pdu(file_data, &pdu_header, &mut buf, &mut dest_handler); let result = dest_handler.state_machine(&mut test_user); assert!(result.is_ok()); assert_eq!(dest_handler.state(), State::Idle); assert_eq!(dest_handler.step(), TransactionStep::Idle); + + assert!(Path::exists(&dest_name)); + let read_content = fs::read_to_string(&dest_name).expect("reading back string failed"); + assert_eq!(read_content, file_data_str); + assert!(fs::remove_file(dest_name).is_ok()); } + + #[test] + fn test_segmented_file_transfer() {} } From 778f30ef1be5834c8e20adb30c26c7c370dd28a4 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sun, 3 Sep 2023 21:06:29 +0200 Subject: [PATCH 34/45] somewhat obfuscate the filename --- satrs-core/src/cfdp/dest.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index 7c7d68f..25c0146 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -512,8 +512,8 @@ mod tests { const LOCAL_ID: UnsignedByteFieldU16 = UnsignedByteFieldU16::new(1); const REMOTE_ID: UnsignedByteFieldU16 = UnsignedByteFieldU16::new(2); - const SRC_NAME: &str = "source-file.txt"; - const DEST_NAME: &str = "dest-file.txt"; + const SRC_NAME: &str = "__cfdp__source-file.txt"; + const DEST_NAME: &str = "__cfdp__dest-file.txt"; #[derive(Default)] struct TestCfdpUser { From 73830afcb7b7fa2ef4653c64037b01901f6901e7 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sun, 3 Sep 2023 21:18:35 +0200 Subject: [PATCH 35/45] now the tests work concurrently --- satrs-core/src/cfdp/dest.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index 25c0146..4404a3d 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -497,11 +497,12 @@ impl DestinationHandler { #[cfg(test)] mod tests { + use core::sync::atomic::{AtomicU8, Ordering}; #[allow(unused_imports)] use std::println; use std::{env::temp_dir, fs}; - use alloc::string::String; + use alloc::{string::String, format}; use spacepackets::{ cfdp::{lv::Lv, ChecksumType}, util::{UbfU16, UnsignedByteFieldU16}, @@ -512,8 +513,10 @@ mod tests { const LOCAL_ID: UnsignedByteFieldU16 = UnsignedByteFieldU16::new(1); const REMOTE_ID: UnsignedByteFieldU16 = UnsignedByteFieldU16::new(2); - const SRC_NAME: &str = "__cfdp__source-file.txt"; - const DEST_NAME: &str = "__cfdp__dest-file.txt"; + const SRC_NAME: &str = "__cfdp__source-file"; + const DEST_NAME: &str = "__cfdp__dest-file"; + + static ATOMIC_COUNTER: AtomicU8 = AtomicU8::new(0); #[derive(Default)] struct TestCfdpUser { @@ -613,8 +616,13 @@ mod tests { fn init_full_filenames() -> (PathBuf, PathBuf) { let mut file_path = temp_dir(); let mut src_path = file_path.clone(); - src_path.push(SRC_NAME); - file_path.push(DEST_NAME); + // Atomic counter used to allow concurrent tests. + let unique_counter = ATOMIC_COUNTER.fetch_add(1, Ordering::Relaxed); + // Create unique test filenames. + let src_name_unique = format!("{SRC_NAME}{}.txt", unique_counter); + let dest_name_unique = format!("{DEST_NAME}{}.txt", unique_counter); + src_path.push(src_name_unique); + file_path.push(dest_name_unique); (src_path, file_path) } From e142215065d16da26f4eda715646527d3fed9f28 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 6 Sep 2023 21:56:12 +0200 Subject: [PATCH 36/45] lets try this out --- README.md | 2 ++ misc/satrs-logo.png | Bin 0 -> 63495 bytes 2 files changed, 2 insertions(+) create mode 100644 misc/satrs-logo.png diff --git a/README.md b/README.md index 75a6b0d..a3f5bad 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +

+ sat-rs ========= diff --git a/misc/satrs-logo.png b/misc/satrs-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ffca2dbc6c353a27e1f54ba71a143d6c780edb3e GIT binary patch literal 63495 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 literal 0 HcmV?d00001 From 2a72967c265e5c5be2aaf88455cf9465b07fc796 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sun, 10 Sep 2023 19:49:31 +0200 Subject: [PATCH 37/45] added segmented file transfer unittest --- satrs-core/Cargo.toml | 1 + satrs-core/src/cfdp/dest.rs | 87 ++++++++++++++++++++++++++++++++++++- 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/satrs-core/Cargo.toml b/satrs-core/Cargo.toml index 7d7aeba..284b84a 100644 --- a/satrs-core/Cargo.toml +++ b/satrs-core/Cargo.toml @@ -79,6 +79,7 @@ serde = "1" zerocopy = "0.7" once_cell = "1.13" serde_json = "1" +rand = "0.8" [dev-dependencies.postcard] version = "1" diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index 4404a3d..93fb5e7 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -502,7 +502,8 @@ mod tests { use std::println; use std::{env::temp_dir, fs}; - use alloc::{string::String, format}; + use alloc::{format, string::String}; + use rand::Rng; use spacepackets::{ cfdp::{lv::Lv, ChecksumType}, util::{UbfU16, UnsignedByteFieldU16}, @@ -785,5 +786,87 @@ mod tests { } #[test] - fn test_segmented_file_transfer() {} + fn test_segmented_file_transfer() { + let (src_name, dest_name) = init_full_filenames(); + assert!(!Path::exists(&dest_name)); + let mut rng = rand::thread_rng(); + let mut random_data = [0u8; 512]; + rng.fill(&mut random_data); + let mut buf: [u8; 512] = [0; 512]; + let mut test_user = TestCfdpUser { + next_expected_seq_num: 0, + expected_full_src_name: src_name.to_string_lossy().into(), + expected_full_dest_name: dest_name.to_string_lossy().into(), + expected_file_size: random_data.len(), + }; + + // We treat the destination handler like it is a remote entity. + let mut dest_handler = DestinationHandler::new(REMOTE_ID); + init_check(&dest_handler); + + let seq_num = UbfU16::new(0); + let pdu_header = create_pdu_header(seq_num); + let metadata_pdu = create_metadata_pdu( + &pdu_header, + src_name.as_path(), + dest_name.as_path(), + random_data.len() as u64, + ); + insert_metadata_pdu(&metadata_pdu, &mut buf, &mut dest_handler); + let result = dest_handler.state_machine(&mut test_user); + if let Err(e) = result { + panic!("dest handler fsm error: {e}"); + } + assert_ne!(dest_handler.state(), State::Idle); + assert_eq!(dest_handler.step(), TransactionStep::ReceivingFileDataPdus); + + // First file data PDU + let mut offset: usize = 0; + let segment_len = 256; + let filedata_pdu = FileDataPdu::new_no_seg_metadata( + pdu_header, + offset as u64, + &random_data[0..segment_len], + ); + filedata_pdu + .write_to_bytes(&mut buf) + .expect("writing file data PDU failed"); + let packet_info = PacketInfo::new(&buf).expect("creating packet info failed"); + let result = dest_handler.insert_packet(&packet_info); + if let Err(e) = result { + panic!("destination handler packet insertion error: {e}"); + } + let result = dest_handler.state_machine(&mut test_user); + assert!(result.is_ok()); + + // Second file data PDU + offset += segment_len; + let filedata_pdu = FileDataPdu::new_no_seg_metadata( + pdu_header, + offset as u64, + &random_data[segment_len..], + ); + filedata_pdu + .write_to_bytes(&mut buf) + .expect("writing file data PDU failed"); + let packet_info = PacketInfo::new(&buf).expect("creating packet info failed"); + let result = dest_handler.insert_packet(&packet_info); + if let Err(e) = result { + panic!("destination handler packet insertion error: {e}"); + } + let result = dest_handler.state_machine(&mut test_user); + assert!(result.is_ok()); + + insert_eof_pdu(&random_data, &pdu_header, &mut buf, &mut dest_handler); + let result = dest_handler.state_machine(&mut test_user); + assert!(result.is_ok()); + assert_eq!(dest_handler.state(), State::Idle); + assert_eq!(dest_handler.step(), TransactionStep::Idle); + + // Clean up + assert!(Path::exists(&dest_name)); + let read_content = fs::read(&dest_name).expect("reading back string failed"); + assert_eq!(read_content, random_data); + assert!(fs::remove_file(dest_name).is_ok()); + } } From 7aecc94fdafc9126b4880543f7b56b6068ccd448 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sun, 10 Sep 2023 20:24:19 +0200 Subject: [PATCH 38/45] addeed first remote config type --- satrs-core/src/cfdp/dest.rs | 1 - satrs-core/src/cfdp/mod.rs | 86 ++++++++++++++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index 93fb5e7..d7ea7cc 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -159,7 +159,6 @@ impl DestinationHandler { state: State::Idle, tparams: Default::default(), packets_to_send_ctx: Default::default(), - //cfdp_user, } } diff --git a/satrs-core/src/cfdp/mod.rs b/satrs-core/src/cfdp/mod.rs index 4e74d24..22fe0aa 100644 --- a/satrs-core/src/cfdp/mod.rs +++ b/satrs-core/src/cfdp/mod.rs @@ -2,7 +2,7 @@ use crc::{Crc, CRC_32_CKSUM}; use spacepackets::{ cfdp::{ pdu::{FileDirectiveType, PduError, PduHeader}, - PduType, + ChecksumType, PduType, TransmissionMode, }, util::UnsignedByteField, }; @@ -14,6 +14,16 @@ use serde::{Deserialize, Serialize}; pub mod dest; pub mod user; +#[derive(Debug)] +pub struct RemoteConfig { + pub entity_id: UnsignedByteField, + pub max_file_segment_len: usize, + pub closure_requeted_by_default: bool, + pub crc_on_transmission_by_default: bool, + pub default_transmission_mode: TransmissionMode, + pub default_crc_type: ChecksumType, +} + #[derive(Debug, PartialEq, Eq, Copy, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct TransactionId { @@ -157,6 +167,78 @@ impl<'raw> PacketInfo<'raw> { #[cfg(test)] mod tests { + use spacepackets::cfdp::{ + lv::Lv, + pdu::{ + eof::EofPdu, + file_data::FileDataPdu, + metadata::{MetadataGenericParams, MetadataPdu}, + CommonPduConfig, FileDirectiveType, PduHeader, + }, + PduType, + }; + + use crate::cfdp::PacketTarget; + + use super::PacketInfo; + + fn generic_pdu_header() -> PduHeader { + let pdu_conf = CommonPduConfig::default(); + PduHeader::new_no_file_data(pdu_conf, 0) + } + #[test] - fn basic_test() {} + fn test_metadata_pdu_info() { + let mut buf: [u8; 128] = [0; 128]; + let pdu_header = generic_pdu_header(); + let metadata_params = MetadataGenericParams::default(); + let src_file_name = "hello.txt"; + let dest_file_name = "hello-dest.txt"; + let src_lv = Lv::new_from_str(src_file_name).unwrap(); + let dest_lv = Lv::new_from_str(dest_file_name).unwrap(); + let metadata_pdu = MetadataPdu::new(pdu_header, metadata_params, src_lv, dest_lv, None); + metadata_pdu + .write_to_bytes(&mut buf) + .expect("writing metadata PDU failed"); + + let packet_info = PacketInfo::new(&buf).expect("creating packet info failed"); + assert_eq!(packet_info.pdu_type(), PduType::FileDirective); + assert!(packet_info.pdu_directive().is_some()); + assert_eq!( + packet_info.pdu_directive().unwrap(), + FileDirectiveType::MetadataPdu + ); + assert_eq!(packet_info.target(), PacketTarget::DestEntity); + } + + #[test] + fn test_filedata_pdu_info() { + let mut buf: [u8; 128] = [0; 128]; + let pdu_header = generic_pdu_header(); + let file_data_pdu = FileDataPdu::new_no_seg_metadata(pdu_header, 0, &[]); + file_data_pdu + .write_to_bytes(&mut buf) + .expect("writing file data PDU failed"); + let packet_info = PacketInfo::new(&buf).expect("creating packet info failed"); + assert_eq!(packet_info.pdu_type(), PduType::FileData); + assert!(packet_info.pdu_directive().is_none()); + assert_eq!(packet_info.target(), PacketTarget::DestEntity); + } + + #[test] + fn test_eof_pdu_info() { + let mut buf: [u8; 128] = [0; 128]; + let pdu_header = generic_pdu_header(); + let eof_pdu = EofPdu::new_no_error(pdu_header, 0, 0); + eof_pdu + .write_to_bytes(&mut buf) + .expect("writing file data PDU failed"); + let packet_info = PacketInfo::new(&buf).expect("creating packet info failed"); + assert_eq!(packet_info.pdu_type(), PduType::FileDirective); + assert!(packet_info.pdu_directive().is_some()); + assert_eq!( + packet_info.pdu_directive().unwrap(), + FileDirectiveType::EofPdu + ); + } } From 609b3c11b116cb82e9d7037dc19fac1f940d3d2c Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sun, 10 Sep 2023 20:24:40 +0200 Subject: [PATCH 39/45] better name --- satrs-core/src/cfdp/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/satrs-core/src/cfdp/mod.rs b/satrs-core/src/cfdp/mod.rs index 22fe0aa..fd8d34f 100644 --- a/satrs-core/src/cfdp/mod.rs +++ b/satrs-core/src/cfdp/mod.rs @@ -15,7 +15,7 @@ pub mod dest; pub mod user; #[derive(Debug)] -pub struct RemoteConfig { +pub struct RemoteEntityConfig { pub entity_id: UnsignedByteField, pub max_file_segment_len: usize, pub closure_requeted_by_default: bool, From 6c47efc244a423d1b11e168537e17f028b264e27 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sun, 10 Sep 2023 20:53:12 +0200 Subject: [PATCH 40/45] wrote some docs --- satrs-core/src/cfdp/mod.rs | 65 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/satrs-core/src/cfdp/mod.rs b/satrs-core/src/cfdp/mod.rs index fd8d34f..a54541f 100644 --- a/satrs-core/src/cfdp/mod.rs +++ b/satrs-core/src/cfdp/mod.rs @@ -7,6 +7,8 @@ use spacepackets::{ util::UnsignedByteField, }; +#[cfg(feature = "alloc")] +use alloc::boxed::Box; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -14,6 +16,64 @@ use serde::{Deserialize, Serialize}; pub mod dest; pub mod user; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EntityType { + Sending, + Receiving, +} + +/// Generic abstraction for a check timer which has different functionality depending on whether +/// the using entity is the sending entity or the receiving entity for the unacknowledged +/// transmission mode. +/// +/// For the sending entity, this timer determines the expiry period for declaring a check limit +/// fault after sending an EOF PDU with requested closure. This allows a timeout of the +/// transfer. +/// +/// For the receiving entity, this timer determines the expiry period for incrementing a check +/// counter after an EOF PDU is received for an incomplete file transfer. This allows out-of-order +/// reception of file data PDUs and EOF PDUs. +pub trait CheckTimerProvider { + fn has_expired(&self) -> bool; +} + +#[cfg(feature = "alloc")] +pub trait CheckTimerCreator { + fn get_check_limit_provider( + local_id: &UnsignedByteField, + remote_id: &UnsignedByteField, + entity_type: EntityType, + ) -> Box; +} + +#[cfg(feature = "std")] +pub struct StdCheckTimer { + expiry_time_seconds: u64, + start_time: std::time::Instant +} + + +#[cfg(feature = "std")] +impl StdCheckTimer { + pub fn new(expiry_time_seconds: u64) -> Self { + Self { + expiry_time_seconds, + start_time: std::time::Instant::now() + } + } +} + +#[cfg(feature = "std")] +impl CheckTimerProvider for StdCheckTimer { + fn has_expired(&self) -> bool { + let elapsed_time = self.start_time.elapsed(); + if elapsed_time.as_secs() > self.expiry_time_seconds { + return true; + } + false + } +} + #[derive(Debug)] pub struct RemoteEntityConfig { pub entity_id: UnsignedByteField, @@ -22,6 +82,11 @@ pub struct RemoteEntityConfig { pub crc_on_transmission_by_default: bool, pub default_transmission_mode: TransmissionMode, pub default_crc_type: ChecksumType, + pub check_limit: u32, +} + +pub trait RemoteEntityConfigProvider { + fn get_remote_config(&self, remote_id: &UnsignedByteField) -> Option<&RemoteEntityConfig>; } #[derive(Debug, PartialEq, Eq, Copy, Clone)] From fc464d4078d0ef30ef2de095116bd754f9646b83 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sun, 10 Sep 2023 20:56:19 +0200 Subject: [PATCH 41/45] ref the chapter in CFDP --- satrs-core/src/cfdp/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/satrs-core/src/cfdp/mod.rs b/satrs-core/src/cfdp/mod.rs index a54541f..7604dfb 100644 --- a/satrs-core/src/cfdp/mod.rs +++ b/satrs-core/src/cfdp/mod.rs @@ -27,12 +27,12 @@ pub enum EntityType { /// transmission mode. /// /// For the sending entity, this timer determines the expiry period for declaring a check limit -/// fault after sending an EOF PDU with requested closure. This allows a timeout of the -/// transfer. +/// fault after sending an EOF PDU with requested closure. This allows a timeout of the transfer. +/// Also see 4.6.3.2 of the CFDP standard. /// /// For the receiving entity, this timer determines the expiry period for incrementing a check /// counter after an EOF PDU is received for an incomplete file transfer. This allows out-of-order -/// reception of file data PDUs and EOF PDUs. +/// reception of file data PDUs and EOF PDUs. Also see 4.6.3.3 of the CFDP standard. pub trait CheckTimerProvider { fn has_expired(&self) -> bool; } From 3ec6590c2369aa70565e15d8c034f23236749edb Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sun, 10 Sep 2023 21:04:02 +0200 Subject: [PATCH 42/45] I suppose that is a good start --- satrs-core/src/cfdp/mod.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/satrs-core/src/cfdp/mod.rs b/satrs-core/src/cfdp/mod.rs index 7604dfb..9fd3830 100644 --- a/satrs-core/src/cfdp/mod.rs +++ b/satrs-core/src/cfdp/mod.rs @@ -37,15 +37,25 @@ pub trait CheckTimerProvider { fn has_expired(&self) -> bool; } +/// A generic trait which allows CFDP entities to create check timers which are required to +/// implement special procedures in unacknowledged transmission mode, as specified in 4.6.3.2 +/// and 4.6.3.3. The [CheckTimerProvider] provides more information about the purpose of the +/// check timer. +/// +/// This trait also allows the creation of different check timers depending on +/// the ID of the local entity, the ID of the remote entity for a given transaction, and the +/// type of entity. #[cfg(feature = "alloc")] pub trait CheckTimerCreator { - fn get_check_limit_provider( + fn get_check_timer_provider( local_id: &UnsignedByteField, remote_id: &UnsignedByteField, entity_type: EntityType, ) -> Box; } +/// Simple implementation of the [CheckTimerProvider] trait assuming a standard runtime. +/// It also assumes that a second accuracy of the check timer period is sufficient. #[cfg(feature = "std")] pub struct StdCheckTimer { expiry_time_seconds: u64, From ead708b1bb1e5a29e45ec7e50fa3adcb07d01e93 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sun, 10 Sep 2023 21:11:44 +0200 Subject: [PATCH 43/45] add empty source handler --- satrs-core/src/cfdp/mod.rs | 2 ++ satrs-core/src/cfdp/source.rs | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 satrs-core/src/cfdp/source.rs diff --git a/satrs-core/src/cfdp/mod.rs b/satrs-core/src/cfdp/mod.rs index 9fd3830..a138fee 100644 --- a/satrs-core/src/cfdp/mod.rs +++ b/satrs-core/src/cfdp/mod.rs @@ -14,6 +14,8 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "std")] pub mod dest; +#[cfg(feature = "std")] +pub mod source; pub mod user; #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/satrs-core/src/cfdp/source.rs b/satrs-core/src/cfdp/source.rs new file mode 100644 index 0000000..8b17c11 --- /dev/null +++ b/satrs-core/src/cfdp/source.rs @@ -0,0 +1,16 @@ +use spacepackets::util::UnsignedByteField; + +pub struct SourceHandler { + id: UnsignedByteField, +} + +impl SourceHandler { + pub fn new(id: impl Into) -> Self { + Self { id } + } +} + +#[cfg(test)] +mod tests { + +} From ffcab9592e762de4cc66ae69646f52b7f29ccca6 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Thu, 21 Sep 2023 18:36:59 +0200 Subject: [PATCH 44/45] clippy fix --- satrs-core/src/cfdp/mod.rs | 5 ++--- satrs-core/src/cfdp/source.rs | 6 ++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/satrs-core/src/cfdp/mod.rs b/satrs-core/src/cfdp/mod.rs index a138fee..dc6e87a 100644 --- a/satrs-core/src/cfdp/mod.rs +++ b/satrs-core/src/cfdp/mod.rs @@ -61,16 +61,15 @@ pub trait CheckTimerCreator { #[cfg(feature = "std")] pub struct StdCheckTimer { expiry_time_seconds: u64, - start_time: std::time::Instant + start_time: std::time::Instant, } - #[cfg(feature = "std")] impl StdCheckTimer { pub fn new(expiry_time_seconds: u64) -> Self { Self { expiry_time_seconds, - start_time: std::time::Instant::now() + start_time: std::time::Instant::now(), } } } diff --git a/satrs-core/src/cfdp/source.rs b/satrs-core/src/cfdp/source.rs index 8b17c11..146f5be 100644 --- a/satrs-core/src/cfdp/source.rs +++ b/satrs-core/src/cfdp/source.rs @@ -6,11 +6,9 @@ pub struct SourceHandler { impl SourceHandler { pub fn new(id: impl Into) -> Self { - Self { id } + Self { id: id.into() } } } #[cfg(test)] -mod tests { - -} +mod tests {} From 520ee1755173ad7c8039c505ff8cb2956202832d Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Thu, 21 Sep 2023 18:44:33 +0200 Subject: [PATCH 45/45] some smaller fixes and tweaks --- satrs-core/src/cfdp/dest.rs | 2 +- satrs-core/src/cfdp/source.rs | 1 + satrs-core/src/executable.rs | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index d7ea7cc..b66365a 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -319,6 +319,7 @@ impl DestinationHandler { Ok(()) } + #[allow(clippy::needless_if)] pub fn handle_eof_pdu(&mut self, raw_packet: &[u8]) -> Result<(), DestError> { if self.state == State::Idle || self.step != TransactionStep::ReceivingFileDataPdus { return Err(DestError::WrongStateForFileDataAndEof); @@ -367,7 +368,6 @@ impl DestinationHandler { } fn fsm_nacked(&mut self, cfdp_user: &mut impl CfdpUser) -> Result<(), DestError> { - if self.step == TransactionStep::Idle {} if self.step == TransactionStep::TransactionStart { self.transaction_start(cfdp_user)?; } diff --git a/satrs-core/src/cfdp/source.rs b/satrs-core/src/cfdp/source.rs index 146f5be..433f4d2 100644 --- a/satrs-core/src/cfdp/source.rs +++ b/satrs-core/src/cfdp/source.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] use spacepackets::util::UnsignedByteField; pub struct SourceHandler { diff --git a/satrs-core/src/executable.rs b/satrs-core/src/executable.rs index 440f8ee..77ed178 100644 --- a/satrs-core/src/executable.rs +++ b/satrs-core/src/executable.rs @@ -29,7 +29,7 @@ pub trait Executable: Send { fn periodic_op(&mut self, op_code: i32) -> Result; } -/// This function allows executing one task which implements the [Executable][Executable] trait +/// This function allows executing one task which implements the [Executable] trait /// /// # Arguments /// @@ -78,7 +78,7 @@ pub fn exec_sched_single< } /// This function allows executing multiple tasks as long as the tasks implement the -/// [Executable][Executable] trait +/// [Executable] trait /// /// # Arguments ///