Compare commits

..

No commits in common. "main" and "python-interop-example" have entirely different histories.

16 changed files with 475 additions and 1561 deletions

View File

@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.81.0
- uses: dtolnay/rust-toolchain@1.75.0
- run: cargo check --release
cross-check:

View File

@ -7,12 +7,3 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
# [unreleased]
- Bumped `thiserror` to v2
- Bumped `spacepackets` to v0.13
- The source and destination handlers can now be used without the `std` feature and only require
the `alloc` feature.
# [v0.1.0] 2024-09-11
Initial release

View File

@ -18,49 +18,45 @@ name = "cfdp"
[dependencies]
crc = "3"
smallvec = "1"
derive-new = ">=0.6, <=0.7"
[dependencies.spacepackets]
version = "0.13"
default-features = false
derive-new = "0.6"
[dependencies.thiserror]
version = "2"
default-features = false
version = "1"
optional = true
[dependencies.hashbrown]
version = ">=0.14, <=0.15"
version = "0.14"
optional = true
[dependencies.serde]
version = "1"
optional = true
[dependencies.defmt]
version = "0.3"
optional = true
[dependencies.spacepackets]
version = "0.12"
default-features = false
git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets"
branch = "main"
[features]
default = ["std"]
std = [
"alloc",
"thiserror/std",
"thiserror",
"spacepackets/std"
]
alloc = [
"hashbrown",
"spacepackets/alloc"
]
serde = ["dep:serde", "spacepackets/serde", "hashbrown/serde"]
defmt = ["dep:defmt", "spacepackets/defmt"]
serde = ["dep:serde", "spacepackets/serde"]
[dev-dependencies]
tempfile = "3"
rand = "0.8"
log = "0.4"
fern = "0.7"
fern = "0.6"
chrono = "0.4"
clap = { version = "4", features = ["derive"] }
[package.metadata.docs.rs]
all-features = true

View File

@ -13,14 +13,10 @@ The underlying base packet library used to generate the packets to be sent is th
# Features
The goal of this library is to be flexible enough to support the use-cases of both on-board
software and of ground software. It has support to make integration on `std` systems as simple
as possible, but also has sufficient abstraction to allow for integration on`no_std` environments
and can be used on these systems as well as long as the `alloc` feature is activated.
Please note even though the `alloc` feature is required for the core handlers, these components
will only allocate memory at initialization time and thus are still viable for systems where
run-time allocation is prohibited.
`cfdp-rs` supports various runtime environments and is also suitable for `no_std` environments.
It is recommended to activate the `alloc` feature at the very least to allow using the primary
components provided by this crate. These components will only allocate memory at initialization
time and thus are still viable for systems where run-time allocation is prohibited.
## Default features

View File

@ -20,7 +20,7 @@ def generate_cov_report(open_report: bool, format: str):
out_path = "./target/debug/lcov.info"
os.system(
f"grcov . -s . --binary-path ./target/debug/ -t {format} --branch --ignore-not-existing "
f"--ignore \"examples/*\" -o {out_path}"
f"-o {out_path}"
)
if format == "lcov":
os.system(

View File

@ -1,37 +0,0 @@
Python Interoperability Example for cfdp-rs
=======
This example application showcases the interoperability of the CFDP handlers written in Rust
with a Python implementation which uses [cfdp-py](https://github.com/us-irs/cfdp-py) library.
Both the Rust and the Python app exchange packet data units via a UDP interface and launch
both a destination and source handler. As such, they are both able to send and receive files.
Both applications can be started with the command line argument `-f` to initiate a file transfer.
You can run both applications with `-h` to get more information about the available options.
## Running the Python App
It is recommended to run the Python App in a dedicated virtual environment. For example, on a
Unix system you can use `python3 -m venv venv` and then `source venv/bin/activate` to create
and activate a virtual environment.
After that, you can install the required dependencies using
```sh
pip install -r requirements.txt
```
and then run the application using `./main.py` or `python3 main.py`.
It is recommended to run `./main.py -h` first to get an overview of some possible options.
Running the Python App with `./main.py -f` will cause the Python App to start a file copy operation
with fixed temporary paths.
## Running the Rust App
You can run the Rust application using `cargo`, for example `cargo run --example python-interop`.
It is recommended to run `cargo run --example python-interop -- -h` to get an overview of some
possible launch options.
Running the Rust App with `cargo run --example python-interop -- -f` will cause the Rust app to
start a file copy operation with fixed temporary paths.

View File

@ -1,9 +1,7 @@
#!/usr/bin/env python3
from datetime import timedelta
from pathlib import Path
import os
import ipaddress
import tempfile
import socket
import select
import threading
@ -53,6 +51,8 @@ from spacepackets.cfdp.tlv.msg_to_user import ProxyPutResponseParams
from spacepackets.countdown import Countdown
from spacepackets.seqcount import SeqCountProvider
from spacepackets.util import ByteFieldU16, UnsignedByteField
from tmtccmd.config.cfdp import CfdpParams, generic_cfdp_params_to_put_request
from tmtccmd.config.args import add_cfdp_procedure_arguments, cfdp_args_to_cfdp_params
PYTHON_ENTITY_ID = ByteFieldU16(1)
@ -544,44 +544,23 @@ class CustomCheckTimerProvider(CheckTimerProvider):
def main():
parser = argparse.ArgumentParser(
prog="CFDP Local Entity Application",
formatter_class=argparse.RawTextHelpFormatter,
)
parser = argparse.ArgumentParser(prog="CFDP Local Entity Application")
parser.add_argument("-v", "--verbose", action="count", default=0)
parser.add_argument(
"-f",
help="Perform a file-copy operation",
action="store_true",
dest="file_copy",
)
parser.add_argument(
"-m",
"--mode",
dest="transmission_mode",
help=(
f"Specify the transfer type{os.linesep}"
f' - "0" or "ack" for unacknowledged (Class 0) transfers{os.linesep}'
f' - "1" or "nak" for acknowledged (Class 1) transfers. Default value'
),
default="nak",
)
# Optional Boolean argument where you can specify True/False
parser.add_argument(
"-c",
type=bool,
nargs="?",
const=True,
default=None,
dest="closure_requested",
help="Request transaction closure for the unacknowledged mode",
)
add_cfdp_procedure_arguments(parser)
args = parser.parse_args()
stop_signal = threading.Event()
logging_level = logging.INFO
if args.verbose >= 1:
logging_level = logging.DEBUG
if args.source is not None and args.target is not None:
# Generate a put request from the CLI arguments.
cfdp_params = CfdpParams()
cfdp_args_to_cfdp_params(args, cfdp_params)
put_req = generic_cfdp_params_to_put_request(
cfdp_params, PYTHON_ENTITY_ID, RUST_ENTITY_ID, PYTHON_ENTITY_ID
)
PUT_REQ_QUEUE.put(put_req)
logging.basicConfig(level=logging_level)
@ -632,6 +611,16 @@ def main():
local_addr = ipaddress.ip_address("0.0.0.0")
# Localhost as default.
remote_addr = ipaddress.ip_address("127.0.0.1")
"""
if Path(LOCAL_CFG_JSON_PATH).exists():
addr_from_cfg = parse_remote_addr_from_json(Path(LOCAL_CFG_JSON_PATH))
if addr_from_cfg is not None:
try:
remote_addr = ipaddress.ip_address(addr_from_cfg)
except ValueError:
_LOGGER.warning(f"invalid remote address {remote_addr} from JSON file")
"""
_LOGGER.info(f"Put request will be sent to remote destination {remote_addr}")
udp_server = UdpServer(
sleep_time=0.1,
addr=(str(local_addr), PY_PORT),
@ -642,27 +631,6 @@ def main():
stop_signal=stop_signal,
)
# Prepare a put request / file copy operation if the user specifies it.
if args.file_copy:
_LOGGER.info("Performing file copy operation")
transmission_mode = None
if args.transmission_mode == "ack":
transmission_mode = TransmissionMode.ACKNOWLEDGED
elif args.transmission_mode == "nak":
transmission_mode = TransmissionMode.UNACKNOWLEDGED
with tempfile.NamedTemporaryFile(delete=False) as srcfile:
srcfile.write(FILE_CONTENT.encode())
srcfile_path = srcfile.name
tempdir = tempfile.TemporaryDirectory()
put_req = PutRequest(
destination_id=RUST_ENTITY_ID,
source_file=Path(srcfile_path),
dest_file=Path(tempdir.name).joinpath("test.txt"),
closure_requested=args.closure_requested,
trans_mode=transmission_mode,
)
PUT_REQ_QUEUE.put(put_req)
source_entity_task.start()
dest_entity_task.start()
udp_server.start()

View File

@ -15,9 +15,8 @@ use cfdp::{
source::SourceHandler,
user::{CfdpUser, FileSegmentRecvdParams, MetadataReceivedParams, TransactionFinishedParams},
EntityType, IndicationConfig, LocalEntityConfig, PduOwnedWithInfo, PduProvider,
RemoteEntityConfig, StdTimerCreator, TransactionId, UserFaultHookProvider,
RemoteEntityConfig, StdCheckTimerCreator, TransactionId, UserFaultHookProvider,
};
use clap::Parser;
use log::{debug, info, warn};
use spacepackets::{
cfdp::{
@ -38,23 +37,6 @@ const LOG_LEVEL: log::LevelFilter = log::LevelFilter::Info;
const FILE_DATA: &str = "Hello World!";
#[derive(Debug, Copy, Clone, clap::ValueEnum)]
pub enum TransmissionModeCli {
Nak,
Ack,
}
#[derive(clap::Parser)]
#[command(about = "Arguments for executing a file copy operation")]
pub struct Cli {
#[arg(short, help = "Perform a file copy operation")]
file_copy: bool,
#[arg(short, default_value = "nak")]
mode: Option<TransmissionModeCli>,
#[arg(short)]
closure_requested: Option<bool>,
}
#[derive(Default)]
pub struct ExampleFaultHandler {}
@ -295,7 +277,6 @@ fn pdu_printout(pdu: &PduOwnedWithInfo) {
}
fn main() {
let cli_args = Cli::parse();
fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
@ -346,7 +327,6 @@ fn main() {
put_request_cacher,
2048,
remote_cfg_python,
StdTimerCreator::default(),
seq_count_provider,
);
let mut cfdp_user_source = ExampleCfdpUser::new(EntityType::Sending);
@ -362,27 +342,18 @@ fn main() {
dest_tm_tx,
NativeFilestore::default(),
remote_cfg_python,
StdTimerCreator::default(),
StdCheckTimerCreator::default(),
);
let mut cfdp_user_dest = ExampleCfdpUser::new(EntityType::Receiving);
let put_request = if cli_args.file_copy {
Some(
PutRequestOwned::new_regular_request(
let put_request = PutRequestOwned::new_regular_request(
PYTHON_ID.into(),
srcfile.to_str().expect("invaid path string"),
destfile.to_str().expect("invaid path string"),
cli_args.mode.map(|m| match m {
TransmissionModeCli::Ack => TransmissionMode::Acknowledged,
TransmissionModeCli::Nak => TransmissionMode::Unacknowledged,
}),
cli_args.closure_requested,
Some(TransmissionMode::Unacknowledged),
Some(true),
)
.expect("put request creation failed"),
)
} else {
None
};
.expect("put request creation failed");
let (source_tc_tx, source_tc_rx) = mpsc::channel();
let (dest_tc_tx, dest_tc_rx) = mpsc::channel();
@ -404,12 +375,9 @@ fn main() {
.name("cfdp src entity".to_string())
.spawn(move || {
info!("Starting RUST SRC");
if let Some(put_request) = put_request {
info!("RUST SRC: Performing put request: {:?}", put_request);
source_handler
.put_request(&put_request)
.expect("put request failed");
}
loop {
let mut next_delay = None;
let mut undelayed_call_count = 0;
@ -469,9 +437,7 @@ fn main() {
}
}
Err(e) => {
println!("Dest handler error: {}", e);
// TODO: I'd prefer a proper cancel request if a transfer is active..
dest_handler.reset();
println!("Source handler error: {}", e);
next_delay = Some(Duration::from_millis(50));
}
}

View File

@ -1 +1,2 @@
cfdp-py @ git+https://github.com/us-irs/cfdp-py.git@main
tmtccmd == 8.0.2

File diff suppressed because it is too large Load Diff

View File

@ -1,35 +1,90 @@
use alloc::string::{String, ToString};
use core::fmt::Display;
use spacepackets::cfdp::ChecksumType;
use spacepackets::ByteConversionError;
#[cfg(feature = "std")]
use std::error::Error;
use std::path::Path;
#[cfg(feature = "std")]
pub use std_mod::*;
#[derive(Debug, thiserror::Error)]
#[cfg_attr(all(feature = "defmt", not(feature = "std")), derive(defmt::Format))]
#[non_exhaustive]
#[derive(Debug, Clone)]
pub enum FilestoreError {
#[error("file does not exist")]
FileDoesNotExist,
#[error("file already exists")]
FileAlreadyExists,
#[error("directory does not exist")]
DirDoesNotExist,
#[error("permission error")]
Permission,
#[error("is not a file")]
IsNotFile,
#[error("is not a directory")]
IsNotDirectory,
#[error("byte conversion: {0}")]
ByteConversion(#[from] ByteConversionError),
#[error("IO error: {0})")]
#[cfg(feature = "std")]
Io(#[from] std::io::Error),
#[error("checksum type not implemented: {0:?}")]
ByteConversion(ByteConversionError),
Io {
raw_errno: Option<i32>,
string: String,
},
ChecksumTypeNotImplemented(ChecksumType),
#[error("utf8 error")]
Utf8Error,
#[error("other error")]
Other,
}
impl From<ByteConversionError> for FilestoreError {
fn from(value: ByteConversionError) -> Self {
Self::ByteConversion(value)
}
}
impl Display for FilestoreError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
FilestoreError::FileDoesNotExist => {
write!(f, "file does not exist")
}
FilestoreError::FileAlreadyExists => {
write!(f, "file already exists")
}
FilestoreError::DirDoesNotExist => {
write!(f, "directory does not exist")
}
FilestoreError::Permission => {
write!(f, "permission error")
}
FilestoreError::IsNotFile => {
write!(f, "is not a file")
}
FilestoreError::IsNotDirectory => {
write!(f, "is not a directory")
}
FilestoreError::ByteConversion(e) => {
write!(f, "filestore error: {e}")
}
FilestoreError::Io { raw_errno, string } => {
write!(
f,
"filestore generic IO error with raw errno {:?}: {}",
raw_errno, string
)
}
FilestoreError::ChecksumTypeNotImplemented(checksum_type) => {
write!(f, "checksum {:?} not implemented", checksum_type)
}
}
}
}
impl Error for FilestoreError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
FilestoreError::ByteConversion(e) => Some(e),
_ => None,
}
}
}
#[cfg(feature = "std")]
impl From<std::io::Error> for FilestoreError {
fn from(value: std::io::Error) -> Self {
Self::Io {
raw_errno: value.raw_os_error(),
string: value.to_string(),
}
}
}
pub trait VirtualFilestore {
@ -56,7 +111,14 @@ pub trait VirtualFilestore {
fn filename_from_full_path(path: &str) -> Option<&str>
where
Self: Sized;
Self: Sized,
{
// Convert the path string to a Path
let path = Path::new(path);
// Extract the file name using the file_name() method
path.file_name().and_then(|name| name.to_str())
}
fn is_file(&self, path: &str) -> Result<bool, FilestoreError>;
@ -66,11 +128,6 @@ pub trait VirtualFilestore {
fn exists(&self, path: &str) -> Result<bool, FilestoreError>;
/// Extract the file name part of a full path.
///
/// This method should behave similarly to the [std::path::Path::file_name] method.
fn file_name<'a>(&self, full_path: &'a str) -> Result<Option<&'a str>, FilestoreError>;
fn file_size(&self, path: &str) -> Result<u64, FilestoreError>;
/// This special function is the CFDP specific abstraction to calculate the checksum of a file.
@ -84,7 +141,6 @@ pub trait VirtualFilestore {
&self,
file_path: &str,
checksum_type: ChecksumType,
size_to_verify: u64,
verification_buf: &mut [u8],
) -> Result<u32, FilestoreError>;
@ -97,14 +153,13 @@ pub trait VirtualFilestore {
/// 4096 or 8192 bytes.
fn checksum_verify(
&self,
expected_checksum: u32,
file_path: &str,
checksum_type: ChecksumType,
size_to_verify: u64,
expected_checksum: u32,
verification_buf: &mut [u8],
) -> Result<bool, FilestoreError> {
Ok(
self.calculate_checksum(file_path, checksum_type, size_to_verify, verification_buf)?
self.calculate_checksum(file_path, checksum_type, verification_buf)?
== expected_checksum,
)
}
@ -121,7 +176,6 @@ pub mod std_mod {
use std::{
fs::{self, File, OpenOptions},
io::{BufReader, Read, Seek, SeekFrom, Write},
path::Path,
};
#[derive(Default)]
@ -147,13 +201,6 @@ pub mod std_mod {
Ok(())
}
fn file_name<'a>(&self, full_path: &'a str) -> Result<Option<&'a str>, FilestoreError> {
let path = Path::new(full_path);
path.file_name()
.map(|s| s.to_str())
.ok_or(FilestoreError::Utf8Error)
}
fn truncate_file(&self, file_path: &str) -> Result<(), FilestoreError> {
if !self.exists(file_path)? {
return Err(FilestoreError::FileDoesNotExist);
@ -169,7 +216,10 @@ pub mod std_mod {
}
fn create_dir(&self, dir_path: &str) -> Result<(), FilestoreError> {
fs::create_dir(dir_path)?;
fs::create_dir(dir_path).map_err(|e| FilestoreError::Io {
raw_errno: e.raw_os_error(),
string: e.to_string(),
})?;
Ok(())
}
@ -255,23 +305,18 @@ pub mod std_mod {
&self,
file_path: &str,
checksum_type: ChecksumType,
size_to_verify: u64,
verification_buf: &mut [u8],
) -> Result<u32, FilestoreError> {
let mut calc_with_crc_lib = |crc: Crc<u32>| -> Result<u32, FilestoreError> {
let mut digest = crc.digest();
let mut buf_reader = BufReader::new(File::open(file_path)?);
let mut remaining_bytes = size_to_verify;
while remaining_bytes > 0 {
// Read the smaller of the remaining bytes or the buffer size
let bytes_to_read = remaining_bytes.min(verification_buf.len() as u64) as usize;
let bytes_read = buf_reader.read(&mut verification_buf[0..bytes_to_read])?;
let file_to_check = File::open(file_path)?;
let mut buf_reader = BufReader::new(file_to_check);
loop {
let bytes_read = buf_reader.read(verification_buf)?;
if bytes_read == 0 {
break; // Reached end of file
break;
}
digest.update(&verification_buf[0..bytes_read]);
remaining_bytes -= bytes_read as u64;
}
Ok(digest.finalize())
};
@ -283,17 +328,6 @@ pub mod std_mod {
_ => Err(FilestoreError::ChecksumTypeNotImplemented(checksum_type)),
}
}
fn filename_from_full_path(path: &str) -> Option<&str>
where
Self: Sized,
{
// Convert the path string to a Path
let path = Path::new(path);
// Extract the file name using the file_name() method
path.file_name().and_then(|name| name.to_str())
}
}
impl NativeFilestore {
@ -329,7 +363,7 @@ pub mod std_mod {
#[cfg(test)]
mod tests {
use std::{fs, path::Path, println, string::ToString};
use std::{fs, path::Path, println};
use super::*;
use alloc::format;
@ -642,7 +676,7 @@ mod tests {
}
assert_eq!(
error.to_string(),
format!("byte conversion: {}", byte_conv_error)
format!("filestore error: {}", byte_conv_error)
);
} else {
panic!("unexpected error");
@ -721,10 +755,9 @@ mod tests {
checksum = checksum.wrapping_add(u32::from_be_bytes(buffer));
let mut verif_buf: [u8; 32] = [0; 32];
let result = NATIVE_FS.checksum_verify(
checksum,
file_path.to_str().unwrap(),
ChecksumType::Modular,
EXAMPLE_DATA_CFDP.len() as u64,
checksum,
&mut verif_buf,
);
assert!(result.is_ok());
@ -737,7 +770,6 @@ mod tests {
// The file to check does not even need to exist, and the verification buffer can be
// empty: the null checksum is always yields the same result.
let result = NATIVE_FS.checksum_verify(
0,
file_path.to_str().unwrap(),
ChecksumType::NullChecksum,
0,
@ -754,7 +786,6 @@ mod tests {
// The file to check does not even need to exist, and the verification buffer can be
// empty: the null checksum is always yields the same result.
let result = NATIVE_FS.checksum_verify(
0,
file_path.to_str().unwrap(),
ChecksumType::Crc32Proximity1,
0,
@ -765,7 +796,7 @@ mod tests {
if let FilestoreError::ChecksumTypeNotImplemented(cksum_type) = error {
assert_eq!(
error.to_string(),
format!("checksum type not implemented: {:?}", cksum_type)
format!("checksum {:?} not implemented", cksum_type)
);
} else {
panic!("unexpected error");

View File

@ -1,69 +1,5 @@
//! This module contains the implementation of the CCSDS File Delivery Protocol (CFDP) high level
//! abstractions as specified in CCSDS 727.0-B-5.
//!
//! The basic idea of CFDP is to convert files of any size into a stream of packets called packet
//! data units (PDU). CFPD has an unacknowledged and acknowledged mode, with the option to request
//! a transaction closure for the unacknowledged mode. Using the unacknowledged mode with no
//! transaction closure is applicable for simplex communication paths, while the unacknowledged
//! mode with closure is the easiest way to get a confirmation of a successful file transfer,
//! including a CRC check on the remote side to verify file integrity. The acknowledged mode is
//! the most complex mode which includes multiple mechanism to ensure succesfull packet transaction
//! even for unreliable connections, including lost segment detection. As such, it can be compared
//! to a specialized TCP for file transfers with remote systems.
//!
//! The goal of this library is to be flexible enough to support the use-cases of both on-board
//! software and of ground software. It has support to make integration on [std] systems as simple
//! as possible, but also has sufficient abstraction to allow for integration on `no_std`
//! environments and can be used on these systems as well as long as the [alloc] feature is used
//! as well.
//!
//! Please note even though the [alloc] feature is required for the core handlers, these components
//! will only allocate memory at initialization time and thus are still viable for systems where
//! run-time allocation is prohibited.
//!
//! The core of this library are the [crate::dest::DestinationHandler] and the
//! [crate::source::SourceHandler] components which model the CFDP destination and source entity
//! respectively. You can find high-level and API documentation for both handlers in the respective
//! [crate::dest] and [crate::source] module.
//!
//! # Examples
//!
//! This library currently features two example application which showcase how the provided
//! components could be used to provide CFDP services.
//!
//! The [end-to-end test](https://egit.irs.uni-stuttgart.de/rust/cfdp/src/branch/main/tests/end-to-end.rs)
//! is an integration tests which spawns a CFDP source entity and a CFDP destination entity,
//! moves them to separate threads and then performs a small file copy operation.
//! You can run the integration test for a transfer with no closure and with printout to the
//! standard console by running:
//!
//! ```sh
//! cargo test end_to_end_test_no_closure -- --nocapture
//! ```
//!
//! or with closure:
//!
//! ```sh
//! cargo test end_to_end_test_with_closure -- --nocapture
//! ```
//!
//! The [Python Interoperability](https://egit.irs.uni-stuttgart.de/rust/cfdp/src/branch/main/examples/python-interop)
//! example showcases the interoperability of the CFDP handlers written in Rust with a Python
//! implementation. The dedicated example documentation shows how to run this example.
//!
//! # Notes on the user hooks and scheduling
//!
//! Both examples feature implementations of the [UserFaultHookProvider] and the [user::CfdpUser]
//! trait which simply print some information to the console to monitor the progress of a file
//! copy operation. These implementations could be adapted for other handler integrations. For
//! example, they could signal a GUI application to display some information for the user.
//!
//! Even though both examples move the newly spawned handlers to dedicated threads, this is not
//! the only way they could be scheduled. For example, to support an arbitrary (or bounded)
//! amount of file copy operations on either source or destination side, those handlers could be
//! moved into a [std::collections::HashMap] structure which is then scheduled inside a thread, or
//! you could schedule a fixed amount of handlers inside a
//! [threadpool](https://docs.rs/threadpool/latest/threadpool/).
//! This module contains the implementation of the CFDP high level abstractions as specified in
//! CCSDS 727.0-B-5.
#![no_std]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#[cfg(feature = "alloc")]
@ -71,11 +7,12 @@ extern crate alloc;
#[cfg(any(feature = "std", test))]
extern crate std;
#[cfg(feature = "alloc")]
#[cfg(feature = "std")]
pub mod dest;
#[cfg(feature = "alloc")]
pub mod filestore;
pub mod request;
#[cfg(feature = "alloc")]
#[cfg(feature = "std")]
pub mod source;
pub mod time;
pub mod user;
@ -83,10 +20,11 @@ pub mod user;
use crate::time::CountdownProvider;
use core::{cell::RefCell, fmt::Debug, hash::Hash};
use crc::{Crc, CRC_32_ISCSI, CRC_32_ISO_HDLC};
#[cfg(feature = "std")]
use hashbrown::HashMap;
#[cfg(feature = "alloc")]
pub use alloc_mod::*;
use core::time::Duration;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use spacepackets::{
@ -97,19 +35,16 @@ use spacepackets::{
util::{UnsignedByteField, UnsignedEnum},
};
#[cfg(feature = "std")]
use std::time::Duration;
#[cfg(feature = "std")]
pub use std_mod::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum EntityType {
Sending,
Receiving,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum TimerContext {
CheckLimit {
local_id: UnsignedByteField,
@ -117,10 +52,10 @@ pub enum TimerContext {
entity_type: EntityType,
},
NakActivity {
expiry_time: Duration,
expiry_time_seconds: f32,
},
PositiveAck {
expiry_time: Duration,
expiry_time_seconds: f32,
},
}
@ -161,10 +96,10 @@ pub enum TimerContext {
/// The timer will be used to perform the Positive Acknowledgement Procedures as specified in
/// 4.7. 1of the CFDP standard. The expiration period will be provided by the Positive ACK timer
/// interval of the remote entity configuration.
pub trait TimerCreatorProvider {
type Countdown: CountdownProvider;
pub trait CheckTimerProviderCreator {
type CheckTimer: CountdownProvider;
fn create_countdown(&self, timer_context: TimerContext) -> Self::Countdown;
fn create_check_timer_provider(&self, timer_context: TimerContext) -> Self::CheckTimer;
}
/// This structure models the remote entity configuration information as specified in chapter 8.3
@ -227,8 +162,6 @@ pub trait TimerCreatorProvider {
/// * `nak_timer_expiration_limit` - See the notes on the Deferred Lost Segment Procedure inside
/// the class documentation. Defaults to 2, so the timer may expire two times.
#[derive(Debug, Copy, Clone)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct RemoteEntityConfig {
pub entity_id: UnsignedByteField,
pub max_packet_len: usize,
@ -286,12 +219,9 @@ pub trait RemoteEntityConfigProvider {
fn remove_config(&mut self, remote_id: u64) -> bool;
}
/// This is a thin wrapper around a [hashbrown::HashMap] to store remote entity configurations.
/// It implements the full [RemoteEntityConfigProvider] trait.
#[cfg(feature = "alloc")]
#[derive(Default, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct StdRemoteEntityConfigProvider(pub hashbrown::HashMap<u64, RemoteEntityConfig>);
#[cfg(feature = "std")]
#[derive(Default)]
pub struct StdRemoteEntityConfigProvider(pub HashMap<u64, RemoteEntityConfig>);
#[cfg(feature = "std")]
impl RemoteEntityConfigProvider for StdRemoteEntityConfigProvider {
@ -309,12 +239,8 @@ impl RemoteEntityConfigProvider for StdRemoteEntityConfigProvider {
}
}
/// This is a thin wrapper around a [alloc::vec::Vec] to store remote entity configurations.
/// It implements the full [RemoteEntityConfigProvider] trait.
#[cfg(feature = "alloc")]
#[derive(Default, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Default)]
pub struct VecRemoteEntityConfigProvider(pub alloc::vec::Vec<RemoteEntityConfig>);
#[cfg(feature = "alloc")]
@ -347,9 +273,6 @@ impl RemoteEntityConfigProvider for VecRemoteEntityConfigProvider {
}
}
/// A remote entity configurations also implements the [RemoteEntityConfigProvider], but the
/// [RemoteEntityConfigProvider::add_config] and [RemoteEntityConfigProvider::remove_config]
/// are no-ops and always returns [false].
impl RemoteEntityConfigProvider for RemoteEntityConfig {
fn get(&self, remote_id: u64) -> Option<&RemoteEntityConfig> {
if remote_id == self.entity_id.value() {
@ -377,7 +300,7 @@ impl RemoteEntityConfigProvider for RemoteEntityConfig {
/// This trait introduces some callbacks which will be called when a particular CFDP fault
/// handler is called.
///
/// It is passed into the CFDP handlers as part of the [UserFaultHookProvider] and the local entity
/// It is passed into the CFDP handlers as part of the [DefaultFaultHandler] and the local entity
/// configuration and provides a way to specify custom user error handlers. This allows to
/// implement some CFDP features like fault handler logging, which would not be possible
/// generically otherwise.
@ -404,8 +327,6 @@ pub trait UserFaultHookProvider {
fn ignore_cb(&mut self, transaction_id: TransactionId, cond: ConditionCode, progress: u64);
}
/// Dummy fault hook which implements [UserFaultHookProvider] but only provides empty
/// implementations.
#[derive(Default, Debug, PartialEq, Eq, Copy, Clone)]
pub struct DummyFaultHook {}
@ -443,7 +364,7 @@ impl UserFaultHookProvider for DummyFaultHook {
/// It does so by mapping each applicable [spacepackets::cfdp::ConditionCode] to a fault handler
/// which is denoted by the four [spacepackets::cfdp::FaultHandlerCode]s. This code is used
/// to select the error handling inside the CFDP handler itself in addition to dispatching to a
/// user-provided callback function provided by the [UserFaultHookProvider].
/// user-provided callback function provided by the [UserFaultHandler].
///
/// Some note on the provided default settings:
///
@ -544,9 +465,6 @@ impl<UserHandler: UserFaultHookProvider> FaultHandler<UserHandler> {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct IndicationConfig {
pub eof_sent: bool,
pub eof_recv: bool,
@ -569,7 +487,6 @@ impl Default for IndicationConfig {
}
}
/// Each CFDP entity handler has a [LocalEntityConfig]uration.
pub struct LocalEntityConfig<UserFaultHook: UserFaultHookProvider> {
pub id: UnsignedByteField,
pub indication_cfg: IndicationConfig,
@ -646,29 +563,31 @@ pub mod std_mod {
}
}
/// Simple implementation of the [CountdownProvider] trait assuming a standard runtime.
/// Simple implementation of the [CheckTimerCreator] trait assuming a standard runtime.
/// It also assumes that a second accuracy of the check timer period is sufficient.
#[derive(Debug)]
pub struct StdCountdown {
expiry_time: Duration,
pub struct StdCheckTimer {
expiry_time_seconds: u64,
start_time: std::time::Instant,
}
impl StdCountdown {
pub fn new(expiry_time: Duration) -> Self {
impl StdCheckTimer {
pub fn new(expiry_time_seconds: u64) -> Self {
Self {
expiry_time,
expiry_time_seconds,
start_time: std::time::Instant::now(),
}
}
pub fn expiry_time_seconds(&self) -> u64 {
self.expiry_time.as_secs()
self.expiry_time_seconds
}
}
impl CountdownProvider for StdCountdown {
impl CountdownProvider for StdCheckTimer {
fn has_expired(&self) -> bool {
if self.start_time.elapsed() > self.expiry_time {
let elapsed_time = self.start_time.elapsed();
if elapsed_time.as_nanos() > self.expiry_time_seconds as u128 * 1_000_000_000 {
return true;
}
false
@ -679,36 +598,40 @@ pub mod std_mod {
}
}
pub struct StdTimerCreator {
pub check_limit_timeout: Duration,
pub struct StdCheckTimerCreator {
pub check_limit_timeout_secs: u64,
}
impl StdTimerCreator {
pub const fn new(check_limit_timeout: Duration) -> Self {
impl StdCheckTimerCreator {
pub const fn new(check_limit_timeout_secs: u64) -> Self {
Self {
check_limit_timeout,
check_limit_timeout_secs,
}
}
}
impl Default for StdTimerCreator {
impl Default for StdCheckTimerCreator {
fn default() -> Self {
Self::new(Duration::from_secs(5))
Self::new(5)
}
}
impl TimerCreatorProvider for StdTimerCreator {
type Countdown = StdCountdown;
impl CheckTimerProviderCreator for StdCheckTimerCreator {
type CheckTimer = StdCheckTimer;
fn create_countdown(&self, timer_context: TimerContext) -> Self::Countdown {
fn create_check_timer_provider(&self, timer_context: TimerContext) -> Self::CheckTimer {
match timer_context {
TimerContext::CheckLimit {
local_id: _,
remote_id: _,
entity_type: _,
} => StdCountdown::new(self.check_limit_timeout),
TimerContext::NakActivity { expiry_time } => StdCountdown::new(expiry_time),
TimerContext::PositiveAck { expiry_time } => StdCountdown::new(expiry_time),
} => StdCheckTimer::new(self.check_limit_timeout_secs),
TimerContext::NakActivity {
expiry_time_seconds,
} => StdCheckTimer::new(Duration::from_secs_f32(expiry_time_seconds).as_secs()),
TimerContext::PositiveAck {
expiry_time_seconds,
} => StdCheckTimer::new(Duration::from_secs_f32(expiry_time_seconds).as_secs()),
}
}
}
@ -718,7 +641,6 @@ pub mod std_mod {
/// number of that transfer which is also determined by the CFDP source entity.
#[derive(Debug, Eq, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct TransactionId {
source_id: UnsignedByteField,
seq_num: UnsignedByteField,
@ -760,15 +682,11 @@ pub enum State {
Suspended = 2,
}
/// [crc::Crc] instance using [crc::CRC_32_ISO_HDLC].
///
/// SANA registry entry: <https://sanaregistry.org/r/checksum_identifiers/records/4>,
/// Entry in CRC catalogue: <https://reveng.sourceforge.io/crc-catalogue/all.htm#crc.cat.crc-32>
/// SANA registry entry: https://sanaregistry.org/r/checksum_identifiers/records/4
/// Entry in CRC catalogue: https://reveng.sourceforge.io/crc-catalogue/all.htm#crc.cat.crc-32
pub const CRC_32: Crc<u32> = Crc::<u32>::new(&CRC_32_ISO_HDLC);
/// [crc::Crc] instance using [crc::CRC_32_ISCSI].
///
/// SANA registry entry: <https://sanaregistry.org/r/checksum_identifiers/records/3>,
/// Entry in CRC catalogue: <https://reveng.sourceforge.io/crc-catalogue/all.htm#crc.cat.crc-32-iscsi>
/// SANA registry entry: https://sanaregistry.org/r/checksum_identifiers/records/3
/// Entry in CRC catalogue: https://reveng.sourceforge.io/crc-catalogue/all.htm#crc.cat.crc-32-iscsi
pub const CRC_32C: Crc<u32> = Crc::<u32>::new(&CRC_32_ISCSI);
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
@ -778,8 +696,6 @@ pub enum PacketTarget {
DestEntity,
}
/// Generic trait which models a raw CFDP packet data unit (PDU) block with some additional context
/// information.
pub trait PduProvider {
fn pdu_type(&self) -> PduType;
fn file_directive_type(&self) -> Option<FileDirectiveType>;
@ -877,7 +793,7 @@ impl<'raw> PduRawWithInfo<'raw> {
});
}
if pdu_header.pdu_datafield_len() < 1 {
return Err(PduError::Format);
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).
@ -1003,7 +919,7 @@ pub(crate) mod tests {
};
use user::{CfdpUser, OwnedMetadataRecvdParams, TransactionFinishedParams};
use crate::{PacketTarget, StdCountdown};
use crate::{PacketTarget, StdCheckTimer};
use super::*;
@ -1353,7 +1269,7 @@ pub(crate) mod tests {
#[test]
fn test_std_check_timer() {
let mut std_check_timer = StdCountdown::new(Duration::from_secs(1));
let mut std_check_timer = StdCheckTimer::new(1);
assert!(!std_check_timer.has_expired());
assert_eq!(std_check_timer.expiry_time_seconds(), 1);
std::thread::sleep(Duration::from_millis(800));
@ -1366,9 +1282,10 @@ pub(crate) mod tests {
#[test]
fn test_std_check_timer_creator() {
let std_check_timer_creator = StdTimerCreator::new(Duration::from_secs(1));
let check_timer = std_check_timer_creator.create_countdown(TimerContext::NakActivity {
expiry_time: Duration::from_secs(1),
let std_check_timer_creator = StdCheckTimerCreator::new(1);
let check_timer =
std_check_timer_creator.create_check_timer_provider(TimerContext::NakActivity {
expiry_time_seconds: 1.0,
});
assert_eq!(check_timer.expiry_time_seconds(), 1);
}
@ -1546,7 +1463,7 @@ pub(crate) mod tests {
#[test]
fn transaction_id_hashable_usable_as_map_key() {
let mut map = hashbrown::HashMap::new();
let mut map = HashMap::new();
let transaction_id_0 = TransactionId::new(
UnsignedByteFieldU8::new(1).into(),
UnsignedByteFieldU8::new(2).into(),

View File

@ -10,8 +10,6 @@ use spacepackets::{
pub use alloc_mod::*;
#[derive(Debug, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FilePathTooLarge(pub usize);
/// This trait is an abstraction for different Put Request structures which can be used
@ -169,8 +167,6 @@ impl<'src_file, 'dest_file> PutRequest<'src_file, 'dest_file, 'static, 'static,
}
#[derive(Debug, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TlvWithInvalidType(pub(crate) ());
impl<'msgs_to_user> PutRequest<'static, 'static, 'msgs_to_user, 'static, 'static, 'static> {
@ -240,7 +236,6 @@ pub mod alloc_mod {
/// Owned variant of [PutRequest] with no lifetimes which is also [Clone]able.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct PutRequestOwned {
pub destination_id: UnsignedByteField,
source_file: Option<alloc::string::String>,

File diff suppressed because it is too large Load Diff

View File

@ -15,8 +15,6 @@ use spacepackets::{
use super::TransactionId;
#[derive(Debug, Copy, Clone)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TransactionFinishedParams {
pub id: TransactionId,
pub condition_code: ConditionCode,

View File

@ -14,7 +14,7 @@ use cfdp::{
source::SourceHandler,
user::{CfdpUser, FileSegmentRecvdParams, MetadataReceivedParams, TransactionFinishedParams},
EntityType, IndicationConfig, LocalEntityConfig, PduOwnedWithInfo, RemoteEntityConfig,
StdTimerCreator, TransactionId, UserFaultHookProvider,
StdCheckTimerCreator, TransactionId, UserFaultHookProvider,
};
use spacepackets::{
cfdp::{ChecksumType, ConditionCode, TransmissionMode},
@ -202,7 +202,6 @@ fn end_to_end_test(with_closure: bool) {
put_request_cacher,
2048,
remote_cfg_of_dest,
StdTimerCreator::default(),
seq_count_provider,
);
let mut cfdp_user_source = ExampleCfdpUser::new(EntityType::Sending, completion_signal_source);
@ -226,7 +225,7 @@ fn end_to_end_test(with_closure: bool) {
dest_tx,
NativeFilestore::default(),
remote_cfg_of_source,
StdTimerCreator::default(),
StdCheckTimerCreator::default(),
);
let mut cfdp_user_dest = ExampleCfdpUser::new(EntityType::Receiving, completion_signal_dest);